diff --git a/README.md b/README.md index 23b366c..7217d7d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ pip install -r requirements.txt python app.py fantasyidler_save.json ``` -The browser opens automatically at `http://127.0.0.1:5000/v/local/`. +The browser opens automatically at your personal viewer URL (e.g. `http://127.0.0.1:5000/v//`). The id is stored in `data/cli-viewer-id` and reused on the next start. ### Docker (host for other players) @@ -93,7 +93,7 @@ Each viewer has its own SQLite database at `data/viewers/.db`. The `viewer_id` is a random URL-safe token. Anyone with the link has access — there is no password and no recovery if the link is lost. -Local CLI usage defaults to the `local` viewer (`/v/local/`). In Docker/production, `/v/local/` is disabled (`DISABLE_LOCAL_VIEWER=1`). +Local CLI usage creates a persistent personal viewer (`data/cli-viewer-id`). Use `--viewer local` for the shared dev viewer at `/v/local/`. In Docker/production, `/v/local/` is disabled (`DISABLE_LOCAL_VIEWER=1`). ## Security diff --git a/app.py b/app.py index 1b2ccce..ed561af 100644 --- a/app.py +++ b/app.py @@ -36,6 +36,7 @@ from viewers import ( LOCAL_VIEWER_ID, create_viewer, ensure_local_viewer, + get_or_create_cli_viewer, is_valid_viewer_id, viewer_db_path, ) @@ -193,7 +194,11 @@ def main() -> int: parser.add_argument("--host", default="127.0.0.1", help="Bind host (use 0.0.0.0 in Docker)") parser.add_argument("--no-browser", action="store_true") parser.add_argument("--db", type=Path, help="SQLite path (legacy single-file mode)") - parser.add_argument("--viewer", default=LOCAL_VIEWER_ID, help="Viewer id for CLI (default: local)") + parser.add_argument( + "--viewer", + metavar="ID", + help="Viewer id for CLI (default: persistent personal viewer; use 'local' for shared dev viewer)", + ) args = parser.parse_args() global DATA_DIR, DB_PATH @@ -204,7 +209,12 @@ def main() -> int: db_path = DB_PATH viewer_id = None else: - viewer_id = ensure_local_viewer(DATA_DIR) if args.viewer == LOCAL_VIEWER_ID else args.viewer + if args.viewer == LOCAL_VIEWER_ID: + viewer_id = ensure_local_viewer(DATA_DIR) + elif args.viewer: + viewer_id = args.viewer + else: + viewer_id = get_or_create_cli_viewer(DATA_DIR) if not is_valid_viewer_id(viewer_id): print(f"Error: invalid viewer id: {viewer_id}", file=sys.stderr) return 1 @@ -250,6 +260,8 @@ def main() -> int: else: url = f"http://{args.host}:{args.port}/" print(f"Starting server at {url}") + if viewer_id and viewer_id != LOCAL_VIEWER_ID: + print(f"Personal viewer URL: {url}") if not args.no_browser and args.host in ("127.0.0.1", "localhost"): webbrowser.open(url) app.run(host=args.host, port=args.port, debug=False) diff --git a/viewers.py b/viewers.py index a3bee84..eaafe2a 100644 --- a/viewers.py +++ b/viewers.py @@ -53,7 +53,7 @@ def create_viewer(data_dir: Path) -> str: def ensure_local_viewer(data_dir: Path) -> str: - """CLI default viewer – not secret, for local single-user use.""" + """Shared dev viewer – predictable path, not secret.""" db_path = viewer_db_path(LOCAL_VIEWER_ID, data_dir) if db_path.exists(): return LOCAL_VIEWER_ID @@ -61,3 +61,21 @@ def ensure_local_viewer(data_dir: Path) -> str: init_db(conn) conn.close() return LOCAL_VIEWER_ID + + +def cli_viewer_marker(data_dir: Path) -> Path: + return data_dir / "cli-viewer-id" + + +def get_or_create_cli_viewer(data_dir: Path) -> str: + """Persistent personal viewer for local CLI starts.""" + data_dir.mkdir(parents=True, exist_ok=True) + marker = cli_viewer_marker(data_dir) + if marker.exists(): + viewer_id = marker.read_text(encoding="utf-8").strip() + if is_valid_viewer_id(viewer_id) and viewer_exists(viewer_id, data_dir): + return viewer_id + + viewer_id = create_viewer(data_dir) + marker.write_text(viewer_id, encoding="utf-8") + return viewer_id