CI / skip-ci-check (pull_request) Successful in 30s
CI / python-lint (pull_request) Successful in 32s
CI / docker-ci (pull_request) Successful in 33s
CI / secret-scan (pull_request) Successful in 36s
CI / viewer-unit (pull_request) Successful in 1m44s
CI / e2e (pull_request) Successful in 1m54s
CI / admin-unit (pull_request) Successful in 2m0s
Sharpness previously used variance of the kernel constant, so every face got ~0 sharpness weight. Compute Laplacian variance on the grayscale crop; add unit tests and an error-tagging checklist for follow-on accuracy work.
12 KiB
12 KiB
PunimTag roadmap
Living plan for product quality, auth/email reliability, and automation.
Now — shipped / in flight
- Fix public-host auth redirects (Kolby #57 family) —
NEXTAUTH_URL+ verify-email - DEV mail trap via Mailpit (not Mailtrap SaaS / not Spamhaus-blocked Mailcow for e2e)
- Playwright e2e suite (
e2e/) on@levkin/playkitagainstpunimtagdev.levkin.ca - Dedicated e2e user
e2e@levkine.ca(Vault / Infisical / Gitea Actions) - Vitest unit tests for
viewer-frontendin Gitea Actions - API smoke + Zod schema checks (health, people, tags, 401 contracts)
- storageState setup project + real upload smoke
- FastAPI e2e users —
e2e(admin, mirrorse2e@levkine.ca) +e2e-viewer(viewer) in FastAPI main DB;E2E_API_USERNAME/PASSWORD+E2E_API_VIEWER_USERNAME/PASSWORDin CI - Search / filter e2e — tag + person filters on public gallery (
gallery.search-filters.spec.ts) - Role permissions e2e — viewer vs admin write gates (
api.role-permissions.spec.ts) - Ansible FastAPI-e2e work committed — ansible PR #127 merged (provisioning script, SMTP quote fix, vault/docs)
- Infisical
/playkit/punimtagE2E_API_ keys created* — self-hosted build's/api/v3/secrets/rawcreate route needs client-side E2EE ciphertext (422 on plaintext); fixed by switching to/api/v4/secrets(plaintext) inprovision-punimtag-e2e-user.py— see ansibledocs/hardening/SECRETS.md - Vaultwarden sync for FastAPI e2e logins —
PunimTag FastAPI e2e admin/viewer (DEV)entries created - Playkit bumped to
v0.3.1— tagged, released (Gitea release job needed aRELEASE_TOKENsecret rename —GITEA_TOKENis a reserved name Gitea's Actions API rejects), OutlineQA & Dev → Playkitpage created - Network silent-failure monitor —
startNetworkErrorMonitoron gallery home, search+filter, upload, Manage Users (network-errors.spec.ts) - Favorites + People filter UI e2e —
gallery.filters.authed.spec.ts(uses storageState) - admin-frontend Vitest — separate Vite/React app;
admin-unitCI job +fastapi-path/media-pathlib tests interceptNetworkCallon upload + search filter UI — typed status/JSON spies replacewaitForResponse/text scraping (upload.smoke.spec.ts,gallery.search-filters.spec.ts)- Widened Zod schemas — FastAPI login
TokenResponse+/auth/meUserResponse(api.fastapi-login.spec.ts), gallerySearchResponse(gallery.search-filters.spec.ts) - PROD smoke spec —
prod.smoke.spec.ts, opt-in viaPROD_BASE_URL; skips (no-op) until the PROD LXC exists — see Ops/docs debt - NextAuth (viewer) write gates — third user store (auth DB,
hasWriteAccess=false) provisioned viaprovision-punimtag-e2e-user.py;viewer.write-gates.spec.tsproves 403 (viewer) vs pass-through (admin) onPOST /api/faces/{id}/identify - 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-checksisActive); disposablee2e-manage-test-*accounts, always cleaned up - Timing budgets —
e2e/timing-budgets.tsexpectWithinBudget()gates everytimings.measure()call site against a sharedBUDGET_MSbucket (api/uiAction/uiLogin/heavyCrud); a step getting order-of-magnitude slower now reds the test instead of only feeding an unwatched sample - CI:
actions/upload-artifact@v4pinned tov3— v4 doesn't work against this Gitea/act runner's artifact backend; report upload on e2e failure was silently broken - CI: npm cache corruption retry —
viewer-unit/admin-unit/e2eall retrynpm cionce afternpm cache clean --forceon first failure (shared act_runner cache has corrupted@next/swc-linux-x64-muslbefore, redding CI with no product bug) - ROADMAP (this file)
- Person contact fields — optional email/phone on identify (viewer + admin), pending approve UI, Modify person
- Approve Identified bulk select — Select All Approve/Deny + Approve Next 10
- Admin pending-queue digest email —
ADMIN_NOTIFY_EMAILvianoreply@levkine.caSMTP (throttled) - UX Sprint A — match-all filters, slim search payloads, DEV
next start— see UX sprint - UX Sprint B — mobile gallery UX — see UX sprint
- UX Sprint C — admin throughput — see UX sprint
- UX Sprint D — hardening — see UX sprint
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 viapct liston the Proxmox node: only DEV (9101) exists today, so this (and PROD smoke actually running, and the PRODNEXTAUTH_URLhostname) stays dormant-but-ready until QA/PROD LXCs are provisioned - Stop seeding
admin@admin.comin docs as the day-to-day login; keep bootstrap scripts but point operators at VaultwardenPunimTag e2e - Set the new
E2E_VIEWER_EMAIL/E2E_VIEWER_PASSWORDGitea 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) 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.- DEV LXC (9101) redeploy — 2026-07-15:
e54c609→23f0616(origin/dev, +24 commits incl.c011c80a11y/aria-labels). Stashed 3 dirty viewer files;npm ci+prisma:generate:all;pm2 restart punimtag-viewer; local +punimtagdev.levkin.ca→ 200. Manage-Usersaria-labels present. (Note:punimtag-apistill PM2waiting 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 untilPROD_BASE_URLis 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)
No latency/perf budget assertions anywhere— closed viaexpectWithinBudget()(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.1network 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)
- Match-all people/tags filters —
/api/searchraw SQL usesGROUP BY+HAVING COUNT(DISTINCT …)whenpeopleMode=all/tagsMode=all(lib/search-filter-sql.ts+ Vitest) - Slim gallery face payloads — home SSR + search API return id, photo_id, person_id, location, person names only
- DEV viewer
next start—scripts/deploy-viewer.sh;ecosystem.config.js.exampleusesnext start -p 3001
Sprint B — mobile gallery (shipped)
- Mobile grid actions — download / favorite / report visible on touch (not
group-hoveronly) - Mobile filter drawer — slide-over panel on phones; desktop collapsible sidebar unchanged
- Skeleton grid — pulse placeholders instead of full-page spinner on filter/load
- Progressive photo viewer — thumbnail first, then full-res swap (local files)
Sprint C — admin throughput (shipped)
- Identify arrow-key nav — ←/→ prev/next face in queue (skips when focus is in form fields)
- Toasts + confirm dialogs — Identify, Auto-Match, Approve Identified
- Auto-Match progress + cancel — indeterminate bar, elapsed timer, abort in-flight request
- Approve Identified pagination — API
page/page_size+ batched face→photo lookup; UI 25 rows/page - Approve in-app photo preview — lightbox instead of
window.open
Sprint D — hardening (shipped)
- Admin e2e — Identify, Auto-Match, Approve pages + gallery scroll restore (
admin.review-pages.spec.ts,viewer.scroll-restore.spec.ts) - Core Web Vitals budget — LCP + CLS gates on home gallery (
viewer.web-vitals.spec.ts,web-vitals-budgets.ts) - Unify
/and/search—/searchredirects to/?…with full query string (peopleMode,tagsMode, etc.)
UX polish (2026-08)
- Favorite sign-in copy — logged-out heart opens favorites messaging (not report) via
SignInRequiredDialog - Text contrast — replace misplaced
text-secondary(pale wash token) withtext-foreground/text-primaryon labels, auth dialogs, menus
Face recognition accuracy (2026-08)
- 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 indocs/FACE_MATCH_ERROR_TAGGING.md - Phase 2 — trusted refs + stricter Auto-Match + accept/reject log
- Phase 3 — recalibrate confidence on labeled pairs; re-score corpus
- Phase 4 — multi-ref / ensemble embeddings
- Phase 5 — harder reject of junk detections (tiny/blur/pose)
- Phase 6 — multi-embedding per person
Later
- Proper DEV deploy (
next start+ CI image) instead of long-livednext dev—scripts/deploy-viewer.sh,ecosystem.config.js.exampleusesnext 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.