Files
summercms/.planning/phases/10-admin-vue-spa/10-02-SUMMARY.md
2026-09-27 16:30:15 +02:00

20 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, app_plan_head_before, app_plan_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before plan_head_after app_plan_head_before app_plan_head_after tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
10-admin-vue-spa 02 admin
cabana
phrasebook
relations
openapi
messages
toolbar
filters
conformance
phase plan provides
10-admin-vue-spa 01 backend.uri prefix, cookie + CSRF transport, typed envelopes, admin OpenAPI pipeline, fonoteka lang catalog
relation field options endpoint and relation values in saves, with labels (D-17, D-18) and the read-only protected-key relation (D-26)
backend::lang framework strings (pl, en), the HasLangOverrides layer and the public GET /lang bundle (D-20)
messages blocks on list, form and relation configs, served as CLDR forms (D-13, D-24)
declarative toolbar.buttons list (D-14)
model-backed filter choices endpoint (D-27)
a fully typed admin OpenAPI document proven against real handler output (D-15, D-16)
fonoteka contracts, YAML copy and toolbars for the five controllers (D-08)
10-03
10-04
10-05
tokens tasks commits
48677 3 3
e9b48d4720 9f296b0484 bb4cd7a17584fe52f43c9884ec555e319431c12f cb6727f3c284265d6254c42e8c00db3f7ab4655e
added patterns
Controller-declared FieldRelationContract (belongsTo foreign key or belongsToMany pivot) validated at activation; framework code never names a table or key
One scoped query (RelationExtendOptionsQuery) serves options and revalidates submitted ids inside the save transaction
Messages cached as phrase keys and resolved per request into CLDR form maps; framework defaults fill omitted keys
One six-segment GET pattern dispatches logical routes that ServeMux cannot register side by side
External conformance test drives every inventoried route through surf.Assemble and decodes into the documented type with DisallowUnknownFields
created modified
cabana/relation_field.go
cabana/relation_field_test.go
cabana/messages.go
cabana/messages_test.go
cabana/lang.go
cabana/filter_options_test.go
cabana/openapi_conformance_test.go
cabana/export_test.go
phrasebook/backend/lang/pl/lang.yaml
phrasebook/backend/lang/en/lang.yaml
phrasebook/phase10_test.go
../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_relations_test.go
../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_copy_test.go
../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go
pact/capabilities.go
phrasebook/lang.go
phrasebook/loader.go
phrasebook/translator.go
cabana/crud.go
cabana/http.go
cabana/registry.go
cabana/contracts.go
cabana/relation.go
cabana/form_schema.go
cabana/list_schema.go
cabana/filter_schema.go
cabana/schema_types.go
cabana/admin_openapi.go
cabana/auth.go
internal/build/stubs/artifacts.tmpl
admin/openapi/admin.json
admin/src/api/schema.d.ts
admin/src/api/types.ts
admin/src/styles/main.css
../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/collections_admin_controller.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/*/config_list.yaml
../fonoteka.go/plugins/golem15/fonoteka/controllers/*/config_form.yaml
../fonoteka.go/plugins/golem15/fonoteka/controllers/collections/config_relation.yaml
../fonoteka.go/plugins/golem15/fonoteka/lang/{pl,en}/lang.yaml
Relation lists, field options and filter options share one mounted pattern GET /{vendor}/{plugin}/{controller}/{id}/{segment}/{name} dispatched by service.nestedGet, because ServeMux rejects fields/{field}/options next to {id}/relations/{name}; the documented D-17/D-27 paths are unchanged
Relation ids are parsed in submitted order with duplicate detection (the asUint coercion normalizeIDs uses), since normalizeIDs sorts and dedups; only JSON integers are accepted
Record responses put relation values in data (belongsTo id or null, belongsToMany ids in pivot order) and labels in meta.labels; the read-only Collections owner appears as data.owner with its email label, owner_id stays hidden
Show and save labels read the related model without the options hook, so a stored row outside the current scope still shows its label
Message key validation at activation checks declared keys and framework defaults; defaults derived from toolbar.search.prompt, noRecordsMessage or the form name may be literal Winter text and are not checked
Cached list and relation schemas carry each message key as its own form; responses resolve them in the request locale; form create defaults to config_form name, list searchPrompt and empty default to the list's prompt and noRecordsMessage
GET /lang resolves meta.locale to the first locale of the fallback chain the catalog has backend strings for (de falls back to en)
Filter options: {scope} in the path is the filter name (the filter[<name>] key); the model's FilterOptions receives the filter's scope method name
Create is documented as 201 RecordEnvelope, matching the handler; every protected route documents 401, 403 and 404, writes 422, bulk delete 409
Tailwind skips the generated schema.d.ts and openapi/ so admin API changes do not churn boardwalk/dist; the committed dist is unchanged
A route inventory entry may name its mounted pattern (adminRoute.mounted) when several logical routes share one ServeMux pattern
Every admin route needs a TestPhase10OpenAPIConformance case; a new route without one fails the test
ADMIN-06
id description requirement verification human_judgment
D1 Relation options: scoped by the hook, case-insensitive escaped search, label then id order, 20/100 paging, numeric values, 403 before SQL, 404 for non-relation and read-only fields ADMIN-06
kind ref status
integration cabana/relation_field_test.go#TestPhase10RelationOptions pass
kind ref status
unit cabana/relation_field_test.go#TestPhase10NestedGetDispatch pass
false
id description requirement verification human_judgment
D2 Relation saves: belongsTo FK, ordered pivot replace with order column, absent keys untouched, null clears, labels in meta; forged, out-of-scope, non-integer and duplicate ids are 422 and roll back everything ADMIN-06
kind ref status
integration cabana/relation_field_test.go#TestPhase10RelationSave pass
kind ref status
integration cabana/relation_field_test.go#TestPhase10RelationForgedID pass
kind ref status
unit cabana/relation_field_test.go#TestPhase10RelationBoot pass
false
id description requirement verification human_judgment
D3 fonoteka albums genre/artists and the read-only collections owner through /plytadmin with the cookie ADMIN-06
kind ref status
integration ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_relations_test.go#TestPhase10AlbumRelations pass
kind ref status
integration ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_relations_test.go#TestPhase10CollectionOwnerReadOnly pass
false
id description requirement verification human_judgment
D4 backend::lang strings, CLDR form conversion, override layer, public bundle, SPA keys resolve in pl and en ADMIN-06
kind ref status
unit phrasebook/phase10_test.go#TestPhase10Forms pass
kind ref status
unit phrasebook/phase10_test.go#TestPhase10LangOverride pass
kind ref status
unit phrasebook/phase10_test.go#TestPhase10SPAKeysResolve pass
kind ref status
unit cabana/messages_test.go#TestPhase10Bundle pass
false
id description requirement verification human_judgment
D5 messages blocks with defaults and plural forms, boot failures on unknown or missing keys; declarative toolbar with boot errors ADMIN-06
kind ref status
unit cabana/messages_test.go#TestPhase10Messages pass
kind ref status
unit cabana/messages_test.go#TestPhase10Toolbar pass
kind ref status
integration ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_copy_test.go#TestPhase10ControllerCopy pass
kind ref status
unit ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_auth_test.go#TestPhase10LangCatalog pass
false
id description requirement verification human_judgment
D6 Model-backed filter choices behind the controller permission; activation fails without FilterOptions ADMIN-06
kind ref status
unit cabana/filter_options_test.go#TestPhase10FilterOptions pass
false
id description requirement verification human_judgment
D7 Every admin route is typed in admin/openapi/admin.json and its real response decodes into the documented type ADMIN-06
kind ref status
integration cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance pass
kind ref status
other scripts/check-admin-openapi.sh --check pass
kind ref status
other npm --prefix admin run typecheck; scripts/check-admin-dist.sh pass
false
id description requirement verification human_judgment
D8 The five fonoteka controllers serve exactly their tracked YAML with built-in field types; created records read back with the same keys ADMIN-06
kind ref status
integration ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go#TestPhase10Controllers pass
false
50min 2026-09-27 complete

