Files
photoanalyzer/WEBAPP_CONCEPT.md

286 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Photo Analyzer — Web App UX Concept
Transform `photo_analyzer.py` from a terminal pipeline into a **local web app** that
reuses the `nsfwtag` review-app design language and architecture. Concept first —
implementation follows after review.
> **North star:** the analyzed data (descriptions, tags, mood, people, year, …) is
> currently invisible — it lives in a SQLite DB nobody can see. The web app's *primary*
> job is to surface it (browse + FTS5 search); the analysis pipeline becomes a
> monitored background job instead of a foreground terminal takeover.
Design validated with the `ui-ux-pro-max` skill: **Data-Dense Dashboard** pattern
(KPI cards, status colors green/amber/red, filtering-first, hover tooltips, row
highlight), debounced search with a real "no results" state, list virtualization for
25k+ photos, and — to stay dependency-free — **CSS progress bars with values-always-
visible** for stats rather than a chart library (the skill's AAA-accessible option).
---
## 1. Architecture (reuse nsfwtag's, unchanged in spirit)
- One **stdlib `ThreadingHTTPServer`** on `127.0.0.1:<random-free-port>`, opened in the
browser. No framework, no build step, no external assets (matches nsfwtag).
- One **token-injected HTML page** (`analyzer.html`, `%%TOKEN%%` replacement) + JSON
endpoints. Same "real HTML file, not a Python string" rule.
- **SQLite stays the single source of truth.** The server never owns pipeline state;
it reads the DB and an in-memory run-state object (exactly like the terminal
`_Dashboard` reads dicts + a deque). A render/HTTP crash can never corrupt the DB.
- **Path args validated against the DB** on every request (nsfwtag validates against
the scanned candidate set) — the server can't be pointed at arbitrary files.
- The existing analysis engine (`_run_folder_loop`, workers, retry/backoff, `_db_lock`,
variant grouping, nsfw-skip, `_IGNORE` exclusion) is **reused verbatim**, driven from
a background thread. The web layer is a new view + control surface, not a rewrite.
### Endpoints
| Method | Route | Purpose |
|--------|-------|---------|
| GET | `/` | The app page |
| GET | `/img?path=` | Thumbnail bytes *(reuse nsfwtag)* |
| GET | `/exif?path=` | Raw EXIF + decimal GPS + Maps URL *(reuse nsfwtag)* |
| GET | `/photo?path=` | Full analyzed record for one photo (all DB fields) |
| GET | `/search?q=&setting=&tod=&season=&people=&year_min=&year_max=&status=&album=&sort=&offset=` | Paged result set (FTS5 when `q` present) → `{rows:[…], total, offset}` |
| GET | `/facets` | Distinct filter values + counts (for the toolbar) |
| GET | `/stats` | Aggregates for the Stats view |
| GET | `/progress` | Live run state: `{running, totals, tokens, eta, albums:[…], feed:[…]}` |
| POST | `/run` | Start analysis with a config body (the CLI flags) |
| POST | `/stop` | Signal the current run to stop (reuses the existing `_stop` event) |
| GET | `/balance` · `/quota-check` | Proxy the existing functions → JSON |
| GET | `/log` | Activity log *(reuse nsfwtag)* |
Live updates use **polling** (`/progress` ~1.5 s, `/log` ~2 s) — same pattern as
nsfwtag's log panel. No SSE/WebSocket dependency.
---
## 2. Information architecture
Flat, three top-level views behind a header **segmented control** (deep-linked via
`#library` / `#analyze` / `#stats`; active state highlighted per the skill's
`nav-state-active`). An **Activity-log** slide-in panel is available from any view.
```
Header (sticky): [◧ logo] Photo Analyzer ⟨ Library · Analyze · Stats ⟩ [ total 25,318 · analyzed 25,316 · errors 8 ] [☰ log]
^ segmented, active = --text on --elev
```
- **Library** (default) — browse + search the analyzed photos. The payoff.
- **Analyze** — run controls + the live dashboard (album progress, feed, tokens/ETA).
- **Stats** — aggregates, breakdowns, error list.
---
## 3. View: Library *(browse + FTS5 search)*
```
┌ Header ─ view: Library ──────────────────────────────────────────────────────────┐
├ Toolbar (sticky) ─────────────────────────────────────────────────────────────────┤
│ 🔍 Search description, tags, mood, place… Setting▾ Time▾ Season▾ People▾ │
│ Year 1998 ●──────● 2019 ☐ has location Status▾ Sort: Relevance▾ │
├──────────────┬────────────────────────────────────────────────────────────────────┤
│ Folders │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ← virtualized / infinite │
│ ▸ Urlaub │ │ [img] │ │ [img] │ │ [img] │ │ [img] │ scroll, server-paged │
│ 2015 USA │ │ '15 👤2│ │ '04 │ │ '11 👤5│ │ ⚠err │ │
│ ▸ job │ └───────┘ └───────┘ └───────┘ └───────┘ │
│ ▸ selfies │ beach sunset… cathedral… wedding… (error: timeout) │
│ … │ … 25,316 results … │
└──────────────┴────────────────────────────────────────────────────────────────────┘
```
- **Search** (`.field`): debounced 250 ms → `/search` FTS5 over `description, tags,
mood, location_hint`. Autocomplete-style live results (skill `Search/Autocomplete`).
Empty query = browse-all (paged). **No-results state** shows a message + suggestions
("try a broader term, or clear filters"), never a blank grid.
- **Facet filters** map every analyzed field:
- Setting (indoor/outdoor) · Time of day · Season → `select` or `.chip` toggles.
- People → buckets `0 / 1 / 2 / 3+`.
- Year → dual-handle range slider (`approx_year` minmax), tabular numbers.
- Has-location → toggle (`location_hint` not null).
- Status → analyzed / exif_written / error / pending.
- Filters AND with the search; each updates the live result count.
- **Folder sidebar** (reuse nsfwtag `.sidebar`/`.fitem`): album = **leaf folder**
(reuse `album_label`), with counts; filter to a folder + its subfolders.
- **Card** (reuse `.card`/`.thumb`): lazy `/img` thumbnail; overlays — **year badge**
+ **people badge** (reuse `.score` badge styling), a **status dot** (error=red,
pending=faint), and the first line of the description under the thumbnail. Tag chips
on hover.
- **Grid scale:** 25k+ photos → **server-paged infinite scroll** (append on scroll),
not load-all (skill `virtualize-lists`, `content-jumping`). Reserve card height to
avoid layout shift.
- **Lightbox** (reuse nsfwtag lightbox + keyboard + prev/next within the current result
set): left = full image; right = a two-section panel:
1. **Analysis** — description, tags, mood, setting, time-of-day, season, people,
year, location. (The EXIF caption Immich will index, made human-readable.)
2. **EXIF** — raw fields + GPS **Google Maps** link (reuse `/exif`).
Per-photo action: **Re-analyze this one** (POST `/run` scoped to one path).
---
## 4. View: Analyze *(run controls + live dashboard)*
Mirrors the terminal `_Dashboard` (see `DASHBOARD_PLAN.md`) as a web surface.
```
┌ Header ─ view: Analyze ───────────────────────────────────────────────────────────┐
├ Run bar ──────────────────────────────────────────────────────────────────────────┤
│ Library [ /…/pictures ▾ ] ☐ Dry-run ☐ Re-analyze ☐ No-EXIF ☐ EXIF-only ☐ Variants │
│ [ Balance ] [ Quota check ] [▶ Start] │
├ Overall ──────────────────────────────────────────────────────────────────────────┤
│ ████████████████░░░░░░░░ 238 / 550 43 % ok 235 · err 3 │
│ tokens 284,192 · est. remaining ~340,000 · ETA 14:23 [■ Stop] (danger) │
├ Albums (data-dense) ──────────────────┬ Recent feed ───────────────────────────────┤
│ Camera Roll 238/550 ███████░░ 43% │ ✓ IMG_4821.jpg │
│ Urlaub/Rom 45/ 45 ████████ ✓ │ A golden retriever on a sunny beach │
│ Urlaub/Venedig 12/ 60 ██░░░░░░ 20% │ ✗ DSC_0042.jpg — 4 retries: timeout │
│ WhatsApp Images 0/120 ░░░░░░░░ 0% │ ✓ IMG_4820.jpg │
│ … │ Two children building a sandcastle │
└───────────────────────────────────────┴─────────────────────────────────────────────┘
```
- **Run bar** = the CLI flags as controls:
Library path (default from `LIBRARY` env) · **Dry-run · Re-analyze · No-EXIF ·
EXIF-only · Group-variants** toggles · **Start** (primary) / **Stop** (danger).
One run at a time (guarded); Start on an interrupted library simply **resumes**
(DB is truth) — resume-safety is automatic, no separate control.
- **Overall panel**: big progress bar, ok/err counts, tokens used + estimated
remaining + ETA (all tabular). Status colors: green done, amber running, red error.
- **Albums panel**: one row per **leaf-folder album** with a mini progress bar + ``
when fully done (the DASHBOARD_PLAN target, now clickable → jumps to that album in
Library). Future albums shown at 0 %.
- **Recent feed**: newest-first list — thumbnail + filename + one-line description;
errors in red with the message. Reuses the activity-log line styling.
- **Balance / Quota-check**: small buttons → `/balance`, `/quota-check` → toast/inline
result. **Debug log**: available via the Activity-log panel.
- Everything here is driven by polling `/progress`; the panel degrades to "Idle — no
run in progress" with a Start hint when nothing is running (empty state with action).
---
## 5. View: Stats *(aggregates + errors, no chart lib)*
```
┌ KPI cards ────────────────────────────────────────────────────────────────────────┐
│ 25,318 total 25,316 analyzed 25,316 EXIF-written 8 errors 2 pending │
├ Breakdowns (CSS bars, counts always visible) ───────────────────────────────────────┤
│ Setting outdoor ██████████████ 14,902 indoor ████████ 9,120 │
│ Season summer ████████ · autumn ██████ · spring ████ · winter ███ · unknown ██ │
│ People 0 ████████ · 1 ██████ · 2 ████ · 3+ ███ │
│ By year 1998▁ 2003▃ 2004▅ 2010▇ 2011█ 2015█ … (histogram of bars) │
│ Top tags beach 1,204 · wedding 980 · city 900 · food 610 … │
├ Errors ─────────────────────────────────────────────────────────────────────────────┤
│ DSC_0042.jpg — connection timeout (4 retries) [Retry] │
│ … │
└──────────────────────────────────────────────────────────────────────────────────────┘
```
- KPI cards reuse `.metric`. Breakdowns are **horizontal CSS bars with the count
printed** (skill: values-always-visible, AAA; no dependency, no CSP issue).
- Error table lists `status='error'` rows with `error_message` + a **Retry** action
(re-queues that path for the next run — matches "re-running retries error records").
---
## 6. Component inventory (reuse existing tokens)
Everything below already exists in `nsfwtag/review.html`'s `:root` token set and class
library — the web app inherits it wholesale (same dark OLED palette, radii, focus
rings, reduced-motion guard).
| Component | Source | New/changed |
|---|---|---|
| Header `.app`/`.brand`/`.logo`/`.titles` | reuse | title copy |
| KPI `.metrics`/`.metric` | reuse | used for header counts + Stats cards |
| View switcher | reuse `.seg` | active-state = current view |
| Toolbar `.toolbar` | reuse | — |
| Search `.field` | reuse | — |
| Facet `.chip` / `select` / `.seg` | reuse | People/Setting/Season/Status |
| Range slider `input[type=range]` | reuse | **dual-handle** year range (small JS) |
| Sidebar `.sidebar`/`.fitem` | reuse | album tree |
| Grid `.grid`/`.card`/`.thumb` | reuse | + year/people badges, status dot |
| Tag chips | reuse `.chip` (mini) | — |
| Lightbox + EXIF panel | reuse | + Analysis section |
| Buttons `.btn`/`.primary`/`.danger`/`.ghost` | reuse | Start/Stop/Balance |
| Toast `.toast` | reuse | — |
| Activity log panel | reuse | run events too |
| **Progress bar** | **new** `.bar` (fill = `--primary`) | overall + per-album + stat bars |
| **Run toggles** | **new** switch styled from tokens | the 5 flags |
**One new token:** `--warn:#f59e0b` (amber) for "running / partial" states — the only
gap in the current green/red/faint status set.
---
## 7. Interaction model
- **View switching**: header segmented control; state deep-linked in the URL hash and
preserved on back/forward (skill `deep-linking`, `state-preservation`).
- **Search + filters**: debounced; combine as AND; the URL hash encodes `q` + active
filters so a search is shareable and survives reload. Result count updates live.
- **Browse at scale**: infinite scroll appends server-paged results; card heights
reserved (no CLS); thumbnails lazy-load.
- **Lightbox**: `←/→` navigate the current result set, `Esc`/backdrop close; EXIF and
full record fetched lazily on open.
- **Analyze**: Start → POST `/run` (config) → server spawns the run on a background
thread → dashboard polls `/progress`. Stop → POST `/stop` → sets the existing
`_stop` event; in-flight requests drain, DB stays consistent, restart resumes.
- **Accessibility/motion**: keyboard nav + visible focus rings (inherited), status
never by color alone (badges carry text/icons), `prefers-reduced-motion` respected
(token media query already present), all touch targets ≥ 3644 px.
---
## 8. CLI → Web feature parity
| CLI feature | Web surface | Preserved how |
|---|---|---|
| `--library` | Analyze Run bar path (default `LIBRARY` env) | same discovery |
| `--dry-run` | Run toggle | passed to engine |
| `--reanalyze` | Run toggle | passed to engine |
| `--no-exif` | Run toggle | passed to engine |
| `--exif-only` | Run toggle | passed to engine |
| `--group-variants` | Run toggle | reuses variant grouping |
| `--stats` | **Stats view** | `/stats` aggregates |
| `--balance` | Balance button | `/balance` |
| `--quota-check` | Quota-check button | `/quota-check` |
| `--debug` | Activity-log panel | server log |
| resume-safety | automatic (Start resumes) | DB source of truth |
| `_IGNORE/` exclusion | server-side discovery | `discover_photos` unchanged |
| nsfw-skip | automatic in every run | `filter_nsfw_tagged` unchanged |
| FTS5 search *(CLI had none)* | **Library search** | new surface for existing FTS5 table |
| DB (all fields) | Library cards + lightbox + facets | new surface for existing schema |
Nothing is dropped. The only genuinely new capability is **surfacing** what the DB
already stores (search/browse/stats) — everything else is the same engine behind a
web control panel instead of a terminal.
---
## 9. Proposed file layout (mirrors `nsfwtag/`)
```
webapp/ (or analyzer_web/)
├── __main__.py # CLI: python -m webapp → starts server, opens browser
├── server.py # ThreadingHTTPServer + routes (reuse nsfwtag/server.py shape)
├── page.py # render analyzer.html with %%TOKENS%% (reuse webapp.py shape)
├── analyzer.html # the single-page frontend (inherits nsfwtag tokens/classes)
├── query.py # DB read helpers: search (FTS5), facets, stats, one-photo
└── runner.py # drive photo_analyzer's engine on a background thread + run-state
```
`photo_analyzer.py` stays the pipeline library; `runner.py` imports and drives it. No
duplication of analysis logic.
---
## 10. Build order (for after approval)
1. **Read-only Library first** — server + page + `/search` + `/facets` + grid +
lightbox over the *existing* DB. Immediately useful, zero risk to the pipeline.
2. **Stats view** — `/stats` + CSS breakdown bars.
3. **Analyze view** — `/run` + `/stop` + `/progress` polling + run-state object,
driving the existing engine on a thread.
4. **Balance/quota/log** wiring + polish (empty states, deep-link hash, reduced-motion).
Each step ships independently; the pipeline keeps working from the CLI throughout.