Add Docker deployment and per-player secret-link viewers.

Each player gets an isolated SQLite viewer via a unique URL without login, with landing page warnings to save the link and compose-based hosting for sharing with others.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-19 16:06:13 +02:00
parent fbc2deec45
commit f51f166fa1
14 changed files with 589 additions and 53 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Per-viewer isolation via secret URL tokens (no login)."""
from __future__ import annotations
import re
import secrets
from pathlib import Path
from db import get_connection, init_db
VIEWER_ID_RE = re.compile(r"^[A-Za-z0-9_-]{16,64}$")
LOCAL_VIEWER_ID = "local"
def generate_viewer_id() -> str:
return secrets.token_urlsafe(16)
def is_valid_viewer_id(viewer_id: str) -> bool:
if viewer_id == LOCAL_VIEWER_ID:
return True
return bool(viewer_id and VIEWER_ID_RE.match(viewer_id))
def viewers_dir(data_dir: Path) -> Path:
return data_dir / "viewers"
def viewer_db_path(viewer_id: str, data_dir: Path) -> Path:
return viewers_dir(data_dir) / f"{viewer_id}.db"
def viewer_exists(viewer_id: str, data_dir: Path) -> bool:
return viewer_db_path(viewer_id, data_dir).exists()
def create_viewer(data_dir: Path) -> str:
"""Create a new viewer with an empty SQLite database."""
data_dir.mkdir(parents=True, exist_ok=True)
viewers_dir(data_dir).mkdir(parents=True, exist_ok=True)
for _ in range(10):
viewer_id = generate_viewer_id()
db_path = viewer_db_path(viewer_id, data_dir)
if db_path.exists():
continue
conn = get_connection(db_path)
init_db(conn)
conn.close()
return viewer_id
raise RuntimeError("Could not allocate a unique viewer id")
def ensure_local_viewer(data_dir: Path) -> str:
"""CLI default viewer not secret, for local single-user use."""
db_path = viewer_db_path(LOCAL_VIEWER_ID, data_dir)
if db_path.exists():
return LOCAL_VIEWER_ID
conn = get_connection(db_path)
init_db(conn)
conn.close()
return LOCAL_VIEWER_ID