Phase 10 Plan 02: Admin backend contract Summary

The cabana admin API now serves everything the SPA screens need: relation choices and relation saves with labels (read-only protected owner), framework backend::lang strings with an override layer and a public bundle, per-controller messages as CLDR plural forms, a declarative [create, delete] toolbar, model-backed filter choices, and an OpenAPI document whose every route is proven against the real handler output.

Performance

  • Duration: 50 min
  • Started: 2026-09-27T13:38:29Z
  • Completed: 2026-09-27T14:28:07Z
  • Tasks: 3
  • Files modified: 36 in summercms.go, 27 in fonoteka.go

Accomplishments

  • Relations (D-17, D-18, D-26). A controller declares AdminFieldRelations() with a belongsTo foreign key or a belongsToMany pivot (optional order column, label column). Activation rejects a relation field without a contract, a missing column, an unknown kind, duplicates and orphans, naming plugin, controller and field. GET .../fields/{field}/options pages {value, label} choices through the controller's RelationExtendOptionsQuery. On save, present relation keys are revalidated through that same scoped query after the Before hook (422 and a full rollback otherwise). belongsTo sets the key before the row write, and belongsToMany deletes and bulk-inserts pivot rows in submitted order. Show, create and update return the values in data and the labels in meta.labels. A belongsTo on a protected fill key (Collections owner_id) is served readOnly, has no options endpoint and is never written.
  • Strings (D-20). phrasebook/backend/lang/{pl,en}/lang.yaml carries the framework admin copy, including every key the SPA already uses, with CLDR plural maps. It loads as namespace backend. pact.HasLangOverrides trees can replace keys and add locales. Activation fails when a backend:: key cannot convert to plural forms. The public GET /lang returns only backend::lang.* for the request locale over the fallback, with Cache-Control: no-cache.
  • Copy and toolbar (D-13, D-14, D-24). messages: blocks on list, form and relation configs are decoded strictly. Omitted keys take framework defaults, every response carries a complete messages object of CLDR form maps, and activation fails on a missing phrase key. toolbar.buttons is an ordered [create, delete] list. The Winter string form, duplicates, unknown actions and delete without checkboxes fail at boot, and create is dropped when there is no form. The form schema also serves the raw Winter redirects.
  • Filters (D-27). A scope filter's model must implement pact.FilterOptions. GET .../filters/{scope}/options serves localized choices behind the controller permission.
  • Typed API (D-15, D-16). Every route documents a concrete success schema and its error responses, and SuccessEnvelope is gone. TestPhase10OpenAPIConformance drives all 25 inventoried routes through the assembled router on PostgreSQL and decodes each body with unknown fields disallowed. It fails when a route has no case.
  • fonoteka. The albums controller has genre and artists contracts, and the collections controller has a read-only owner. All five controllers declare [create, delete] and messages, and the editors relation has its own copy. The new keys come with Polish plurals. Assembled tests cover relations, the read-only owner, Polish copy and the YAML-exact schemas.

