Running the application depended on work_item/scripts/python, which is the
work-item helper's launcher: it probes for conda with `command -v`, cannot see a
lazily-defined shell function, and falls back to a bare system interpreter with
none of the dependencies installed. The application now installs into an ordinary
virtualenv instead.
That exposed two packaging faults. Pillow, numpy, and scipy were declared
test-only although the application imports them at runtime — thumbnails decode
through Pillow and the perceptual hash is a DCT over decoded pixels — and an
editable install failed outright because setuptools could not choose between
photo_pipeline, migrations, and work_item. Both are fixed here, and the OpenAI
client moves to a `vision` extra so a review-only install does not pull an API
client it never calls.
Config.from_env now reads `.env`, or the file named by PHOTO_PIPELINE_ENV_FILE,
before it looks at the environment. The file is parsed, never executed: KEY=value
lines, comments, optional quotes, no interpolation and no export. Anything already
exported wins, so the file is standing configuration and the shell stays the
override for one run. The archived CLI's names are mapped as aliases
(LLM_API_KEY/GEMINI_API_KEY, LLM_BASE_URL, LIBRARY), so an existing
photo_analyzer.env configures the application unchanged.
.env, *.env, .venv/, and *.egg-info/ are ignored: the real configuration file
holds a live provider key and must never be committed.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH4M1PHPwJsM9btcg8hzCs
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>