Ports the safety review and the Photo Analyzer Library/Analyze/Stats
experiences onto the shared API + service layer, and adds the Workflow
home, enforcing the pipeline gates and the one-mutating-job policy.
Backend
- migration 0005 + models: safety_reviews (append-only, latest row is the
current decision) and analysis_results (donor photos schema re-keyed to
asset_id).
- SafetyService: persist scores/decisions, review queue with filters, and
the EXIF safety checkpoint (mutually-exclusive sfw/nsfw keyword written,
read back, current_sha256 refreshed) that upload eligibility depends on.
- AnalysisService: the privacy gate — the vision provider is called ONLY for
canonical, confirmed-SFW assets; nsfw/undecided are recorded skipped without
a request. Provider is an injected adapter (real OpenAI-compatible Gemini
call extracted from photo_analyzer.analyze_image; a fake in tests).
- LibraryService: Library search + Stats read model ported from webapp/query.py
(LIKE search in place of FTS5; facets, top tags, years, albums, people).
- WorkflowService + GET /api/v1/workflow: per-stage readiness derived from the
source tables — counts, blockers, last-run, action, and an active_job that
drives read-only-during-jobs. Safety scoring and analysis run as durable jobs
under the library_write lock via new domain handlers, so a second mutating
job is refused.
- routes: workflow, safety (queue/counts/decisions/jobs), analysis
(counts/results/jobs), library (assets/facets/stats).
Frontend
- five views (frontend/js/views.js) on the US02-05 shell: Workflow stepper
(status text+icon, not colour alone; actions disabled with a reason while a
job runs), Safety review (filter tabs, decide, persists across reload),
Library (search + cards), Analyze (counts + live job log via the SSE
adapter), Stats. Shared DOM helpers extracted to dom.js; Workflow is the home
route.
Tests
- integration: provider-call privacy (nsfw never reaches the provider),
sfw→nsfw flip drops analysis eligibility, decision persistence, one-mutating-
job rejection, workflow counts, and the exiftool safety-keyword write/verify.
- e2e: Workflow cards, actions disabled+explained during a job, safety
decide-persists-across-reload, Library search, Stats, Analyze counts.
- traceability map updated for US02-05 and US02-06.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the reusable browser shell primitives Phase B views build on:
- store.js: observable store (get/set/subscribe)
- events.js: job activity adapter — SSE preferred, polling fallback,
sharing the event `seq` as cursor so a transport switch drops nothing
- api.js: cancellable() (AbortController) + job endpoints; aborted
requests reject with code "cancelled"
Make the SSE stream generically consumable: emit default `message`
events with the type in the JSON payload instead of `event: <type>`, so
a browser EventSource receives the open-ended type set (state:*,
claimed, …) via onmessage without enumerating it. The `event: done`
sentinel and resumable `id:` cursors are unchanged.
Tests: frontend/js/tests/ in-browser unit suite (api errors +
cancellation, store transitions, routing, SSE→polling fallback) served
over the app's static mount and driven by tests/e2e/test_frontend_shell.py,
which also asserts asset loading, deep-link + reload restore, JSON-only
/api/v1, and a clean console/network. Reuses the installed playwright —
no JS toolchain added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>