Task Commits

  1. Task 1 (tracer): relation options and saves with labels - fe04dbc (feat, summercms.go), 2914892 (feat, fonoteka.go). Preceded by 84dbda1 (test, fonoteka.go), which repairs pre-existing Phase 9 test failures (see Deviations).
  2. Task 2: backend strings, messages, declarative toolbar - c87148a (feat, summercms.go), 648bff6 (feat, fonoteka.go)
  3. Task 3: filter choices, full typing, conformance - 9f296b0 (feat, summercms.go), cb6727f (test, fonoteka.go)

Plan metadata: recorded in the docs commit that adds this file.

Tracer gate (Task 1): the Task 1 <verify> was re-run end to end after its commits and passed (human_verify_mode end-of-phase, automated verify), then Task 2 started.

Decisions Made

See key-decisions in the frontmatter. The most consequential one is the shared six-segment GET pattern. It keeps the D-17 and D-27 paths exactly as decided without touching the Phase 9 relation route.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] ServeMux conflict between the new options routes and the relation list route

  • Found during: Task 1
  • Issue: GET {api}/{vendor}/{plugin}/{controller}/fields/{field}/options and the Phase 9 GET .../{id}/relations/{name} both match paths like .../fields/relations/options, and neither is more specific. Go's ServeMux panics at registration (verified with a probe). The same applies to filters/{scope}/options.
  • Fix: one mounted pattern GET .../{id}/{segment}/{name} with identifier constraints, dispatched by service.nestedGet on the literal segments. A numeric id never equals fields or filters, and anything else is the JSON not_found envelope. The route inventories in both repos gained mounted keys, and the OpenAPI document keeps the three logical paths.
  • Files: cabana/http.go, cabana/security_coverage_test.go, ../fonoteka.go/.../admin_phase09_security_test.go
  • Commits: fe04dbc, 2914892, 9f296b0

