CI / skip-ci-check (pull_request) Successful in 30s
CI / python-lint (pull_request) Successful in 32s
CI / docker-ci (pull_request) Successful in 31s
CI / secret-scan (pull_request) Successful in 39s
CI / viewer-unit (pull_request) Successful in 1m44s
CI / admin-unit (pull_request) Successful in 2m1s
CI / e2e (pull_request) Failing after 2m2s
Nest Identify/Auto-Match/Modify under /people with redirects; reject blur/extreme pose at Process; Name-cluster on Identify; axe smoke tests; helpers to rotate DEV admin password and seed match_decisions.
150 lines
14 KiB
Markdown
150 lines
14 KiB
Markdown
# PunimTag roadmap
|
|
|
|
Living plan for product quality, auth/email reliability, and automation.
|
|
|
|
## Now — shipped / in flight
|
|
|
|
- [x] Fix public-host auth redirects (Kolby #57 family) — `NEXTAUTH_URL` + verify-email
|
|
- [x] DEV mail trap via Mailpit (not Mailtrap SaaS / not Spamhaus-blocked Mailcow for e2e)
|
|
- [x] Playwright e2e suite (`e2e/`) on `@levkin/playkit` against `punimtagdev.levkin.ca`
|
|
- [x] Dedicated e2e user `e2e@levkine.ca` (Vault / Infisical / Gitea Actions)
|
|
- [x] Vitest unit tests for `viewer-frontend` in Gitea Actions
|
|
- [x] API smoke + Zod schema checks (health, people, tags, 401 contracts)
|
|
- [x] storageState setup project + real upload smoke
|
|
- [x] **FastAPI e2e users** — `e2e` (admin, mirrors `e2e@levkine.ca`) + `e2e-viewer` (viewer) in FastAPI main DB; `E2E_API_USERNAME`/`PASSWORD` + `E2E_API_VIEWER_USERNAME`/`PASSWORD` in CI
|
|
- [x] **Search / filter e2e** — tag + person filters on public gallery (`gallery.search-filters.spec.ts`)
|
|
- [x] **Role permissions e2e** — viewer vs admin write gates (`api.role-permissions.spec.ts`)
|
|
- [x] **Ansible FastAPI-e2e work committed** — ansible PR #127 merged (provisioning script, SMTP quote fix, vault/docs)
|
|
- [x] **Infisical `/playkit/punimtag` E2E_API_* keys created** — self-hosted build's `/api/v3/secrets/raw` create route needs client-side E2EE ciphertext (422 on plaintext); fixed by switching to `/api/v4/secrets` (plaintext) in `provision-punimtag-e2e-user.py` — see ansible `docs/hardening/SECRETS.md`
|
|
- [x] **Vaultwarden sync for FastAPI e2e logins** — `PunimTag FastAPI e2e admin/viewer (DEV)` entries created
|
|
- [x] **Playkit bumped to `v0.3.1`** — tagged, released (Gitea release job needed a `RELEASE_TOKEN` secret rename — `GITEA_TOKEN` is a reserved name Gitea's Actions API rejects), Outline `QA & Dev → Playkit` page created
|
|
- [x] **Network silent-failure monitor** — `startNetworkErrorMonitor` on gallery home, search+filter, upload, Manage Users (`network-errors.spec.ts`)
|
|
- [x] **Favorites + People filter UI e2e** — `gallery.filters.authed.spec.ts` (uses storageState)
|
|
- [x] **admin-frontend Vitest** — separate Vite/React app; `admin-unit` CI job + `fastapi-path`/`media-path` lib tests
|
|
- [x] **`interceptNetworkCall` on upload + search filter UI** — typed status/JSON spies replace `waitForResponse`/text scraping (`upload.smoke.spec.ts`, `gallery.search-filters.spec.ts`)
|
|
- [x] **Widened Zod schemas** — FastAPI login `TokenResponse` + `/auth/me` `UserResponse` (`api.fastapi-login.spec.ts`), gallery `SearchResponse` (`gallery.search-filters.spec.ts`)
|
|
- [x] **PROD smoke spec** — `prod.smoke.spec.ts`, opt-in via `PROD_BASE_URL`; skips (no-op) until the PROD LXC exists — see Ops/docs debt
|
|
- [x] **NextAuth (viewer) write gates** — third user store (auth DB, `hasWriteAccess=false`) provisioned via `provision-punimtag-e2e-user.py`; `viewer.write-gates.spec.ts` proves 403 (viewer) vs pass-through (admin) on `POST /api/faces/{id}/identify`
|
|
- [x] **Manage Users real CRUD + causal cross-session e2e** — `admin.manage-users-actions.spec.ts`: admin creates/edits/deletes a user through the actual panel (the older spec only opened/closed it), and deactivating a live user immediately revokes their *already open* session (jwt callback re-checks `isActive`); disposable `e2e-manage-test-*` accounts, always cleaned up
|
|
- [x] **Timing budgets** — `e2e/timing-budgets.ts` `expectWithinBudget()` gates every `timings.measure()` call site against a shared `BUDGET_MS` bucket (`api`/`uiAction`/`uiLogin`/`heavyCrud`); a step getting order-of-magnitude slower now reds the test instead of only feeding an unwatched sample
|
|
- [x] **CI: `actions/upload-artifact@v4` pinned to `v3`** — v4 doesn't work against this Gitea/act runner's artifact backend; report upload on e2e failure was silently broken
|
|
- [x] **CI: npm cache corruption retry** — `viewer-unit`/`admin-unit`/`e2e` all retry `npm ci` once after `npm cache clean --force` on first failure (shared act_runner cache has corrupted `@next/swc-linux-x64-musl` before, redding CI with no product bug)
|
|
- [x] ROADMAP (this file)
|
|
- [x] **Person contact fields** — optional email/phone on identify (viewer + admin), pending approve UI, Modify person
|
|
- [x] **Approve Identified bulk select** — Select All Approve/Deny + Approve Next 10
|
|
- [x] **Admin pending-queue digest email** — `ADMIN_NOTIFY_EMAIL` via `noreply@levkine.ca` SMTP (throttled)
|
|
- [x] **UX Sprint A — match-all filters, slim search payloads, DEV `next start`** — see [UX sprint](#ux-sprint--trust-mobile-admin-throughput-2026-08)
|
|
- [x] **UX Sprint B — mobile gallery UX** — see [UX sprint](#ux-sprint--trust-mobile-admin-throughput-2026-08)
|
|
- [x] **UX Sprint C — admin throughput** — see [UX sprint](#ux-sprint--trust-mobile-admin-throughput-2026-08)
|
|
- [x] **UX Sprint D — hardening** — see [UX sprint](#ux-sprint--trust-mobile-admin-throughput-2026-08)
|
|
|
|
## Next (near-term)
|
|
|
|
### Ops / docs debt
|
|
|
|
- [ ] **Wire QA/PROD SMTP on live guests** when LXCs 9102/9103 exist (`make punimtag-sync-smtp ENV=qa|prod`) — confirmed via `pct list` on the Proxmox node: only DEV (`9101`) exists today, so this (and PROD smoke actually running, and the PROD `NEXTAUTH_URL` hostname) stays dormant-but-ready until QA/PROD LXCs are provisioned
|
|
- [ ] **Stop seeding `admin@admin.com` in docs** as the day-to-day login; keep bootstrap scripts but point operators at Vaultwarden `PunimTag e2e`
|
|
- [ ] Set the new `E2E_VIEWER_EMAIL`/`E2E_VIEWER_PASSWORD` Gitea secrets from Infisical/Vaultwarden if CI doesn't already have them synced (script sets Gitea directly, so likely already OK — verify on next CI run)
|
|
- [x] ~~**pve10 RAM overcommit / CI OOM under bursts**~~ — dropped from backlog 2026-07-15 (user); git-ci-01 already at 8 GB (ansible PR #128). Revisit only if OOM storms return.
|
|
- [x] **DEV LXC (9101) redeploy** — 2026-07-15: `e54c609` → `23f0616` (`origin/dev`, +24 commits incl. `c011c80` a11y/aria-labels). Stashed 3 dirty viewer files; `npm ci` + `prisma:generate:all`; `pm2 restart punimtag-viewer`; local + `punimtagdev.levkin.ca` → **200**. Manage-Users `aria-label`s present. *(Note: `punimtag-api` still PM2 `waiting restart` — pre-existing, not introduced by this pull.)*
|
|
|
|
### Playkit adoption (pin is `v0.3.1` today)
|
|
|
|
| Kit feature | In pin? | PunimTag use |
|
|
|-------------|---------|--------------|
|
|
| Zod `schema` on `ApiClient` | yes | health, people/tags catalog, FastAPI login `TokenResponse` + `/auth/me`, gallery `SearchResponse` |
|
|
| `saveStorageState` / `storageStateUse` | yes | setup uses `saveStorageState` for admin **and** viewer (`hasWriteAccess=false`); specs use raw `test.use({ storageState })` (`storageStateUse` optional cosmetic) |
|
|
| `playkitFailureArtifacts()` | yes | `playwright.config.ts` |
|
|
| Mailpit / `createMailInbox` | yes | mail specs |
|
|
| `interceptNetworkCall` / `startNetworkErrorMonitor` | yes | both in use — `network-errors.spec.ts`, `upload.smoke.spec.ts`, `gallery.search-filters.spec.ts` |
|
|
| Timing → Pushgateway | yes API | **not wired** — needs Pushgateway deployed (ansible `deploy/observability/`) + CI `PLAYKIT_METRICS_*` env |
|
|
|
|
### Product / coverage gaps
|
|
|
|
- [ ] **PROD smoke actually running** — spec exists (`prod.smoke.spec.ts`) and is verified against a real public host, but stays skipped until `PROD_BASE_URL` is set (blocked on the LXC existing — see Ops/docs debt)
|
|
- [ ] Consider a non-admin **manager/editor**-role FastAPI + NextAuth user if finer-grained role coverage is ever needed (today's viewer/admin split covers the binary write-access gate)
|
|
- [x] ~~No latency/perf budget assertions anywhere~~ — closed via `expectWithinBudget()` (see "Now — shipped / in flight"). Still no Core Web Vitals (LCP/CLS/INP) collection — separate gap, not addressed.
|
|
|
|
### Deferred (soak policy)
|
|
|
|
- [ ] **Burn-in / deploy-smoke CLI** (playkit ROADMAP v0.4/v0.5+) — playkit's own ROADMAP has an explicit "adoption pause" until the `v0.3.1` network helpers + release job soak for a few days in this repo's CI; revisit once that settles rather than adding more moving parts at once.
|
|
|
|
### UX sprint — trust, mobile, admin throughput (2026-08)
|
|
|
|
#### Sprint A — quick trust + speed (shipped)
|
|
|
|
- [x] **Match-all people/tags filters** — `/api/search` raw SQL uses `GROUP BY` + `HAVING COUNT(DISTINCT …)` when `peopleMode=all` / `tagsMode=all` (`lib/search-filter-sql.ts` + Vitest)
|
|
- [x] **Slim gallery face payloads** — home SSR + search API return id, photo_id, person_id, location, person names only
|
|
- [x] **DEV viewer `next start`** — `scripts/deploy-viewer.sh`; `ecosystem.config.js.example` uses `next start -p 3001`
|
|
|
|
#### Sprint B — mobile gallery (shipped)
|
|
|
|
- [x] **Mobile grid actions** — download / favorite / report visible on touch (not `group-hover` only)
|
|
- [x] **Mobile filter drawer** — slide-over panel on phones; desktop collapsible sidebar unchanged
|
|
- [x] **Skeleton grid** — pulse placeholders instead of full-page spinner on filter/load
|
|
- [x] **Progressive photo viewer** — thumbnail first, then full-res swap (local files)
|
|
|
|
#### Sprint C — admin throughput (shipped)
|
|
|
|
- [x] **Identify arrow-key nav** — ←/→ prev/next face in queue (skips when focus is in form fields)
|
|
- [x] **Toasts + confirm dialogs** — Identify, Auto-Match, Approve Identified
|
|
- [x] **Auto-Match progress + cancel** — indeterminate bar, elapsed timer, abort in-flight request
|
|
- [x] **Approve Identified pagination** — API `page` / `page_size` + batched face→photo lookup; UI 25 rows/page
|
|
- [x] **Approve in-app photo preview** — lightbox instead of `window.open`
|
|
|
|
#### Sprint D — hardening (shipped)
|
|
|
|
- [x] **Admin e2e** — Identify, Auto-Match, Approve pages + gallery scroll restore (`admin.review-pages.spec.ts`, `viewer.scroll-restore.spec.ts`)
|
|
- [x] **Core Web Vitals budget** — LCP + CLS gates on home gallery (`viewer.web-vitals.spec.ts`, `web-vitals-budgets.ts`)
|
|
- [x] **Unify `/` and `/search`** — `/search` redirects to `/?…` with full query string (`peopleMode`, `tagsMode`, etc.)
|
|
|
|
#### UX polish (2026-08)
|
|
|
|
- [x] **Favorite sign-in copy** — logged-out heart opens favorites messaging (not report) via `SignInRequiredDialog`
|
|
- [x] **Text contrast** — replace misplaced `text-secondary` (pale wash token) with `text-foreground` / `text-primary` on labels, auth dialogs, menus
|
|
- [x] **Admin Chabad chrome** — navy sidebar, gold accent, theme toggle matching viewer tokens (`docs/ADMIN_UX_REVIEW.md`)
|
|
|
|
### Face recognition accuracy (2026-08)
|
|
|
|
- [x] **Phase 1 — quality sharpness** — Laplacian variance computed on the face crop (was kernel constant); unit tests in `tests/test_face_quality_score.py`; error-tag checklist in `docs/FACE_MATCH_ERROR_TAGGING.md`
|
|
- [x] **Phase 2 — trusted refs + stricter Auto-Match + accept/reject log** — ref quality floor 0.5; browse/run tolerances 0.5/0.4; auto-accept default 85%; `match_decisions` table + logging on Save / auto-accept; tests in `tests/test_auto_match_phase2.py`
|
|
- [x] **Phase 3 — recalibrate confidence + re-score quality** — fit distance→confidence knots from identified pairs / `match_decisions`; optional JSON overrides legacy curve; `scripts/fit_confidence_calibration.py` + `scripts/rescore_face_quality.py`; docs in `docs/FACE_CONFIDENCE_CALIBRATION.md`
|
|
- [x] **Phase 4 — multi-ref matching** — up to 3 trusted refs per person; best (min) distance wins; rolling-mean calibration fitter; status in `docs/FACE_ACCURACY_STATUS.md`
|
|
- [x] **Immich precision gates** — max recognition distance, next-best person margin, detection floor 0.55, auto-accept distance cap; admin Chabad Blue theme (shell + login)
|
|
- [x] **Person merge** — `POST /people/{id}/merge` + Modify People UI (#95)
|
|
- [x] **Admin brand parity** — JRCC logos tracked; page tokens; navy sidebar + light/dark toggle (#96 + chrome follow-up)
|
|
- [x] **Admin People hub IA** — `/people/{identify,auto-match,modify}` tabs + redirects from old paths
|
|
- [x] **a11y smoke** — axe on admin login + viewer home; viewer skip-link already present
|
|
- [x] **Phase 5 — harder reject of junk detections** — blur (laplacian) + extreme yaw/pitch at Process time
|
|
- [x] **Cluster-name Identify** — “Name cluster” selects similarity group into Identify payload
|
|
- [ ] Soak `match_decisions` on DEV then revisit calibration `--apply` (needs ≥20 same + ≥20 diff pairs)
|
|
|
|
## Later
|
|
|
|
- [x] Proper DEV deploy (`next start` + CI image) instead of long-lived `next dev` — `scripts/deploy-viewer.sh`, `ecosystem.config.js.example` uses `next start -p 3001`
|
|
- [ ] Ansible/app_setup path aligned with `/opt/punimtag/viewer-frontend/.env` (not `/srv/app`)
|
|
- [ ] OpenAPI-driven contract suite (playkit / consumer)
|
|
- [ ] Multi-browser matrix (firefox/webkit)
|
|
- [ ] Hermes/Mattermost report on e2e failure
|
|
- [ ] Enable playkit timing metrics in CI once Pushgateway is live
|
|
|
|
## Test map
|
|
|
|
| Layer | Where | Status |
|
|
|-------|--------|--------|
|
|
| Viewer unit | `viewer-frontend` Vitest | CI `viewer-unit` |
|
|
| Backend unit/integration | `tests/` pytest | existing |
|
|
| E2E browser + mail | `e2e/tests/*` | CI `e2e` |
|
|
| E2E API (unauth + catalog) | `e2e/tests/api.*` | CI |
|
|
| E2E FastAPI authed | `api.fastapi-login.spec.ts` | CI (secrets set) |
|
|
| E2E role-permission gates | `api.role-permissions.spec.ts` | CI (secrets set) |
|
|
| E2E gallery search filters | `gallery.search-filters.spec.ts` | CI (public, no login) |
|
|
| E2E favorites + people filter UI | `gallery.filters.authed.spec.ts` | CI (storageState) |
|
|
| E2E network silent-failure monitor | `network-errors.spec.ts` | CI |
|
|
| E2E NextAuth write gates (viewer vs admin) | `viewer.write-gates.spec.ts` | CI (secrets set) |
|
|
| E2E PROD smoke (health + login) | `prod.smoke.spec.ts` | dormant — needs `PROD_BASE_URL` (PROD LXC) |
|
|
| Admin UI unit | `admin-frontend` Vitest | CI `admin-unit` |
|
|
| Timing metrics push | — | gap (ops + CI env) |
|
|
|
|
See also: `e2e/README.md`, playkit `ROADMAP.md` / `docs/NETWORK.md` / `docs/IDEAS.md`.
|