diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md index 6a419b8..2bdcac1 100644 --- a/.planning/WINDOWS.md +++ b/.planning/WINDOWS.md @@ -1,10 +1,10 @@ --- schema_version: 1 -open_count: 2 +open_count: 1 waived_count: 0 -fixed_count: 1 -total_count: 3 -last_updated: 2026-09-27T13:36:36.179Z +fixed_count: 3 +total_count: 4 +last_updated: 2026-09-27T15:01:17.178Z --- # Broken Windows Ledger @@ -16,8 +16,9 @@ last_updated: 2026-09-27T13:36:36.179Z | id | phase | kind | file | line | description | status | reason | recorded_at | resolved_at | |----|-------|------|------|------|-------------|--------|--------|-------------|-------------| | 1 | 09 | deviation | plugins/golem15/fonoteka/routes_cors_test.go | | Admin test secret also set on bootConfigWithHTTP and testConfigCORS so router assembly keeps failing closed | fixed | | 2026-09-24T15:22:55.173Z | 2026-09-24T15:23:23.945Z | -| 2 | 10 | stub | admin/src/app/i18n.ts | | t() returns backend::lang keys until the bundle is served (10-02) and loaded (10-03); SPA copy renders raw keys | open | | 2026-09-27T13:36:35.976Z | | -| 3 | 10 | stub | admin/src/views/ListView.vue | | read-only tracer list without search, sort, paging or row links; full list screens in 10-03 | open | | 2026-09-27T13:36:36.179Z | | +| 2 | 10 | stub | admin/src/app/i18n.ts | | t() returns backend::lang keys until the bundle is served (10-02) and loaded (10-03); SPA copy renders raw keys | fixed | | 2026-09-27T13:36:35.976Z | 2026-09-27T15:01:16.955Z | +| 3 | 10 | stub | admin/src/views/ListView.vue | | read-only tracer list without search, sort, paging or row links; full list screens in 10-03 | fixed | | 2026-09-27T13:36:36.179Z | 2026-09-27T15:01:17.178Z | +| 4 | 10 | stub | admin/src/components/form/registry.ts | | relation-manager is not registered; the Collections editors tab renders the unsupported-field box until the relation manager lands in 10-04 | open | | 2026-09-27T15:01:12.833Z | | ````json [ @@ -41,10 +42,10 @@ last_updated: 2026-09-27T13:36:36.179Z "file": "admin/src/app/i18n.ts", "line": null, "description": "t() returns backend::lang keys until the bundle is served (10-02) and loaded (10-03); SPA copy renders raw keys", - "status": "open", + "status": "fixed", "reason": "", "recorded_at": "2026-09-27T13:36:35.976Z", - "resolved_at": null, + "resolved_at": "2026-09-27T15:01:16.955Z", "milestone": "v1.0" }, { @@ -54,9 +55,22 @@ last_updated: 2026-09-27T13:36:36.179Z "file": "admin/src/views/ListView.vue", "line": null, "description": "read-only tracer list without search, sort, paging or row links; full list screens in 10-03", - "status": "open", + "status": "fixed", "reason": "", "recorded_at": "2026-09-27T13:36:36.179Z", + "resolved_at": "2026-09-27T15:01:17.178Z", + "milestone": "v1.0" + }, + { + "id": 4, + "kind": "stub", + "phase": "10", + "file": "admin/src/components/form/registry.ts", + "line": null, + "description": "relation-manager is not registered; the Collections editors tab renders the unsupported-field box until the relation manager lands in 10-04", + "status": "open", + "reason": "", + "recorded_at": "2026-09-27T15:01:12.833Z", "resolved_at": null, "milestone": "v1.0" } diff --git a/.planning/phases/10-admin-vue-spa/10-03-SUMMARY.md b/.planning/phases/10-admin-vue-spa/10-03-SUMMARY.md new file mode 100644 index 0000000..8ac2f1d --- /dev/null +++ b/.planning/phases/10-admin-vue-spa/10-03-SUMMARY.md @@ -0,0 +1,288 @@ +--- +phase: 10-admin-vue-spa +plan: 03 +subsystem: admin +tags: [vue, spa, forms, lists, filters, settings, i18n, openapi-fetch, reka-ui] + +requires: + - phase: 10-admin-vue-spa + plan: 02 + provides: relation options and saves with labels, backend::lang bundle, CLDR messages, declarative toolbar, filter choices, typed admin OpenAPI +provides: + - backend::lang bundle loading at boot, t/tc/message with Intl.PluralRules and phrasebook-compatible interpolate (D-20, D-24) + - mapWinterUrl for recordUrl and config_form redirects onto the D-10 list, create and record routes + - schema-driven FormView with tabs, 422 mapping, toasts, footer actions, dirty guard and redirect mapping + - D-05 field registry with text, textarea, number, dropdown, switch, checkbox and relation renderers plus the UnsupportedField fallback + - schema-driven ListView with URL state, sort, search, selection, bulk delete, filter bar and pagination + - settings index and settings form through the same renderer (D-21) + - write request bodies (AdminRecord, AdminIDsRequest) and the deepObject filter query in the admin OpenAPI document +affects: [10-04, 10-05] + +actuals: + tokens: 43500 # chars/4 over added lines in summercms.go; excludes boardwalk/dist, admin.json, schema.d.ts and the generated lang.json fixture + tasks: 3 + commits: 3 # MEASURED: git rev-list --count 453b8ae..8f32416 (summercms.go only; fonoteka.go untouched) +plan_head_before: 453b8ae786e558827257c2b9ff287c57f83008f6 +plan_head_after: 8f32416f4ffef00573729da637e2c80403fe48eb + +tech-stack: + added: [] + patterns: + - Every API payload type is an alias of a generated schema type; list query and write bodies are typed through the OpenAPI document + - Field controls share one props interface (FieldControlProps) and are resolved by type through the registry + - Promise-based confirmation (useConfirm) over a Reka AlertDialog + - Global toast queue mounted by AppShell so toasts survive navigation + - RouterView keyed by route path; list query changes keep the view, a new record or controller remounts it + +key-files: + created: + - admin/src/app/winterUrl.ts + - admin/src/app/listQuery.ts + - admin/src/state/useToasts.ts + - admin/src/state/useSettings.ts + - admin/src/views/FormView.vue + - admin/src/views/SettingsIndexView.vue + - admin/src/views/SettingsFormView.vue + - admin/src/components/form/registry.ts + - admin/src/components/form/formState.ts + - admin/src/components/form/FormGrid.vue + - admin/src/components/form/FormField.vue + - admin/src/components/form/FieldRenderer.vue + - admin/src/components/form/FormTabs.vue + - admin/src/components/form/FormErrorBanner.vue + - admin/src/components/form/fields/TextField.vue + - admin/src/components/form/fields/TextareaField.vue + - admin/src/components/form/fields/NumberField.vue + - admin/src/components/form/fields/DropdownField.vue + - admin/src/components/form/fields/SwitchField.vue + - admin/src/components/form/fields/CheckboxField.vue + - admin/src/components/form/fields/RelationField.vue + - admin/src/components/form/fields/UnsupportedField.vue + - admin/src/components/list/ListToolbar.vue + - admin/src/components/list/FilterBar.vue + - admin/src/components/list/Pagination.vue + - admin/src/components/list/CellValue.vue + - admin/src/components/ui/Button.vue + - admin/src/components/ui/Toast.vue + - admin/src/components/ui/ConfirmDialog.vue + - admin/src/components/ui/confirm.ts + - admin/tests/helpers.ts + - admin/tests/fixtures/lang.json + - admin/tests/fixtures/widgets.form-schema.json + - admin/tests/fixtures/widgets.record.json + - admin/tests/fixtures/widgets.options.json + - admin/tests/fixtures/settings.json + - admin/tests/smoke/edit.smoke.test.ts + - admin/tests/smoke/list.smoke.test.ts + - admin/tests/smoke/form.smoke.test.ts + - admin/tests/smoke/settings.smoke.test.ts + modified: + - admin/src/main.ts + - admin/src/App.vue + - admin/src/app/router.ts + - admin/src/app/i18n.ts + - admin/src/api/types.ts + - admin/src/api/schema.d.ts + - admin/openapi/admin.json + - admin/src/views/ListView.vue + - admin/src/views/LoginView.vue + - admin/src/components/list/DataTable.vue + - admin/src/components/shell/AppShell.vue + - admin/src/components/shell/PluginRail.vue + - admin/tests/fixtures/widgets.list-schema.json + - admin/tests/fixtures/widgets.list.json + - admin/tests/smoke/tracer.smoke.test.ts + - cabana/admin_openapi.go + - internal/tools/swagger2openapi/main.go + - phrasebook/backend/lang/pl/lang.yaml + - phrasebook/backend/lang/en/lang.yaml + - boardwalk/dist/** + +key-decisions: + - "The admin OpenAPI document now declares the write bodies (AdminRecord for create, update and settings PUT; AdminIDsRequest for bulk delete and relation link/unlink) and the list filter query as a deepObject of strings, so openapi-fetch sends them typed instead of the SPA declaring shapes" + - "A form field without span fills the whole row (Winter's default span is full); auto and row take the next free slot" + - "The save body holds every context-allowed field that has a value, except read-only fields and types without a renderer, so the relation manager and unknown types are never sent" + - "Winter attributes: pass through an allowlist only (readonly, placeholder, maxlength, minlength, min, max, step, autocomplete, spellcheck, pattern)" + - "Toggle fields keep a numeric stored value numeric (0/1) and send booleans otherwise" + - "aria-sort reflects only the explicit URL sort; the list's defaultSort is applied by the server without a header indicator, so the asc, desc, none cycle stays predictable" + - "A 422 focuses the first invalid field in schema order, preferring the active tab, and switches to its tab when needed" + - "Settings saves toast backend::lang.settings.saved; settings are loaded at boot and after login alongside navigation" + - "The form's Usuń button shows in update mode for every controller; the server enforces the delete permission (T-10-18)" + +patterns-established: + - "New SPA strings go into phrasebook/backend/lang/{pl,en}/lang.yaml; tests/fixtures/lang.json is generated from the pl file" + - "Smoke tests share tests/helpers.ts (mockApi keyed by METHOD path, mountApp at a route with a signed-in admin)" + +requirements-completed: [ADMIN-06] + +coverage: + - id: D1 + description: "Strings bundle loaded before auth, document language, plural selection and phrasebook-compatible interpolation" + requirement: ADMIN-06 + verification: + - kind: unit + ref: "admin/tests/smoke/edit.smoke.test.ts#strings" + status: pass + - kind: unit + ref: "phrasebook/phase10_test.go#TestPhase10SPAKeysResolve" + status: pass + human_judgment: false + - id: D2 + description: "Row opens the record route; typed PUT keyed by field name; saved toast; 422 mapping with focus, banner and clearing; unsupported box; Winter URL mapping" + requirement: ADMIN-06 + verification: + - kind: unit + ref: "admin/tests/smoke/edit.smoke.test.ts" + status: pass + human_judgment: false + - id: D3 + description: "List URL state, sort cycle with aria-sort, debounced search, tri-state selection, confirmed bulk delete and 409 toast, three filter shapes, per-page and pager, empty, empty-search and skeleton states" + requirement: ADMIN-06 + verification: + - kind: unit + ref: "admin/tests/smoke/list.smoke.test.ts" + status: pass + human_judgment: false + - id: D4 + description: "Tabs with error badges, toggle cards, single, multiple and read-only relations, context filtering, delete confirm, dirty guard and beforeunload, create and close redirect mapping" + requirement: ADMIN-06 + verification: + - kind: unit + ref: "admin/tests/smoke/form.smoke.test.ts" + status: pass + human_judgment: false + - id: D5 + description: "Ustawienia rail item visibility, settings index by category, settings save and 422 through the shared renderer" + requirement: ADMIN-06 + verification: + - kind: unit + ref: "admin/tests/smoke/settings.smoke.test.ts" + status: pass + human_judgment: false + - id: D6 + description: "Every field type the five fonoteka controllers serve has a renderer except relation-manager (10-04); committed dist and OpenAPI outputs match fresh builds" + requirement: ADMIN-06 + verification: + - kind: integration + ref: "../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go#TestPhase10Controllers" + status: pass + - kind: other + ref: "scripts/check-admin-dist.sh; scripts/check-admin-openapi.sh --check" + status: pass + human_judgment: false + - id: D7 + description: "Real rendering of Albumy, Artyści, Kolekcje, Gatunki, Style and Ustawienia against the Go backend at /plytadmin (genre dropdown, artist chips, read-only owner, toast, required-name error)" + verification: [] + human_judgment: true + rationale: "The plan's human-check: no browser e2e in Phase 10 (D-23); visual fidelity and real rendering can only be judged in a browser" + +duration: 23min +completed: 2026-09-27 +status: complete +--- + +# Phase 10 Plan 03: Admin lists, forms, filters and settings Summary + +**Any cabana controller now gets a working admin list and form from its server schema. Lists have URL-driven search, sort, filters and paging, plus selection and confirmed bulk delete. Forms have tabs and every built-in field type, including searchable relation selects and chips. They show 422 messages under their fields, toast on save, and follow the Winter redirects mapped onto SPA routes. Settings pages render through the same form renderer.** + +## Performance + +- **Duration:** 23 min +- **Started:** 2026-09-27T14:37:22Z +- **Completed:** 2026-09-27T15:00:38Z +- **Tasks:** 3 +- **Files modified:** 64 in summercms.go (including rebuilt dist assets and generated OpenAPI outputs); fonoteka.go untouched + +## Accomplishments + +- **Strings (D-20, D-24).** `main.ts` loads `GET /lang` first, then `/auth/me`, then navigation and settings. `i18n.ts` stores the bundle and `meta.locale` and sets ``. `t`, `tc` and `message` pick CLDR forms with `Intl.PluralRules` and fall back to `other`. `interpolate` mirrors `phrasebook.interpolate` for `:name`, `:Name` and `:NAME`, replacing longest placeholders first. +- **Routes and URLs (D-10).** New routes `/:vendor/:plugin/:controller/create`, `/:id(\d+)`, `/settings` and `/settings/:code`. `mapWinterUrl` only produces the current controller's list, create or record route. Foreign or unknown shapes fall back to the list (T-10-20). +- **Forms (D-05, D-09, D-18).** `FormView` loads the form schema and the record (`data` plus `meta.labels`) and shows the fields whose context allows the mode. Fields are grouped into tabs, and `FormGrid` lays them out by span. Saving POSTs or PUTs values keyed by field name, including relation ids. A 422 maps `error.details` onto fields (aria-invalid, aria-describedby), shows the plural banner, badges tabs, focuses the first invalid field and clears a field's error when it changes. The sticky footer has Usuń (confirm, DELETE), Anuluj, Zapisz i zamknij (mapped `redirectClose`) and Zapisz (mapped `create.redirect` after a create). A dirty form confirms before any route leave and sets a `beforeunload` guard. +- **Registry (D-05, D-17, D-26).** text, textarea, number, dropdown, switch, checkbox and relation are registered. Any other type, including `relation-manager` for now, renders the dashed `UnsupportedField` box with the type in DM Mono. Relation fields cover three modes. A read-only field shows only its label. A single field is a searchable combobox over `fields/{field}/options`, with `emptyOption` first and muted, a 300 ms debounce, 20 per page and more on scroll or a button. A multiple field shows ordered chips with initials avatars and "Usuń: :name" remove buttons, plus an appending search. +- **Lists (D-12, D-13, D-14, D-22, D-27).** Search (300 ms debounce, resets the page), sort, page, per_page and `filter[]` live in the URL, and any change clears the selection. `DataTable` renders exactly the schema columns: a tri-state page checkbox, a sort cycle with `aria-sort`, selected rows, a sticky header, eight skeleton rows and an empty slot. `CellValue` renders text, datetime and switch pills. The heading shows the plural `recordCount` and the create button. The toolbar holds search and delete, which is disabled without a selection and otherwise confirms with the plural `deleteConfirm` before `POST bulk-delete`. `FilterBar` sends `JSON.stringify` of switch values, `from..to` date ranges and scope values loaded from `filters/{scope}/options`. `Pagination` shows the range, the per-page select (hidden with one option) and a pager with ellipsis. +- **Settings (D-21).** The rail pins Ustawienia to the bottom when `/settings` is non-empty. The index groups pages by category, and each page renders its schema through `FormGrid` and the registry, PUTs its values and maps a 422. +- **Contract.** The admin OpenAPI document now types the write bodies and the list filter query (see Deviations), so the SPA sends typed payloads and declares no API shapes of its own. + +## Task Commits + +1. **Task 1 (tracer): open, edit and save a record with toast and 422 feedback:** `126ca5b` (feat) +2. **Task 2: search, sort, filter, page and bulk-delete any list:** `8b5f856` (feat) +3. **Task 3: tabs, toggles, relation fields, form lifecycle and settings pages:** `8f32416` (feat) + +**Plan metadata:** recorded in the docs commit that adds this file. + +Tracer gate (Task 1): the Task 1 `` (typecheck, edit smoke test, TestPhase10SPAKeysResolve, check-admin-dist) was re-run after its commit and passed before Task 2 started (human_verify_mode end-of-phase, automated verify). + +## Decisions Made + +See `key-decisions` in the frontmatter. The most consequential one is documenting the write bodies and the filter query in the framework OpenAPI document instead of loosening types in the SPA. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] The admin OpenAPI document had no request bodies and no filter parameter** +- **Found during:** Task 1 +- **Issue:** `schema.d.ts` typed create, update, settings PUT and bulk delete with `requestBody?: never`, so openapi-fetch rejected any body. The list query had no `filter` parameter. The plan requires typed PUT and bulk-delete bodies and forbids SPA-declared payload shapes. +- **Fix:** `cabana/admin_openapi.go` declares `@Param body body AdminRecord` on create, update and settings PUT, and a new doc type `AdminIDsRequest {ids: uint64[]}` on bulk delete and relation link/unlink. A `filter` object query parameter is also declared. `internal/tools/swagger2openapi` converts an object query parameter into `style: deepObject, explode: true` with string values. Both generated outputs were regenerated and the drift check passes. Handlers and the conformance test are unchanged, since the conformance test checks responses only. +- **Files:** `cabana/admin_openapi.go`, `internal/tools/swagger2openapi/main.go`, `admin/openapi/admin.json`, `admin/src/api/schema.d.ts` +- **Commit:** `126ca5b` + +**2. [Rule 3] Helper modules not named in the plan's file list** +- `admin/src/components/form/formState.ts` (context filtering, save payload, 422 mapping, focus and tab ids) and `FormErrorBanner.vue` are shared by `FormView` and `SettingsFormView`, as the plan requires ("maps a 422 like the record form"). +- `admin/src/components/ui/confirm.ts` is the promise wrapper both views use around `ConfirmDialog`. +- `admin/tests/helpers.ts` holds the fetch mock and app mount the four new smoke files share. +- **Commits:** `126ca5b`, `8b5f856`, `8f32416` + +**3. [Rule 1] Tracer smoke test adjusted to the new list** +- **Found during:** Task 2 +- **Issue:** The shared list fixture now has a checkbox column (`showCheckboxes: true` renders one), a datetime column and a switch column. The 10-01 tracer assertions compared every `th` and `td`. +- **Fix:** The tracer test skips `[data-select]` cells and compares the first three schema columns of each row. Its intent (schema columns and rows through the typed client) is unchanged. +- **Commit:** `8b5f856` + +**4. [Rule 2 - Security] Winter `attributes:` pass through an allowlist** +- A field's `attributes` map is plugin YAML. Binding it wholesale onto inputs would let a plugin set arbitrary attributes, including `on*` handlers. Only readonly, placeholder, length and range limits, step, autocomplete, spellcheck and pattern are applied. +- **Commit:** `126ca5b` + +**5. [Process] Not a TDD plan** +- The plan's tasks carry no `tdd="true"`, so no RED evidence was recorded. Each task's tests and code were committed together, keeping every commit green (CLAUDE.md). + +**6. [Process] Commits land on master** +- As in 10-01 and 10-02 (`branching_strategy: none`), the orchestrator directed commits onto `master`. + +--- + +**Total deviations:** 4 auto-fixed (2 blocking, 1 bug, 1 security), 2 process notes. +**Impact on plan:** None on scope. The OpenAPI change adds request documentation only, and wire behaviour is unchanged. + +## Issues Encountered + +- The known fonoteka.go `parity` failures (`TestMigrateSeedsCanonicalGenres`, `TestSchemaMatchesPHPSnapshot`) remain, as logged in `deferred-items.md`. Every other package passes `go vet` and `go test`, including the fonoteka plugin modules. `TestPhase10Controllers` passes. +- `gofmt -l internal/build/registry.go` is still listed (pre-existing, deferred). + +## Known Stubs + +- `admin/src/components/form/registry.ts`: `relation-manager` is not registered, so the Collections "Redaktorzy" tab renders the unsupported-field box. The plan requires this: Plan 10-04 adds the relation manager. It is recorded in `.planning/WINDOWS.md` (entry 4). +- WINDOWS entries 2 (untranslated keys until the bundle loads) and 3 (read-only tracer list) are marked fixed by this plan. + +## Threat Flags + +None. No new endpoint, auth path or schema change. The OpenAPI change documents existing request bodies. Plugin strings render as text only (no `v-html`), all requests go through the typed client, and Winter URLs are mapped, never followed verbatim. + +## User Setup Required + +None. + +## Next Phase Readiness + +- Plan 10-04 can register `relation-manager` in `registry.ts`, which removes the fallback on the Collections editors tab. It can reuse `DataTable` (selection, sorting and states), `ConfirmDialog` with `useConfirm`, `Button`, the toasts and the typed `AdminIDsRequest` body for link and unlink. `RelationSchema.messages` is already typed. +- Plan 10-05's unit tests can target `i18n.ts`, `winterUrl.ts`, `listQuery.ts`, `formState.ts` and `registry.ts` directly. The smoke tests already cover most component behaviour. +- The human browser check (five controllers and Ustawienia at /plytadmin) is left for phase verification. + +--- +*Phase: 10-admin-vue-spa* +*Completed: 2026-09-27* + +## Self-Check: PASSED + +All created files listed above exist; commits 126ca5b, 8b5f856 and 8f32416 (summercms.go) are present. Plan verification re-run: typecheck, all 51 smoke tests, go test ./phrasebook, check-admin-dist.sh, check-admin-openapi.sh --check, go vet ./... and go test ./... pass; TestPhase10Controllers passes in fonoteka.go.