Pairing: scan a QR off the terminal once; the cookie lasts a year

The key already persisted across restarts, but the cookie was a
session cookie — Safari eventually drops those and the paste ritual
returned. The pairing cookie now lasts a year, and startup prints a QR
code of the pairing URL (qrcode dep, ASCII render) so a phone pairs by
pointing its camera at the terminal. Revoke every paired device by
deleting data/.lan_key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-03 16:50:36 -04:00
co-authored by Claude Fable 5
parent 78debdd62f
commit 44dd03d06a
6 changed files with 45 additions and 3 deletions
+1 -1
View File
@@ -61,7 +61,7 @@
<h2 id="files">Your data, on disk</h2>
<div class="card prose">
<p>Everything lives in flat files under <code>data/</code> — inspectable, hand-editable, and git-friendly. The pipeline artifacts: <code>titles.json</code> (what was read), <code>matches.csv</code> (what it matched), <code>to_add.csv</code>/<code>to_update.csv</code> (what upload will do), <code>upload_log.csv</code> (what it did), <code>games.json</code> (the library). Your curation: <code>title_edits.json</code>, <code>title_splits.json</code>, <code>title_removals.json</code>, <code>unidentified_dismissed.json</code>.</p>
<p>Credentials never live in files — only environment variables, set up by <code>bggpipe init</code>. The app serves localhost only, unless started with <code>--lan</code> — that opens it to your network behind a per-run access key printed at startup (no login beyond the key; trusted networks only).</p>
<p>Credentials never live in files — only environment variables, set up by <code>bggpipe init</code>. The app serves localhost only, unless started with <code>--lan</code> — that opens it to your network behind an access key: scan the QR code the server prints (or open the printed link) once per device, and a year-long cookie keeps it paired. Delete <code>data/.lan_key</code> to revoke every device. No login beyond the key; trusted networks only.</p>
<p>More depth: the README covers setup and photo technique; <code>docs/bgg-upload-flow.md</code> documents the upload automation.</p>
</div>
+25 -1
View File
@@ -380,8 +380,15 @@ def create_app(
status_code=303,
headers={"Location": request.url.path or "/"},
)
# a YEAR, not a session: pairing a device should be a
# one-time act (the key file persists too; delete
# data/.lan_key to revoke every paired device)
response.set_cookie(
LAN_COOKIE, lan_token, httponly=True, samesite="lax"
LAN_COOKIE,
lan_token,
max_age=365 * 24 * 3600,
httponly=True,
samesite="lax",
)
return response
if request.method not in ("GET", "HEAD", "OPTIONS") and (
@@ -1105,6 +1112,21 @@ def _dev_app() -> FastAPI:
return create_app(load_config(Path(path) if path else None), lan_token=token)
def _print_qr(url: str) -> None:
"""Pairing without typing: the phone's camera reads the URL straight
off the terminal."""
import io as _io
import qrcode
qr = qrcode.QRCode(border=1)
qr.add_data(url)
buffer = _io.StringIO()
qr.print_ascii(out=buffer, invert=True)
for line in buffer.getvalue().splitlines():
typer.echo(f" {line}")
def _lan_key(cfg: Config) -> str:
"""The --lan access key: created once, reused across restarts so a
phone's cookie keeps working. Delete the file to rotate the key."""
@@ -1143,6 +1165,8 @@ def run_web_review(
typer.echo(f"bggpipe web UI: {url} (this machine needs no key)")
if primary:
typer.echo(f" on your phone, open: http://{primary}:{port}/?k={token}")
typer.echo(" or point its camera at this code:")
_print_qr(f"http://{primary}:{port}/?k={token}")
if spares:
typer.echo(
" (several network interfaces here — if that address "