diff --git a/.planning/phases/10-admin-vue-spa/10-VERIFICATION.md b/.planning/phases/10-admin-vue-spa/10-VERIFICATION.md index 63749a6..d54dfb8 100644 --- a/.planning/phases/10-admin-vue-spa/10-VERIFICATION.md +++ b/.planning/phases/10-admin-vue-spa/10-VERIFICATION.md @@ -1,8 +1,8 @@ --- phase: 10-admin-vue-spa -verified: 2026-09-27T18:45:00Z -status: human_needed -score: 4/4 roadmap success criteria verified by automated evidence (plan truths 45/46 verified, 1 abstained non-inferable) +verified: 2026-09-27T18:43:00Z +status: passed +score: 4/4 roadmap success criteria verified (plan truths 46/46 verified; A3 closed by human UAT) covered_files: - ".gitignore" - ".planning/phases/10-admin-vue-spa/10-01-PLAN.md" @@ -146,8 +146,12 @@ covered_files: - "admin/vitest.config.ts" - "boardwalk/boardwalk.go" - "boardwalk/boardwalk_test.go" + - "boardwalk/dist/index.html" - "bouncer/cookie_guard_test.go" - "bouncer/jwt.go" + - "bouncer/jwt_guard_test.go" + - "bouncer/refresh.go" + - "bouncer/refresh_test.go" - "bouncer/registry_test.go" - "cabana/admin_openapi.go" - "cabana/admin_paths_test.go" @@ -177,6 +181,7 @@ covered_files: - "cabana/phase10_csrf_test.go" - "cabana/prefix.go" - "cabana/query_test.go" + - "cabana/refresh_revocation_test.go" - "cabana/registry.go" - "cabana/relation.go" - "cabana/relation_field.go" @@ -206,8 +211,8 @@ covered_files: - "surf/middleware_test.go" - "surf/router.go" - "surf/router_test.go" -covered_digest: "v2:sha256:e78a8c0a010c731fccbd5a20d3f708e03eb957734c07f3739f4eae8249ecb0c0" -covered_files_note: "fonoteka.go files are outside the project root and cannot be fingerprinted; they are listed in the report body (Required Artifacts) and were checked at fonoteka.go HEAD 3359a83." +covered_digest: "v2:sha256:d9ac088393759ff9aab5aa67b3cb4a80a04c978e48adb20618eaa71b6154a3b3" +covered_files_note: "fonoteka.go files are outside the project root and cannot be fingerprinted. They are listed in the report body (Required Artifacts) and were checked at fonoteka.go HEAD 3359a83, which is unchanged since the previous verification and has a clean working tree." behavior_unverified: 0 overrides_applied: 0 mvp_mode_note: "ROADMAP marks Phase 10 mode: mvp, but the goal is not a User Story. Following the Phase 1/3/5/8 precedent, the four ROADMAP success criteria are the contract and User Flow Coverage is derived from them." @@ -215,58 +220,77 @@ decision_coverage: honored: 28 total: 28 not_honored: [] -insufficient_spec_items: - - truth: "[flagged assumption A3] Browsers accept the Secure admin cookie on http://localhost during development" - reason: insufficient_spec - note: "The production refusal of cookie_secure=false is tested (TestPhase10Prefix/cookie_secure). Browser acceptance of a Secure cookie on http://localhost is browser behavior that no test can observe." -escalations: - - finding: "CR-01 (open, critical): POST /auth/refresh ignores tokens_valid_after and is_activated and re-mints iat=now" - classification: "Not a failed Phase 10 must-have. It falsifies the Phase 9 09-02 truth that admin:reset-password invalidates earlier tokens (T-09-04). The Phase 10 SPA's automatic refresh on 401 makes it the default path." - decision_needed: "Fix before the phase closes (recommended: small change in cabana/auth.go refresh plus one regression test), or accept it with an override and a tracked follow-up." +re_verification: + previous_status: human_needed + previous_score: "4/4 roadmap success criteria (plan truths 45/46, 1 abstained non-inferable)" + previous_head: f47a560 + gaps_closed: + - "CR-01 escalation: admin POST /auth/refresh now enforces tokens_valid_after, is_activated and soft delete through bouncer.RefreshAudienceFor (be4a923, a13a121)" + - "A3 insufficient_spec: Secure admin cookie accepted on http://localhost (closed by human UAT, 10-UAT.md test 6)" + - "All 7 human verification items approved in 10-UAT.md (status: complete)" + gaps_remaining: [] + regressions: [] human_verification: - - test: "Decide CR-01 before closing the phase. Suggested repro: log in to /plytadmin in a browser, run `summer admin:reset-password `, wait for the access token to expire (or delete nothing and just reload after expiry), then click any list." - expected: "Secure behavior: the SPA lands on the login screen. Current code: the SPA silently refreshes and keeps working for up to refresh_ttl (14 days)." - why_human: "This is a security-policy decision (fix now vs accept with override). The code path is confirmed by reading cabana/auth.go:217-241, bouncer/refresh.go and bouncer/mint.go:48-60, but no test covers it." - - test: "At /plytadmin, log in as a limited admin (role with only golem15.fonoteka.access_genres) and then as a superuser/developer." - expected: "Limited admin: the rail shows only fonoteka, the side panel only Genres, no Ustawienia item. Superuser: Albums, Collections, Genres, Styles, Artists plus Ustawienia." - why_human: "The server filtering is proven on PostgreSQL and the rail/panel rendering is proven with fixtures in happy-dom. No test renders the real SPA against the real server in a browser (D-23: no browser e2e)." - - test: "Walk through Albums, Artists, Collections, Genres and Styles at /plytadmin: list (search, sort, filter, page, bulk delete), open a record, edit, save, create. Include Albums' genre (single relation with emptyOption) and artists (multiple relation chips), and Ustawienia > search_use_typesense." - expected: "Each list and form renders the columns and fields from its YAML. Saves show the toast. 422 errors show under their fields. The datetime and switch columns render as designed." - why_human: "SPA component tests use neutral acme fixtures. The fonoteka schemas are proven only at the API level. The end-to-end user flow in a real browser is unobserved." - - test: "Open an existing Collection, go to the Editors tab, open Dodaj, search a user, select across pages, confirm, then select the linked row and unlink it." - expected: "The picker shows 5 per page with the owner excluded. Dodaj (N) is disabled at 0. The linked list refreshes with the plural toast. Unlink asks for confirmation, then removes the row. The relation manager is absent on the create form." - why_human: "The link/unlink round trip is proven by TestPhase10AssembledAcceptance (API) and relation.smoke.test.ts (mocked fetch). The real browser round trip, focus trap and Esc behavior need a person." - - test: "Visual check in light and dark (system preference) at desktop width and at about 900px, against .planning/phases/10-admin-vue-spa/design." - expected: "Matches the design tokens. The sidebar stays dark in both modes. Below about 1100px the section panel collapses to the rail, and hovering or focusing a rail item opens the flyout, which closes on Esc and returns focus." - why_human: "Visual appearance and responsive behavior cannot be verified by grep or happy-dom." - - test: "Run the admin with backend.cookie_secure unset (default true) on http://localhost:/plytadmin in Chrome and Firefox and log in." - expected: "The browser stores the summer_admin cookie and the session works (flagged assumption A3)." - why_human: "Browser cookie policy for Secure cookies on localhost is outside any test (non-inferable truth, insufficient_spec)." - - test: "Review the 17 judgment-tier plan prohibitions in the Prohibitions table below." - expected: "Accept or reject the verifier's non-authoritative verdict for each (all currently 'not violated')." - why_human: "The prohibitions are judgment-tier. The verifier's verdict is non-authoritative by design." + - test: "Decide CR-01 (refresh ignores tokens_valid_after / is_activated)" + expected: "Fix now or accept with override" + why_human: "Security-policy decision" + resolution: "Fixed in quick 260927-q23 (be4a923, a13a121). TestAdminRefreshRevocation and TestRefreshAudienceForSubject re-run by the verifier: PASS. Approved in 10-UAT.md test 1." + - test: "Limited admin vs superuser navigation at /plytadmin" + expected: "Limited admin sees only fonoteka > Genres, no Ustawienia; superuser sees all five plus Ustawienia" + why_human: "Real browser against the real server (D-23: no browser e2e)" + resolution: "Approved in 10-UAT.md test 2 (local fonoteka binary, superuser plus genres-only admin)." + - test: "Walkthrough of the five controllers and Ustawienia" + expected: "Lists and forms render from YAML; search, sort, filter, paging, bulk delete, save toast, 422 field errors, album relations, search_use_typesense" + why_human: "End-to-end user flow in a real browser" + resolution: "Approved in 10-UAT.md test 3." + - test: "Collection editors link/unlink round trip" + expected: "Picker 5 per page, owner excluded, Dodaj (N) disabled at 0, plural toast, confirm unlink, focus trap and Esc, no manager on create" + why_human: "Real browser round trip, focus trap and Esc" + resolution: "Approved in 10-UAT.md test 4." + - test: "Visual check in light/dark at desktop and about 900px" + expected: "Matches design/, dark sidebar, rail collapse and flyout below about 1100px" + why_human: "Visual appearance and responsive behavior" + resolution: "Approved in 10-UAT.md test 5, before and after the full-width form change (2585671)." + - test: "Secure cookie on http://localhost (assumption A3)" + expected: "Chrome and Firefox store summer_admin with default cookie_secure" + why_human: "Browser cookie policy, non-inferable" + resolution: "Approved in 10-UAT.md test 6." + - test: "Review the 17 judgment-tier prohibitions" + expected: "Accept or reject the verifier's non-authoritative verdicts" + why_human: "Judgment-tier prohibitions need human resolution" + resolution: "All 17 'not violated' verdicts accepted in 10-UAT.md test 7." --- # Phase 10: Admin Vue SPA Verification Report **Phase Goal:** A minimal Vue 3 + TypeScript admin SPA renders login, permission-gated navigation, lists, forms and the relation manager for Albums, Artists, Collections, Genres and Styles, typed from the generated OpenAPI document. -**Verified:** 2026-09-27T18:45:00Z -**Status:** human_needed -**Re-verification:** No, initial verification +**Verified:** 2026-09-27T18:43:00Z (summercms.go HEAD c7487f6, fonoteka.go HEAD 3359a83) +**Status:** passed +**Re-verification:** Yes. The previous report (f47a560, human_needed) went stale when CR-01 was fixed (be4a923, a13a121) and the forms became full width (2585671). **MVP note:** ROADMAP marks this phase `mode: mvp`, but the goal is not a User Story. Following the precedent of Phases 1, 3, 5 and 8, the four ROADMAP success criteria are the contract and plan `must_haves` are supporting evidence. +## What changed since the previous verification + +| Commit | Change | Effect on this report | +|---|---|---| +| be4a923 | `cabana/auth.go` refresh calls `bouncer.RefreshAudienceFor(r.Context(), s.users, ...)`. `s.users` is the same `lazyBackendUsers` provider the backend guard uses (`cabana/http.go`). A refusal of the subject over cookie transport expires `summer_admin`. `bouncer/refresh.go` runs `subjectPrincipal` and `issuedBeforeCutoff` after every token-only check and before minting. `Refresh` and `RefreshAudience` pass a nil hook, so their behavior is unchanged. | Closes the CR-01 escalation. The diff was read, and the named tests were re-run (see Spot-Checks). | +| a13a121 | Adds `TestRefreshAudienceForSubject`, a `TestJWTGuardTokensValidAfter` pin, and a `TestPhase10Coverage` subtest for cookie expiry on subject refusal | Behavioral evidence for the fix | +| 2585671 | `FormView.vue` and `SettingsFormView.vue` drop `mx-auto max-w-[980px]`. The dist was rebuilt. | CSS only. The dist drift gate and the 441 Vitest tests re-run clean. | +| c7487f6 | 10-REVIEW.md re-review (0 open critical), 10-UAT.md complete (7/7 pass), disposition updated | Human verification closed | + ## User Flow Coverage Derived user story: *As a Płytarium admin, I want to log in to /plytadmin, see only what my role permits, and manage Albums, Artists, Collections, Genres, Styles and Collection editors, so that the catalogue can be administered without the PHP backend.* | Step | Expected | Evidence | Status | |---|---|---|---| -| Open /plytadmin | Embedded SPA served with base /plytadmin | `boardwalk/boardwalk.go`, `TestPhase10TracerSPA` (run: PASS), `check-admin-dist.sh` (run: dist matches a fresh build) | VERIFIED | -| Log in | Cookie session, no token in body | `cabana/auth.go` login, `useAuth.login`, `TestPhase10AdminAuth` (run: PASS), `LoginView.test.ts` | VERIFIED | -| See permitted navigation | Limited admin sees Genres only | `TestPhase10AssembledAcceptance` SC-1 (run: PASS), `useNavigation.ts` renders server data | VERIFIED (browser: human) | -| Use five lists and forms | Schema-driven list and form, create, update | `TestPhase10AssembledAcceptance` SC-2, `TestPhase10Controllers` (run: PASS), ListView/FormView smoke tests | VERIFIED (browser: human) | -| Link and unlink an editor | Search candidates, link, unlink | `TestPhase10AssembledAcceptance` SC-3, `relation.smoke.test.ts` | VERIFIED (browser: human) | +| Open /plytadmin | Embedded SPA served with base /plytadmin | `boardwalk/boardwalk.go`, `TestPhase10TracerSPA` (re-run: PASS), `check-admin-dist.sh` (re-run: dist matches a fresh build) | VERIFIED | +| Log in | Cookie session, no token in body | `cabana/auth.go` login, `useAuth.login`, `TestPhase10AdminAuth`, `LoginView.test.ts`; UAT test 6 (Secure cookie on localhost) | VERIFIED | +| See permitted navigation | Limited admin sees Genres only | `TestPhase10AssembledAcceptance` SC-1 (re-run: PASS); UAT test 2 | VERIFIED | +| Use five lists and forms | Schema-driven list and form, create, update | `TestPhase10AssembledAcceptance` SC-2, `TestPhase10Controllers` (re-run: PASS), ListView/FormView tests; UAT test 3 | VERIFIED | +| Link and unlink an editor | Search candidates, link, unlink | `TestPhase10AssembledAcceptance` SC-3, `relation.smoke.test.ts`; UAT test 4 | VERIFIED | +| Stay signed in / be signed out | Refresh keeps an active session; a reset, deactivation or deletion ends it | `TestAdminRefreshRevocation` (re-run on Postgres: 5/5 subtests PASS) | VERIFIED | | Log out | Cookie expired, old cookie 401 | `TestPhase10AssembledAcceptance` (logout then /auth/me 401) | VERIFIED | ## Goal Achievement @@ -275,175 +299,169 @@ Derived user story: *As a Płytarium admin, I want to log in to /plytadmin, see | # | Truth | Status | Evidence | |---|---|---|---| -| 1 | An admin logs in through the SPA and sees only the navigation items their permissions allow. | ✓ VERIFIED | Server: `TestPhase10AssembledAcceptance` asserts the limited admin's nav is exactly `fonoteka:[genres]`, the developer's is `albums,collections,genres,styles,artists`, and that the limited admin gets 403 on albums and an empty settings list (re-run this session on testcontainers Postgres: PASS 0.70s). SPA: `useNavigation.ts` stores `/navigation` verbatim and `railEntries` only drops plugins with an empty side menu (D-11); `PluginRail.test.ts`, `SectionPanel.test.ts`, `tracer.smoke.test.ts`. Login: `LoginView.vue` → `useAuth.login` → `api.POST('/auth/login')`; `main.ts` boots `/lang` → `/auth/me` → `/navigation`. Real-browser rendering is a human item. CR-01 does not falsify this truth: it concerns session revocation, and permission filtering still runs on every request. | -| 2 | Each of the five controllers renders a working list and form generated from its JSON schema. | ✓ VERIFIED | Server: SC-2 loop in `TestPhase10AssembledAcceptance` (list schema, list, form schema, create 201, update, show, album genre/artists relations persisted) and `TestPhase10Controllers` (fields and columns equal the tracked YAML); both PASS. SPA: `ListView.vue` loads `/schema/list` and the list, and `FormView.vue` loads `/schema/form` and the record, then POSTs or PUTs. Every form field type in fonoteka's fields.yaml (text, textarea, dropdown, switch, checkbox, relation, relation-manager) is registered in `registry.ts`. Both column types (datetime, switch) are handled in `CellValue.vue`. 441 Vitest tests pass (re-run: 48 files, 441 passed). WR-05 (loaders without try/catch stay stuck loading on network failure) is an open warning. | -| 3 | The Collections form's relation manager lets an admin search, link and unlink an editor. | ✓ VERIFIED | Server: the SC-3 block searches candidates (the owner is excluded), links, lists, sees the linked user drop from the candidates, unlinks, and sees the list empty (PASS). SPA: `RelationManager.vue` (linked list, unlink with confirm, `/relations/{name}/unlink`), `RelationPickerModal.vue` (`/candidates` with 5 per page, `/link` with ids). It is registered as `relation-manager` and rendered only in update mode (`FormView.vue:71`). Tests: `RelationManager.test.ts`, `RelationPickerModal.test.ts`, `relation.smoke.test.ts`. | -| 4 | API calls in the SPA use TypeScript types generated from the OpenAPI document, with no hand-maintained duplicate type. | ✓ VERIFIED | `client.ts` is `createClient` over the generated `schema.d.ts`. All 26 `api.*` call sites use typed path templates. There is no other `fetch(` in `admin/src`. `types.ts` is aliases onto `components['schemas']` only, and the hygiene gate enforces that. `check-admin-openapi.sh --check` re-run: exit 0 (document and types drift-clean). `vue-tsc --noEmit` re-run: clean. SC-4 in the acceptance test: every called template is in `admin/openapi/admin.json`. Info: `app/controllerRoutes.ts` declares a local `ControllerParams` interface with the same shape as the generated path-params alias. It is used for parsing controller ids, not for API payloads, but it could simply alias the generated type. | +| 1 | An admin logs in through the SPA and sees only the navigation items their permissions allow. | ✓ VERIFIED | Server: `TestPhase10AssembledAcceptance` asserts the limited admin's nav is exactly `fonoteka:[genres]` and the developer's is `albums,collections,genres,styles,artists`, that the limited admin gets 403 on albums, and that the limited admin gets an empty settings list. Re-run this session on testcontainers Postgres: PASS. SPA: `useNavigation.ts` stores `/navigation` verbatim. `railEntries` drops only plugins whose side menu is empty (D-11). Tests: `PluginRail.test.ts`, `SectionPanel.test.ts`, `tracer.smoke.test.ts`. Real browser: UAT test 2 approved with a superuser and a genres-only admin. The CR-01 fix makes the session end correctly on reset (`TestAdminRefreshRevocation`). | +| 2 | Each of the five controllers renders a working list and form generated from its JSON schema. | ✓ VERIFIED | Server: the SC-2 loop in `TestPhase10AssembledAcceptance` and `TestPhase10Controllers` (fields and columns equal the tracked YAML). Both re-run: PASS. SPA: `ListView.vue` loads `/schema/list` and the list. `FormView.vue` loads `/schema/form` and the record, then POSTs or PUTs. The full-width change (2585671) touches only the section's class attribute. Vitest re-run: 48 files, 441 passed. Real browser: UAT test 3 approved. WR-05 (loaders without try/catch) is still open as a warning. | +| 3 | The Collections form's relation manager lets an admin search, link and unlink an editor. | ✓ VERIFIED | Server: the SC-3 block covers candidates with the owner excluded, link, list, the linked user dropping out of the candidates, unlink, and an empty list afterwards (re-run: PASS). SPA: `RelationManager.vue` and `RelationPickerModal.vue`, registered as `relation-manager` and shown in update mode only. Tests: `RelationManager.test.ts`, `RelationPickerModal.test.ts`, `relation.smoke.test.ts`. Real browser: UAT test 4 approved. | +| 4 | API calls in the SPA use TypeScript types generated from the OpenAPI document, with no hand-maintained duplicate type. | ✓ VERIFIED | `client.ts` is `createClient` over the generated `schema.d.ts`. `admin/src` has no other `fetch(`. `types.ts` contains only aliases onto `components['schemas']`, and the hygiene gate enforces that. `check-admin-openapi.sh --check` re-run: exit 0. The CR-01 fix changed no route or response shape, and the document did not drift. Info: `controllerRoutes.ts` has a local `ControllerParams` interface. It parses route ids and is not an API payload. | **Score:** 4/4 ROADMAP truths verified (0 present-but-behavior-unverified). ### Plan must-have truths (supporting evidence) -46 plan truths across 10-01..10-05. 45 are verified by named, passing tests or gates: -- the prefix, cookie and CSRF truths: `TestPhase10Prefix`, `TestPhase10CookieAuth`, `TestPhase10CSRF`, `TestPhase10CookieGuard`, `TestPhase10AdminPrefixCollision` -- boardwalk: `TestPhase10BoardwalkServing` -- relation options and saves: `TestPhase10RelationOptions`, `TestPhase10RelationSave`, `TestPhase10RelationForgedID`, `TestPhase10AlbumRelations`, `TestPhase10CollectionOwnerReadOnly` -- lang and messages: `TestPhase10Bundle`, `TestPhase10LangOverride`, `TestPhase10Messages`, `TestPhase10SPAKeysResolve` -- toolbar and filters: `TestPhase10Toolbar`, `TestPhase10FilterOptions` -- conformance: `TestPhase10OpenAPIConformance` -- SPA truths: the Vitest suites -- gate, security review and validation truths: `check-phase10.sh --all` (orchestrator run: exit 0), `10-SECURITY-REVIEW.md`, `10-VALIDATION.md` (`nyquist_compliant: true`) +There are 46 plan truths across 10-01..10-05. 45 were verified by named, passing tests or gates in the previous run, and their files are unchanged apart from the two form views and the refresh path. Those two are covered again below. + +The 46th is backstop truth A3: browsers accept the Secure admin cookie on http://localhost. The previous run abstained on it as `insufficient_spec`. It is now closed by directly observed behavior: in UAT test 6 the user ran the fonoteka binary at http://localhost:8080/plytadmin with the default `cookie_secure`, and the session worked. + +The refresh path affects truths that touch cookie refresh (10-01 cookie auth, T-10-05). These were re-checked with `TestPhase10Coverage` (PASS), `TestAdminRefreshRevocation` (PASS, Postgres) and `TestRefreshAudienceForSubject` (PASS). The quick task's removal check (run with the check hook set to nil) makes both unit tests fail, so the tests do exercise the hook. Backstop (non-inferable) truths: | Truth | Evidence | Status | |---|---|---| -| A1 default prefix /backend; fonoteka /plytadmin | `DefaultAdminPrefix = "/backend"`, `TestPhase10Prefix` case `"" → /backend`, `config/backend.yaml uri: /plytadmin` | VERIFIED | -| A3 Secure cookie accepted on http://localhost | Only the production refusal is tested | ⚠️ insufficient_spec (human) | +| A1 default prefix /backend; fonoteka /plytadmin | `DefaultAdminPrefix = "/backend"`, `TestPhase10Prefix`, fonoteka `config/backend.yaml uri: /plytadmin` | VERIFIED | +| A3 Secure cookie accepted on http://localhost | 10-UAT.md test 6: pass (observed in a real browser) | VERIFIED (human-observed) | | A10 30 s blacklist grace plus single-flight refresh | `config/admin.yaml blacklist_grace: 30`, `client.test.ts` single-flight cases | VERIFIED | -| A8 pivot sort_order = array index | `admin_phase10_relations_test.go:394` asserts the sort_order rows | VERIFIED | -| fonoteka has no RelationExtendOptionsQuery | the `albums_admin_controller.go:58` comment; no implementation in fonoteka; the hook is proven with acme fixtures | VERIFIED | -| Required relation is a schema hint only | Covered by the Phase 9 decision 304 tests plus `TestPhase10RelationSave` | VERIFIED | -| A6 dark mode follows the system only | `theme.ts` matchMedia, `theme.test.ts` | VERIFIED | -| SC-4 mechanical enforcement | `check-phase10.sh` hygiene lines 323-331 | VERIFIED (scope: `admin/src/api` only, see Info) | +| A8 pivot sort_order = array index | `admin_phase10_relations_test.go` sort_order assertions | VERIFIED | +| fonoteka has no RelationExtendOptionsQuery | no implementation in fonoteka; the hook is proven with acme fixtures | VERIFIED | +| Required relation is a schema hint only | Phase 9 decision 304 tests plus `TestPhase10RelationSave` | VERIFIED | +| A6 dark mode follows the system only | `theme.ts` matchMedia, `theme.test.ts`; UAT test 5 | VERIFIED | +| SC-4 mechanical enforcement | `check-phase10.sh` hygiene rules | VERIFIED | -### Prohibitions (judgment tier, non-authoritative verdicts, human review recommended) +### Prohibitions (judgment tier) -| Plan | Prohibition | Verdict | Evidence | -|---|---|---|---| -| 01 | No Płytarium/fonoteka names in summercms.go SPA, fixtures, document or dist | not violated | grep over admin/src, tests, openapi, boardwalk, cabana, phrasebook, bouncer, surf: 0 hits; hygiene gate appname plant self-test | -| 01 | Cookie login/refresh never carries the JWT; the SPA never reads or stores it | not violated | `cookieLoginData`, `phase10NoToken` in the acceptance test, hygiene storage rule | -| 01 | No foreign-origin fonts, icons or scripts | not violated | dist URLs: only w3.org namespaces and a vuejs.org warning string; fonts from @fontsource bundled | -| 01 | Non-admin routes and the parity doc change only by dropping admin paths | not violated | `git diff feaca6b..HEAD -- docs/openapi.json`: 1672 deletions, 0 additions, all `/_admin/api/v1/*` and cabana schemas | -| 02 | Relation save never writes a protected FK or an out-of-scope id | not violated in declared config | `TestPhase10RelationForgedID`, `TestPhase10CollectionOwnerReadOnly`. WR-02 notes a misconfiguration bypass (a scalar FK field declared next to a relation) | -| 02 | Public bundle exposes only backend::lang | not violated | `TestPhase10Bundle` | -| 02 | Framework never names a plugin table, pivot or FK | not violated | hygiene appname rule, `relation_field.go` reads the contract | -| 02 | Phase 9 security assertions not weakened | not violated | `check-phase9.sh --all` passes (orchestrator) | -| 03 | Plugin text rendered as text only | not violated | no `v-html` or `innerHTML` in admin/src; hygiene vhtml plant | -| 03 | No hand-written API payload shapes | not violated | `types.ts` aliases only (see the SC-4 Info) | -| 03 | SPA does not hide or add nav, actions or fields | not violated | nav, toolbar and fields come from the server; the only local rule is D-11 (hide a plugin with an empty side menu) | -| 03 | Winter URLs not used verbatim | not violated | `winterUrl.ts`, `winterUrl.test.ts` | -| 04 | SPA does not filter candidates itself | not violated | `RelationPickerModal.fetchPage` renders `data.data` unfiltered | -| 04 | localStorage holds only the sidebar preference | not violated | the only use is `useSidebar.ts`; hygiene storage rule | -| 05 | Acceptance does not depend on skips, zero-test runs or hand-edited dist/types | not violated | the detector refuses skip and zero-test runs; dist and openapi drift re-run clean; no `it.skip` or `t.Skip` in phase tests | -| 05 | No app names in summercms.go tests | not violated | same grep as above | -| 05 | High threats cite an executable test or gate | formally met | but see the CR-01 note: the T-10-05 residual risk ("valid until logout or expiry") understates the revocation gap | +All 17 verdicts from the previous report were "not violated", and the user accepted them in 10-UAT.md test 7. The changes since then do not touch what they cover. The CR-01 fix adds no app names, no token in a response body and no new route. The CSS change adds no `v-html` and no foreign-origin asset. The one qualified verdict from before was the 05 prohibition "High threats cite an executable test or gate", which was only formally met. It is now met outright: the T-10-05 row in 10-SECURITY-REVIEW.md cites `TestAdminRefreshRevocation` and `TestRefreshAudienceForSubject`, and its residual-risk text now describes revocation on reset, deactivation and deletion accurately, with WR-07 named as the remaining sliding-window risk. + +| Plan | Prohibition | Verdict | +|---|---|---| +| 01 | No Płytarium/fonoteka names in summercms.go SPA, fixtures, document or dist | not violated (human-accepted) | +| 01 | Cookie login/refresh never carries the JWT; the SPA never reads or stores it | not violated (human-accepted; the refresh cookie path still returns `token_type: cookie` only) | +| 01 | No foreign-origin fonts, icons or scripts | not violated (human-accepted) | +| 01 | Non-admin routes and the parity doc change only by dropping admin paths | not violated (human-accepted) | +| 02 | Relation save never writes a protected FK or an out-of-scope id | not violated in declared config (human-accepted; WR-02 open) | +| 02 | Public bundle exposes only backend::lang | not violated (human-accepted) | +| 02 | Framework never names a plugin table, pivot or FK | not violated (human-accepted) | +| 02 | Phase 9 security assertions not weakened | not violated; strengthened by the CR-01 fix, which restores T-09-04 for admin sessions | +| 03 | Plugin text rendered as text only | not violated (human-accepted) | +| 03 | No hand-written API payload shapes | not violated (human-accepted) | +| 03 | SPA does not hide or add nav, actions or fields | not violated (human-accepted) | +| 03 | Winter URLs not used verbatim | not violated (human-accepted) | +| 04 | SPA does not filter candidates itself | not violated (human-accepted) | +| 04 | localStorage holds only the sidebar preference | not violated (human-accepted) | +| 05 | Acceptance does not depend on skips, zero-test runs or hand-edited dist/types | not violated (dist and openapi drift re-run clean) | +| 05 | No app names in summercms.go tests | not violated (human-accepted) | +| 05 | High threats cite an executable test or gate | not violated (T-10-05 now cites the revocation tests) | ### Required Artifacts -All 30 plan artifacts pass `verify.artifacts` (exists and substantive). Wiring was checked by hand: +All 30 plan artifacts passed `verify.artifacts` in the previous run, and none was deleted. Changed or added since then: | Artifact | Status | Details | |---|---|---| -| `cabana/prefix.go`, `cabana/csrf.go`, `cabana/relation_field.go`, `cabana/messages.go`, `cabana/lang.go` | ✓ VERIFIED | Wired from `cabana/http.go` and `crud.go`; exercised by the named tests | -| `boardwalk/boardwalk.go` + `boardwalk/dist` | ✓ VERIFIED | Mounted by `cabana/http.go` via `boardwalk.Handler`; the dist equals a fresh build | -| `internal/tools/swagger2openapi/main.go`, `scripts/check-admin-openapi.sh`, `scripts/check-admin-dist.sh` | ✓ VERIFIED | Both gates re-run: exit 0 | -| `admin/src/api/client.ts`, `schema.d.ts`, `types.ts` | ✓ VERIFIED | The only HTTP path in the SPA | -| `admin/src/views/{ListView,FormView,SettingsFormView}.vue`, `components/list/*`, `components/form/*` | ✓ VERIFIED | Routed in `router.ts`; data from typed calls | -| `admin/src/components/relation/{RelationManager,RelationPickerModal}.vue` | ✓ VERIFIED | Registered as `relation-manager`; calls link, unlink and candidates | -| `admin/src/components/shell/{SectionFlyout,UserMenu}.vue`, `state/useSidebar.ts` | ✓ VERIFIED | UserMenu → `useAuth.logout` → `POST /auth/logout` | -| `scripts/check-phase10.sh` | ✓ VERIFIED | Fail-closed detector; orchestrator `--all` exit 0 | -| `../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_{tracer,controllers,e2e,auth,relations,copy}_test.go` | ✓ VERIFIED | e2e, tracer, controllers, auth and owner tests re-run this session: PASS | -| `10-SECURITY-REVIEW.md`, `10-VALIDATION.md` | ✓ VERIFIED | Present and complete; T-10-05 residual wording is inaccurate (CR-01) | +| `bouncer/refresh.go` (`RefreshAudienceFor`) | ✓ VERIFIED | Substantive, and wired from `cabana/auth.go:224`. `Refresh` and `RefreshAudience` keep their signatures and pass a nil hook, so the core user plugin contract is unchanged. | +| `bouncer/jwt.go` (`subjectPrincipal`, `issuedBeforeCutoff`, `ErrSubjectRejected`) | ✓ VERIFIED | Shared by the guard and refresh | +| `cabana/auth.go`, `cabana/http.go` | ✓ VERIFIED | `service.users` is the guard's `lazyBackendUsers` | +| `cabana/refresh_revocation_test.go`, `bouncer/refresh_test.go`, `bouncer/jwt_guard_test.go` | ✓ VERIFIED | Re-run: PASS | +| `admin/src/views/FormView.vue`, `SettingsFormView.vue` + `boardwalk/dist` | ✓ VERIFIED | CSS class change only; the dist matches a fresh build | +| `../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_*_test.go` | ✓ VERIFIED | Unchanged at 3359a83. Acceptance, tracer and controllers re-run: PASS | +| All other artifacts from 10-01..10-05 | ✓ VERIFIED | Unchanged since the previous run | ### Key Link Verification -`verify.key-links` reported 16/19. The 3 misses are tool false negatives, traced by hand: +The previous run checked all 19 links (16 by the tool, 3 traced by hand), and all were WIRED. The one new link: | From | To | Via | Status | |---|---|---|---| -| `admin/src/main.ts` | `GET /lang` | `main.ts` → `loadStrings()` (`app/i18n.ts:32` `api.GET('/lang')`) before `me()` | WIRED (indirect) | -| `UserMenu.vue` | `POST /auth/logout` | `UserMenu.vue:51 logout(router)` → `useAuth.ts:71 api.POST('/auth/logout')` | WIRED (indirect) | -| `10-VALIDATION.md` | 10-01..10-05 verify commands | 25 `10-0x` task rows | WIRED (the tool cannot parse a non-file `to`) | -| All others (surf→cabana prefix, cabana→boardwalk, auth→bouncer cookie, client→schema, crud→relation_field, translator→lang, RelationField→options, FilterBar→filter options, Picker→link, registry→RelationManager, gate→detectors) | | | WIRED | +| `cabana/auth.go` refresh | `bouncer.RefreshAudienceFor` → the guard's `UserProvider` | `s.users` set from the same `lazyBackendUsers` value passed to `NewBackendJWTGuard` (`cabana/http.go:113,137`) | WIRED | +| `bouncer/refresh.go` check hook | `subjectPrincipal` / `issuedBeforeCutoff` | closure passed as `check` to `refreshAudience`, called before `MintAudience` | WIRED | ### Data-Flow Trace (Level 4) -| Artifact | Data | Source | Real data | Status | -|---|---|---|---|---| -| PluginRail/SectionPanel | `navigation` | `GET /navigation` (server-filtered by permission) | yes, from the Postgres roles in the acceptance test | ✓ FLOWING | -| ListView/DataTable | rows, columns | `GET /{v}/{p}/{c}` + `/schema/list` | yes | ✓ FLOWING | -| FormView | fields, record, labels | `/schema/form` + `GET /{id}` | yes | ✓ FLOWING | -| RelationManager / Picker | linked, candidates | `/relations/{name}`, `/candidates` | yes | ✓ FLOWING | -| i18n | strings | `GET /lang` (`backend::lang` + plugin overrides) | yes | ✓ FLOWING | +These are unchanged from the previous run. Every flow is ✓ FLOWING: + +- navigation comes from `GET /navigation`, filtered on the server +- list rows and columns come from the list endpoint and `/schema/list` +- form fields and the record come from `/schema/form` and `GET /{id}` +- the relation manager's linked rows and candidates come from `/relations/{name}` and `/candidates` +- strings come from `GET /lang` ### Behavioral Spot-Checks (run this session) | Behavior | Command | Result | Status | |---|---|---|---| -| SC-1..SC-4 assembled on Postgres | `go test -run '^TestPhase10AssembledAcceptance$' ./plugins/golem15/fonoteka/` (fonoteka.go) | PASS 0.70s | ✓ PASS | -| Tracer, controllers, auth, owner read-only | `go test -run '^(TestPhase10TracerSPA\|TestPhase10Controllers\|TestPhase10CollectionOwnerReadOnly\|TestPhase10AdminAuth)$'` | 4 PASS | ✓ PASS | +| CR-01: reset, deactivation and deletion end admin refresh | `go test -count=1 -v -run '^TestAdminRefreshRevocation$' ./cabana/` | 5/5 subtests PASS (pre-reset cookie refused and expired, pre-reset Bearer refused, deactivated, soft-deleted, active still refreshes) | ✓ PASS | +| RefreshAudienceFor unit and guard pin | `go test -count=1 -run '^(TestRefreshAudienceForSubject\|TestJWTGuardTokensValidAfter)$' ./bouncer/` | ok | ✓ PASS | +| Cookie refresh coverage (incl. expiry on subject refusal) | `go test -count=1 -run '^TestPhase10Coverage$' ./cabana/` | ok | ✓ PASS | +| SC-1..SC-4 assembled on Postgres plus tracer and controllers | `go test -count=1 -run '^(TestPhase10AssembledAcceptance\|TestPhase10TracerSPA\|TestPhase10Controllers)$' ./plugins/golem15/fonoteka/` (fonoteka.go) | ok 6.8s | ✓ PASS | | SPA unit/component suite | `npx vitest run` (admin) | 48 files, 441 passed | ✓ PASS | -| SPA typecheck | `npx vue-tsc --noEmit` | no errors | ✓ PASS | +| Embedded dist drift | `scripts/check-admin-dist.sh` | vue-tsc clean, "boardwalk/dist matches a fresh build" | ✓ PASS | | OpenAPI and types drift | `scripts/check-admin-openapi.sh --check` | exit 0 | ✓ PASS | -| Embedded dist drift | `scripts/check-admin-dist.sh` | "boardwalk/dist matches a fresh build" | ✓ PASS | -| Phase gate | `scripts/check-phase10.sh --all` (orchestrator) | exit 0, two allow-listed pre-existing parity failures | ✓ PASS | +| Phase gate | `scripts/check-phase10.sh --all` (orchestrator, after the changes) | exit 0, "phase10 all passed", the two pre-existing parity failures allow-listed by name | ✓ PASS | ### Probe Execution -No `probe-*.sh` scripts are declared or present. The phase gate `check-phase10.sh` stands in for probes. It was run by the orchestrator, and its sub-gates (openapi, dist) and key tests were re-run here. - -### Test Quality Audit - -| Test File | Linked Req | Skipped | Circular | Assertion level | Verdict | -|---|---|---|---|---|---| -| `admin_phase10_e2e_test.go` | ADMIN-06 SC-1..4 | 0 | no | behavioral (multi-step, DB-asserted) | OK | -| `admin_phase10_controllers_test.go` | SC-2 | 0 | no (compares against the tracked YAML) | value | OK | -| `cabana/phase10_*_test.go`, `openapi_conformance_test.go` | ADMIN-06 backend | 0 | no | value/behavioral | OK | -| `admin/tests/**` (48 files) | SC-1..4 SPA | 0 | no | structural/behavioral with mocked fetch | OK (no real-browser run, D-23) | - -Disabled tests: 0. Circular patterns: 0. Insufficient assertions: 0. +No `probe-*.sh` scripts are declared or present. `check-phase10.sh` is the phase gate. The orchestrator ran it, and I re-ran its sub-gates (openapi, dist) and its key tests. ### Requirements Coverage | Requirement | Source Plan | Description | Status | Evidence | |---|---|---|---|---| -| ADMIN-06 | 10-01..10-05 | Minimal Vue 3 + TS SPA renders login, permission-gated navigation, lists, forms and the relation manager for the five controllers using generated types | ✓ SATISFIED (browser UAT pending) | Truths 1-4 above | +| ADMIN-06 | 10-01..10-05 | Minimal Vue 3 + TS SPA renders login, permission-gated navigation, lists, forms and the relation manager for the five controllers using generated types | ✓ SATISFIED | Truths 1-4, UAT 7/7. REQUIREMENTS.md marks it `[x]` and Complete. | -Orphaned requirements: none. ADMIN-06 is the only ID mapped to Phase 10 in REQUIREMENTS.md. +Orphaned requirements: none. ADMIN-06 is the only ID REQUIREMENTS.md maps to Phase 10. ### Decision Coverage -All 28 trackable CONTEXT.md decisions are honored by shipped artifacts (`check.decision-coverage-verify`). +All 28 trackable CONTEXT.md decisions are honored (unchanged since the previous run). ### Anti-Patterns Found | File | Line | Pattern | Severity | Impact | |---|---|---|---|---| -| `cabana/auth.go` | 217-241 | Refresh never loads the user; ignores `tokens_valid_after` and `is_activated` (CR-01) | ⚠️ Warning (escalated) | Password reset does not end SPA sessions; the SPA auto-refresh on 401 makes this the default path. Not a Phase 10 must-have; falsifies Phase 9 09-02 "reset invalidates earlier tokens" (T-09-04) | -| `cabana/auth.go`, `cabana/http.go` | 243-269, 196 | Logout behind the guard does not expire a rejected cookie (WR-01) | ⚠️ Warning | Stale cookie can linger; combines with CR-01 | -| `cabana/relation_field.go`, `crud.go` | — | Scalar FK next to a relation field bypasses the scope check (WR-02); validation runs before relation assignment (WR-03) | ⚠️ Warning | Not reachable with the current fonoteka YAML | -| `pact/capabilities.go` | 265-270 | `FilterOptions(scope)` has no ctx or db (WR-04) | ⚠️ Warning | New contract; cheaper to change now | -| `admin/src/views/*.vue`, `FilterBar.vue`, `LoginView.vue` | — | Loaders without try/catch (WR-05) | ⚠️ Warning | Network failure leaves the skeleton spinning | -| `ListView.vue`, `RelationManager.vue` | — | No page clamp after delete or unlink (WR-06) | ⚠️ Warning | Empty last page shown | -| `bouncer/refresh.go` | 52-71 | Sliding refresh with no absolute cap (WR-07) | ⚠️ Warning | Check against the PHP contract | -| `admin/src/app/controllerRoutes.ts` | 3 | Local `ControllerParams` interface duplicates the generated path-params alias shape | ℹ️ Info | Not an API payload; aliasing would remove any doubt about SC-4 | -| — | — | IN-01..IN-07 from 10-REVIEW.md | ℹ️ Info | Open, non-blocking | +| `cabana/auth.go` | refresh | CR-01 | resolved | Fixed in be4a923 and a13a121; verified above | +| `bouncer/refresh.go`, fonoteka `golem15/user` api_controller | — | WR-08: the frontend user refresh still ignores tokens_valid_after | ⚠️ Warning | Site-user audience, outside the Phase 10 contract. Deliberately left unchanged to keep the core user plugin's contract. Needs a parity check against PHP first. | +| `cabana/auth.go`, `cabana/http.go` | — | WR-01: logout behind the guard does not expire a rejected cookie | ⚠️ Warning | Open, non-blocking | +| `cabana/relation_field.go`, `crud.go` | — | WR-02 and WR-03 | ⚠️ Warning | Not reachable with the current fonoteka YAML | +| `pact/capabilities.go` | — | WR-04: `FilterOptions(scope)` has no ctx or db | ⚠️ Warning | Open | +| `admin/src/views/*.vue` and others | — | WR-05: loaders have no try/catch | ⚠️ Warning | A network failure leaves the skeleton spinning | +| `ListView.vue`, `RelationManager.vue` | — | WR-06: no page clamp after delete or unlink | ⚠️ Warning | Open | +| `bouncer/refresh.go` | — | WR-07: the refresh window slides with no absolute cap | ⚠️ Warning | Named as residual risk in T-10-05 | +| — | — | IN-01..IN-10 in 10-REVIEW.md | ℹ️ Info | Open, non-blocking | -No `TBD`, `FIXME` or `XXX` markers in any file changed by Phase 10 in either repository. The `PLACEHOLDER` hits in `DropdownField.vue` are a legitimate constant for the placeholder option. +No `TBD`, `FIXME` or `XXX` markers are in any file changed since the previous verification (`bouncer/jwt.go`, `bouncer/refresh.go`, `cabana/auth.go`, `cabana/http.go`, the new tests, and the two form views). The previous run found none in the rest of the Phase 10 files. ### Other observations (Info) -- `scripts/check-phase1.sh` fails with `surf: config http.body_limits.default_bytes is required` in `examples/hello`. Confirmed this session. It dates from Phase 6, not Phase 10. The orchestrator reports the same for phase 4, and a missing admin JWT secret in check-phase8 (since 09-01). -- The working tree has an uncommitted change to `examples/hello/main.go` that adds `cabana` and `RouteListCommand`. It is not part of any Phase 10 commit. Decide whether to keep or discard it separately. -- Phase 9 has no VERIFICATION.md, its ROADMAP entry is unchecked, and ADMIN-01..05 are still "Pending" in REQUIREMENTS.md, although Phase 10 depends on Phase 9. Close Phase 9's verification before or alongside this phase. -- Two fonoteka `parity` tests fail since Phase 9 (deferred-items.md). The gate allow-lists them by package and name and refuses once either passes. +- The summercms.go working tree is clean apart from `.planning/milestone.lock`, `.planning/state.json`, `.gsd/` and `go.work.sum`. None of these is Phase 10 code. The uncommitted `examples/hello/main.go` change noted in the previous report is gone. +- Two fonoteka `parity` tests have failed since Phase 9 (deferred-items.md). The gate allow-lists them by name and refuses once either passes. +- The quick task saw one-off load flakes in fonoteka `golem15/user` (`TestCodes`, `TestForgotPassword`) during a parallel gate run. The orchestrator reports those tests pass when run uncached, and the final `--all` gate exited 0. +- Phase 9 still has no VERIFICATION.md. CR-01 was the Phase 9 T-09-04 concern, and it is now resolved for the admin audience. -### Human Verification Required +### Human Verification -1. **CR-01 decision (escalation).** `/auth/refresh` does not re-check `tokens_valid_after` or `is_activated`, and it re-mints `iat=now`. The guard (`bouncer/jwt.go:144`) therefore accepts the refreshed token, so after `summer admin:reset-password` the SPA's automatic refresh brings the old session back for up to 14 days. Recommendation: fix before closing. Load the principal in `refresh`, reject when `iat < tokens_valid_after` or the user is not activated, expire the cookie, and add a reset-then-refresh 401 regression test. Also correct T-10-05's residual-risk text. If you accept it instead, record an override and a follow-up. -2. **Permission-gated menu in a real browser:** limited admin (Genres only) vs superuser at /plytadmin. -3. **Five-controller walkthrough plus Ustawienia:** list, search, sort, filter, page, bulk delete, open, edit, save, create, 422 feedback; Albums genre and artists relations. -4. **Editor link/unlink round trip** on a real Collection: picker paging, owner excluded, Dodaj (N), confirm unlink, focus trap and Esc. -5. **Visual fidelity:** light and dark, desktop and about 900px, collapsed rail and flyout, against `design/`. -6. **A3:** the Secure cookie is accepted on http://localhost in the target dev browsers. -7. **Prohibitions review:** accept or reject the 17 non-authoritative verdicts above. +Complete. 10-UAT.md has `status: complete` with 7/7 passed. The user ran the fonoteka binary at http://localhost:8080/plytadmin with a superuser and a genres-only admin, and approved: + +1. the CR-01 decision (fixed) +2. navigation for the limited admin versus the superuser +3. the walkthrough of the five controllers and Ustawienia +4. the editor link/unlink round trip +5. the visual check in light and dark at desktop and responsive widths +6. A3, the Secure cookie on localhost +7. the 17 judgment-tier prohibitions + +Each item is recorded as resolved in the `human_verification` frontmatter. ### Gaps Summary -No Phase 10 must-have failed. The backend contract is proven on PostgreSQL for all four success criteria, and I re-ran the acceptance test. The SPA is fully wired to it through the generated, drift-clean types, and its 441 component tests pass. The embedded dist matches a fresh build. The status is `human_needed` rather than `passed` for two reasons. First, the SPA has never been exercised in a real browser against the real server (D-23 excludes browser e2e). Second, CR-01 is an open critical security defect. It does not falsify a Phase 10 truth, but the Phase 10 cookie auto-refresh makes it the default path, and it breaks the Phase 9 password-reset revocation guarantee. It needs an explicit fix-or-accept decision before the phase is marked complete. +None. All four ROADMAP success criteria are verified by automated evidence re-run this session: + +- the assembled Postgres acceptance test +- 441 SPA tests +- the dist and OpenAPI drift gates +- the CR-01 revocation tests + +The approved UAT covers the real-browser behavior that D-23 keeps out of automated tests. CR-01, the reason the previous run stopped at human_needed, is fixed and proven by a test that fails without the fix. The open warnings (WR-01..WR-08) and info items are non-blocking review findings. None of them falsifies a Phase 10 must-have. --- -_Verified: 2026-09-27T18:45:00Z_ +_Verified: 2026-09-27T18:43:00Z_ _Verifier: Claude (gsd-verifier)_