2. [Rule 3 - Blocking] Twelve fonoteka plugin tests were already failing before this plan

  • Found during: Task 1 (the plan's verify runs TestAlbumsAdmin.* and TestCollectionsAdmin.*)
  • Issue: On untouched HEAD copies of both repos, go test in plugins/golem15/fonoteka failed 12 tests. Form and list tests pinned raw phrase keys and identical pl/en bodies, which the 10-01 lang catalog now translates. The /me route-isolation check also flagged the backend admin /plytadmin/api/v1/auth/me. The fonoteka root go test ./... does not include the plugin modules, so 10-01 did not see these failures.
  • Fix: those tests now pin the resolved en labels plus a localized pl body, and the isolation check allows the admin profile route. The tracer genre list searches for its own row, because genres created by other tests can push it past page 1.
  • Commits: 84dbda1 (artists, genres, styles, tracer, oauth tools); album and collection pins are in 2914892
  • Result: every fonoteka plugin module package now passes.

3. [Rule 2 - Correctness] jsonScalar and fieldContext had no JSON decoders

  • Found during: Task 3 (conformance test)
  • Fix: UnmarshalJSON for both, accepting exactly their served shapes, so a FormView decodes back into its documented type.
  • Commit: 9f296b0

4. [Rule 3 - Blocking] boardwalk/dist drifted on API-only changes

  • Found during: Task 3 (check-admin-dist.sh)
  • Issue: Tailwind v4 scanned schema.d.ts and openapi/admin.json, so new description prose produced new utility classes.
  • Fix: @source not for both in admin/src/styles/main.css. The fresh build equals the committed dist again, so no dist change was needed.
  • Commit: 9f296b0

5. [Rule 3] Phase 9 contract test accepted only 200

  • Fix: TestPhase09ContractInventory accepts 200 or 201 (create is now documented as 201, matching the handler) and requires 401 only for login and refresh among public routes (/lang is public and never 401).
  • Commits: fe04dbc, c87148a

6. [Process] TDD RED evidence

  • RED runs were recorded and verified RED_EVIDENCE_OK by gsd-tools check tdd-red-evidence for:
    • TestPhase10AlbumRelations (with TestPhase10CollectionOwnerReadOnly)
    • TestPhase10RelationSave (with RelationOptions, ForgedID and Boot in the same run)
    • TestPhase10Forms (with LangOverride and SPAKeysResolve)
  • The cabana Messages, Toolbar, Bundle, FilterOptions and OpenAPIConformance tests and the fonoteka ControllerCopy and Controllers tests were written with or right after their implementation, so no failing run was captured for them.
  • As in 10-01, tests and code were committed together per repository so that every commit stays green (CLAUDE.md), so there are no separate test(...) RED commits.

7. [Acceptance note] grep -rniE 'golem15|album' cabana/relation_field.go

  • This prints one line: the framework's own import path git.golem15.com/golem15/summercms/pact. No plugin table, key, pivot or domain name appears in the file.

Total deviations: 5 auto-fixed (4 blocking, 1 correctness), 2 process/acceptance notes. Impact on plan: None on scope. The mounted route table differs from the documented one by one shared pattern.

Issues Encountered

  • The known fonoteka.go parity failures (TestMigrateSeedsCanonicalGenres, TestSchemaMatchesPHPSnapshot) remain, as logged in deferred-items.md. Everything else passes go vet and go test in both repositories, including every fonoteka plugin module.
  • gofmt -l internal/build/registry.go is still pre-existing (deferred).

Known Stubs

  • admin/src/app/i18n.ts: t() still returns keys because the SPA does not load the bundle yet. The server now serves it at GET {prefix}/api/v1/lang, and Plan 10-03 wires setBundle at startup. This was intentional per D-28.

User Setup Required

None.

Next Phase Readiness

  • Plan 10-03 can render lists, forms, filters and settings entirely from typed schemas. It should use:
    • ListSchema.messages, toolbarButtons and filters with /filters/{scope}/options
    • FormView.messages and redirects, plus FormField.multiple and readOnly
    • /fields/{field}/options for relation inputs
    • RecordEnvelope (data plus meta.labels) for show and save
    • GET /lang for the UI strings
  • Plan 10-04 has RelationSchema.messages for the editors manager.
  • Any new admin route needs an inventory entry and a TestPhase10OpenAPIConformance case.

Phase: 10-admin-vue-spa Completed: 2026-09-27

Self-Check: PASSED

All created files listed above exist; commits fe04dbc, c87148a and 9f296b0 (summercms.go) and 84dbda1, 2914892, 648bff6 and cb6727f (fonoteka.go) are present.