# webapp — Photo Analyzer web UI A local, browser-based UI for the `photo_analyzer.py` pipeline. Browse and full-text-search the analyzed library, watch analysis runs live, and start/stop runs — reusing the `nsfwtag` review-app design (dark OLED, stdlib HTTP server, one token-injected HTML page). Nothing leaves the machine. ```bash python -m webapp # DB + library default from photo_analyzer.env python -m webapp --db photo_analysis.db --library pictures/ python -m webapp --no-open # don't auto-open a browser ``` See `../WEBAPP_CONCEPT.md` for the full UX concept and rationale. ## Views - **Library** — FTS5 search over description/tags/mood/location + facet filters (setting, time of day, season, people, year range, has-place, status), a folder sidebar (album = leaf folder), an infinite-scroll grid, and a lightbox with an **Analysis** panel (all DB fields) and a raw **EXIF** panel (incl. Google Maps link for geotagged photos). - **Analyze** — run controls mapping every CLI flag (dry-run / reanalyze / no-exif / exif-only / group-variants), a live overall progress bar, per-album progress, and a recent feed. Start/Stop drive `photo_analyzer.py` as a subprocess; Stop is the analyzer's own SIGINT drain. Balance / quota-check buttons. A **Duplicates & maintenance** row runs the content-hash actions (backfill hashes / find duplicates / mark duplicates) — no API calls; output streams to the Activity log and marked duplicates appear under the `duplicate` status filter. (Move/rename reconciliation and perceptual hashing also run automatically at the start of every normal run.) - **Stats** — KPI cards + CSS breakdown bars (setting/time/season/people, by-year histogram, top tags) + an error list. No chart library. ## Architecture - `server.py` — stdlib `ThreadingHTTPServer` on `127.0.0.1:`; routes below. - `query.py` — read-only DB access (FTS5 search, facets, stats, one-photo). - `runner.py` — drives `photo_analyzer.py` as a subprocess; DB-derived progress. - `page.py` / `analyzer.html` — the single frontend page (inherits nsfwtag's tokens). - SQLite stays the source of truth; the server only reads it (+ a `busy_timeout` so a concurrent analysis write never errors a read). `/img` and `/exif` paths are validated against the DB — the server can't be pointed at arbitrary files. | Route | Purpose | |-------|---------| | GET `/` | the page | | GET `/search?q=&setting=&tod=&season=&people=&year_min=&year_max=&has_location=&status=&album=&sort=&offset=` | paged results (FTS5 when `q`) | | GET `/facets` · `/stats` | filter options · aggregates | | GET `/photo?path=` · `/exif?path=` · `/img?path=` | one record · raw EXIF · thumbnail | | GET `/progress` · `/log` | live run state · activity log | | POST `/run` · `/stop` | start / stop a run — body `{library, ...flags}` or `{library, action}` where action ∈ backfill-phash \| list-dupes \| dedupe | | GET `/balance` · `/quota-check` | provider balance · one-shot quota probe | ## Known ceilings (ponytail) - `/img` serves **full-resolution originals**, not resized thumbnails — a 120-card page can move a lot of bytes. Fine locally; add a Pillow thumbnail+cache endpoint if the grid feels slow. (Same limitation as nsfwtag.) - HEIC files won't render in the browser (broken thumb); metadata/search still work. - The Analyze **tokens/ETA** readout is omitted — those live only in the terminal dashboard's memory; the subprocess model surfaces DB-derived progress instead. Parse run stdout for token counts if you want them back.