# 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) ## 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 `10.0.10.201`: 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) - [ ] **git-ci-01 still OOM-kills under multi-repo bursts even at 8 GB** — bumped LXC 241 3→8 GB (ansible PR #128) after act_runner OOM-killed mid-job; that fix landed, but **pve10 the hypervisor itself is overcommitted** (~75 GB allocated across guests vs 62 GB physical RAM, host swap observed 7.9/8.0 GB used, ~8-9 GB free) — a burst of concurrent CI across `ansible`/`punimtag`/`playkit`/`maCopy` OOM-killed act_runner three times in 13 minutes on 2026-07-15 even with the larger allocation. Real fix is host-wide: audit over-allocated idle guests (e.g. `automationlab` 4 GB stopped, `immich`/`paperless` at 6 GB each) and either trim or move some guests to pve201. Out of scope for a CI-runner-only fix — flagging for a deliberate pass. - [ ] **DEV LXC (9101) redeploy** — `pct exec 9101` shows viewer-frontend checked out at `e54c609`, **19 commits behind** `dev` HEAD (missing e.g. `c011c80` accessibility/aria-label pass). Two uncommitted local files from an earlier session (`e2e/tests/admin.manage-users-actions.spec.ts`, `e2e/pages/ManageUsersPanel.ts` — real Manage-Users CRUD + a causal cross-session deactivation check) time out because the deployed UI's row action buttons lack the `aria-label`s those specs target; every other spec here is unaffected since it doesn't depend on that commit. Redeploy DEV, then commit + verify those two files. ### 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. ## Later - [ ] Proper DEV deploy (`next start` + CI image) instead of long-lived `next dev` - [ ] 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`.