app: FastAPI API + serve CLI (Phase 2)
- create_app(db_path, img_dir, state_dir) factory; every route + payload
from app.state — selfcheck runs the real router on a temp DB copy
- /api/health (ok/db/counts), /api/stats (live/new/likes/maybes/nopes/
last_import), /api/deck (undecided semantics, score/first_seen/id DESC,
opaque '<score>|<first_seen>|<id>' cursor, strictly-after tuple page,
remaining count), /api/swipe upsert + DELETE (idempotent), /api/list
(decision/sort/q/limit/offset, total), /api/listing/{id}, /api/import
(importer, HTTP 200 even when gates fail), /api/badge/seen, /img/{id}
(cached file -> 302 img_url -> 404), static routes (/, manifest, sw.js,
icons, apple-touch; no-cache on index + sw.js)
- frozen payload contract incl. img=/img/{id} when img_url or cached file
- serve.py: argparse (--db/--img-dir/--state-dir/--host/--port, defaults
127.0.0.1:9453) -> uvicorn.run
- verified: 560-deep deck walks clean with zero page overlap, swipe ->
list -> edit -> delete -> back on deck, badge roundtrip (new 560 -> 0);
real uvicorn returns true 302 for the img fallback (TestClient in this
fastapi/httpx pair mangles RedirectResponse into a 404 — framework
artifact, not app behavior)