Files
summercms/.planning/phases/10-admin-vue-spa/10-CONTEXT.md
2026-09-27 13:26:11 +02:00

18 KiB

Phase 10: Admin Vue SPA - Context

Gathered: 2026-09-26 Status: Ready for planning (after Phase 9 plans 09-11 and 09-12 complete)

## Phase Boundary

Phase 10 delivers the framework's admin SPA: a Vue 3 + TypeScript single-page app, living in summercms.go, embedded in the binary and served at a configurable backend URI. It renders login, permission-gated plugin navigation, schema-driven lists and forms, the relation manager, the settings screen and the filter bar, consuming the Phase 9 cabana admin API. The five Płytarium controllers (Albums, Artists, Collections, Genres, Styles) are the acceptance targets; the Collections editors relation manager must search, link and unlink.

The phase also carries the backend changes the SPA needs, all in cabana (framework) plus YAML/registry updates in fonoteka.go: a configurable admin prefix for both SPA and API, cookie transport for the admin JWT, a relation-field options endpoint, relation values in form saves, messages: and declarative toolbar.buttons in controller config, a server-served SPA string bundle, and a framework-owned admin OpenAPI document the SPA's types are generated from.

Requirement: ADMIN-06. Not in scope: runtime plugin extensions (plugin JS/CSS assets, custom widget and partial field types), which are Phase 10.1; browser e2e tests.

## Implementation Decisions

Packaging and serving

  • D-01: The SPA source lives in summercms.go/admin (Vite project). Its built dist/ is embedded through embed.FS in a framework package and served by the binary. No separate deploy, no CORS.
  • D-02: The mount path is configurable (backend.uri, Winter-style), e.g. /plytadmin, /manage, /horoadmin, chosen per project so the admin location is not guessable. The committed build is path-agnostic: the Go server injects the configured base path into index.html at serve time. Client-side routes are handled by an SPA fallback under the prefix. — Reversibility: reversible
  • D-03: The admin API moves with the prefix: {backend.uri}/api/v1/... replaces the hardcoded /_admin/api/v1 from Phase 9 (D-09 there). This is safe because nothing outside the SPA calls the admin API yet (Nuxt and fonoteka-mcp never touch it). The api segment under the prefix is reserved and never used as an SPA route. OpenAPI paths are written relative (/navigation, /auth/login) and the SPA client gets its base URL at runtime. — Reversibility: costly — every admin route, test and the OpenAPI document are keyed to the prefix scheme.
  • D-04: The built dist/ is committed, with a check script that rebuilds and fails on drift (same pattern as check-openapi.sh). Rationale agreed with the user: everything an app plugin changes in the admin (YAML columns/fields, navigation, permissions, hooks, and in 10.1 partials and JS/CSS assets) is embedded in the plugin and needs only a Go binary rebuild; the Node build is needed only when the framework SPA itself changes (new built-in field type, layout). App developers never need Node.
  • D-05: Extension seam only: a field-type renderer registry (FieldRenderer maps type to component). Any unregistered type renders the design's UnsupportedField box ("Nieobsługiwany typ pola: <type>") instead of breaking the form. The full extension point is Phase 10.1 (see Deferred).

Look and components

  • D-06: The design reference is Direction C v2 from claude.ai/design, stored at .planning/phases/10-admin-vue-spa/design/ (README.md is the spec; Direction C v2.dc.html + support.js is the viewable reference). High fidelity: tokens, typography, radii, spacing, control heights, focus rings, copy, a11y roles, states (selected, empty, loading, 422, toast, modal), responsive rules (collapse below ~1100px, tablet width), light and dark mode. Build the generic components it lists (PluginRail, SectionPanel, SectionFlyout, AppShell, DataTable, ListToolbar, Pagination, FormTabs, FormGrid, FormField, FieldRenderer, RelationManager, RelationPickerModal, Toast, UserMenu).
  • D-07: Stack: Vue 3 + Vite + TypeScript + Tailwind + Reka UI primitives + lucide-vue-next icons (same family as vue-fonoteka-app). DM Sans and DM Mono are self-hosted inside dist/, not loaded from Google Fonts (works offline, no third-party requests from a hidden admin URL).
  • D-08: Real YAML only. Płytarium screens render exactly what their YAML declares (Albums: name, artists, format, genre, shelf; no tabs). The mock's extra fields, tabs and columns (Opis, Wypożyczony, Ulubiony, Utworzono, Szczegóły tab) are component fixtures for tests, not additions to Płytarium.
  • D-09: The SPA follows Phase 9's API contract where the mock differs: error envelope {"error": {code, message, details}} with 422 details as field → messages (not the mock's {errors}); list query and meta per Phase 9 D-11.
  • D-10: Routes derive from controller IDs: golem15.fonoteka.albums → {backend.uri}/golem15/fonoteka/albums, /create, /:id. The rail groups by plugin from /navigation; the active rail item comes from the route's owning plugin. Items the admin lacks permission for are absent (server-filtered), and a plugin whose side menu is empty after filtering is absent from the rail.
  • D-11: Navigation icons are lucide names in the registry. The SPA also carries a small map from Winter icon-* names to lucide so ported Winter plugins work unchanged; unknown names get a neutral fallback icon. fonoteka.go switches its navigation and settings icons to lucide names.
  • D-12: The Albums format column renders as plain text in Phase 10 (no icon pills).

Controller copy and toolbar

  • D-13: config_list.yaml and config_form.yaml accept an optional messages: block of phrasebook keys (e.g. recordCount, searchPrompt, deleteConfirm, empty on lists; create, saved on forms). Keys are resolved server-side in the admin's locale. Plural keys are served with all their CLDR forms ({"one": ..., "few": ..., "many": ..., "other": ...}) and the SPA picks the form with Intl.PluralRules(locale), because only the client knows the count (e.g. selected rows). Placeholders {count}, {name}, {term} are interpolated by the SPA. Every key is optional; the framework ships generic defaults (e.g. "Nowy rekord", "Usunąć zaznaczone ({count})?", "Zapisano"). Unknown messages keys fail at boot (Phase 9 DisallowUnknownField rule).
  • D-14: Toolbar is declarative: toolbar.buttons: [create, delete] lists built-in actions in display order (create gated by create permission; delete is bulk delete, enabled with a selection, confirming with deleteConfirm), plus toolbar.search. A string value (Winter's buttons: list_toolbar partial) is a boot error pointing at the new syntax. The five Płytarium config files are updated accordingly. Custom actions are Phase 10.1. — Reversibility: costly — changes the YAML contract every ported controller writes.

Types and relations

  • D-15: The SPA's types come from a framework-owned admin OpenAPI document: swag v1 annotations on cabana handlers, generated in summercms.go, converted to OpenAPI 3 (reuse the swagger2openapi.go approach), committed, and fed to openapi-typescript, with a drift check. The framework SPA never reads an app repo's document. fonoteka.go/docs/openapi.json remains the parity API document.
  • D-16: Records are typed generically: generated types cover envelopes, form/list/filter/relation/navigation/settings schemas, list meta and errors; a record is Record<string, unknown> read through its field schema. No per-controller TypeScript types and no hand-maintained duplicates of API shapes (success criterion 4).
  • D-17: Relation fields get their choices from a new endpoint, GET {prefix}/api/v1/{vendor}/{plugin}/{controller}/fields/{field}/options?search=&page=&per_page=, returning {value, label} with the label from nameFrom, on the Phase 9 D-11 list contract. An optional RelationExtendOptionsQuery(ctx, field, *gorm.DB) *gorm.DB hook on the admin controller lets apps scope options (e.g. to the active collection). Permission gating is the controller's. The emptyOption stays a schema property rendered by the SPA.
  • D-18: Form saves send relation values keyed by the YAML field name with ids: {"genre": 3, "artists": [4, 9]}. cabana maps belongs-to to the foreign key and syncs many-to-many through the Phase 5 join-table contract inside the save transaction, after FormBeforeCreate/FormBeforeUpdate. Record GET returns the same shape plus display labels so the form can show chips and the selected dropdown value without extra calls.

Session and UX

  • D-19: The admin JWT moves into an httpOnly, Secure, SameSite=Strict cookie scoped to {backend.uri}, set by login and refresh and cleared by logout. JS never reads the token (this matters once 10.1 lets plugin JS run in the admin origin). The backend guard accepts the cookie and still accepts Authorization: Bearer for CLI and tests. State-changing requests must carry a custom header (e.g. X-Requested-With) as CSRF defence in depth. It is still a JWT with the Phase 7 blacklist and sliding refresh; no session store. — Reversibility: costly — changes Phase 9's auth transport and the security matrix tests that pin it.
  • D-20: The SPA's own UI strings (Zapisz, Anuluj, Wyloguj, pagination, confirmations, defaults from D-13) live in the framework's phrasebook (backend::lang, pl and en) and are fetched as a resolved bundle for the admin's locale at startup. Projects can override or add locales without a Node rebuild.
  • D-21: The settings screen is in scope: the rail's "Ustawienia" lists HasSettings pages the admin may manage and renders each through the same form renderer against the Phase 9 settings endpoints (fonoteka: search_use_typesense).
  • D-22: The filter bar is in scope: it renders config_filter.yaml scopes of the three Phase 9 shapes (switch, daterange, model-backed) and drives filter[<scope>]. Płytarium has no filters, so it is verified with fixtures.
  • D-23: Testing follows the lean rules: Vitest component and unit tests as the phase's last plan; Go tests for every cabana change. Browser e2e (Playwright) is not part of Phase 10.

Plan-time resolutions (2026-09-27, after research)

  • D-24: messages placeholders use phrasebook/Winter syntax :count, :name, :term (with :Name/:NAME casing variants); D-13's braces were notation only. The SPA's interpolation mirrors phrasebook.interpolate so server and client behave identically and Winter strings port verbatim.
  • D-25: fonoteka.go adds a Collections side-menu item (collections, lucide library, permission golem15.fonoteka.access_collections) as a documented deviation from the PHP navigation; navigation is not part of the API parity contract.
  • D-26: The Collections owner relation field is read-only in Phase 10: relation fields whose foreign key is a protected fill key are rendered as a read-only label; protectedFillKey and FormBeforeCreate are not widened.
  • D-27: Model-backed filter scopes (D-22) get their choices from an optional model capability FilterOptions(scope string) []pact.Option, served at GET {prefix}/api/v1/{vendor}/{plugin}/{controller}/filters/{scope}/options, verified with fixtures only.
  • D-28: Phase 10 is planned as 5 plans: 01 tracer (prefix, cookie+CSRF, embed/serve, admin OpenAPI pipeline, SPA scaffold with login + nav shell + read-only Genres list, fonoteka lang/icons), 02 backend contract (backend lang + string bundle, messages, toolbar, relation options + save, filter options, typed OpenAPI, fonoteka YAML), 03 SPA lists/forms/filters/settings for all five controllers, 04 relation manager + shell polish, 05 unit tests (Vitest + Go) and check-phase10.sh.

Claude's Discretion

  • SPA state management (Pinia or composables), router setup, HTTP client (openapi-fetch or a thin wrapper over generated types).
  • Dev loop: Vite dev server proxying the API vs rebuilding into the embedded path; recommended a Vite proxy to a running summer serve for development.
  • Exact messages key vocabulary beyond the examples, and the generic default wording.
  • Refresh timing (proactive before expiry plus one retry on 401, then redirect to login with the return URL).
  • Per-page options (design says 12/24/48/96; reconcile with recordsPerPage and the Phase 9 cap).
  • Unsaved-changes confirmation, create-then-redirect behaviour (use config_form create.redirect/redirectClose semantics mapped to SPA routes), toast queue.
  • Whether admin paths are removed from fonoteka.go/docs/openapi.json once the framework document exists (recommended: yes, one owner per path).
  • Package layout inside summercms.go for the embed/serve package, following beach-themed naming.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Design

  • .planning/phases/10-admin-vue-spa/design/README.md — the full UI spec: tokens, screens, states, interactions, component list.
  • .planning/phases/10-admin-vue-spa/design/Direction C v2.dc.html (+ support.js) — viewable reference; props switch screens and states.

Roadmap and requirements

  • .planning/ROADMAP.md §Phase 10 — goal and success criteria.
  • .planning/REQUIREMENTS.md — ADMIN-06.
  • .planning/PROJECT.md — two-repo split (framework holds the admin SPA shell), stack constraints.
  • .planning/research/STACK.md — swag v1, openapi-typescript, goccy/go-yaml.

Phase 9 contract this phase consumes and amends

  • .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md — D-02 guard, D-06 typed schema, D-07 server-side labels, D-09/D-10/D-11 API paths, envelope and list contract, D-12 filters, D-13 hooks, D-15/D-16 relation manager, D-17 settings, D-18 navigation.
  • .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-11-PLAN.md, 09-12-PLAN.md and their SUMMARYs once written — final state of navigation/settings endpoints, security matrix and OpenAPI generation.
  • cabana/http.go — route mounting (hardcoded /_admin/api/v1 changed by D-03).
  • cabana/auth.go — login/refresh/logout/me (transport changed by D-19).
  • cabana/form_schema.go, cabana/list_schema.go, cabana/filter_schema.go, cabana/relation.go, cabana/settings.go, cabana/navigation.go, cabana/schema_types.go — the JSON the SPA renders.

OpenAPI tooling

  • ../fonoteka.go/scripts/check-openapi.sh and ../fonoteka.go/scripts/swagger2openapi.go — the generation/conversion/validation pattern to replicate for the framework document.

Płytarium config the SPA renders

  • ../fonoteka.go/plugins/golem15/fonoteka/controllers/{albums,artists,collections,genres,styles}/ — config_form/config_list (updated by D-13/D-14).
  • ../fonoteka.go/plugins/golem15/fonoteka/admin_navigation.go, admin_settings.go — icons switched to lucide (D-11).
  • /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/album/fields.yaml, columns.yaml — PHP originals (real field set, D-08).

UI stack reference

  • /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/package.json — the Vue/Tailwind/Reka/lucide versions the team already uses.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • cabana (~10k lines): complete admin API: auth, navigation, list/form/filter/relation schemas, CRUD, bulk delete, relation linked/candidates/link/unlink, settings. The SPA is its first client.
  • bouncer.MintAudience / RefreshAudience / blacklist: stay the token mechanism under cookie transport (D-19).
  • phrasebook: resolves labels and the new messages and SPA string bundle (D-13, D-20); CLDR plural data matches Intl.PluralRules.
  • pact.DropdownOptionsProvider and model-method options: pattern for the new relation options hook (D-17).
  • Phase 5 join-table contract: many-to-many sync on save (D-18).
  • check-openapi.sh pipeline (swag v1.16.6 → swagger2openapi → openapi-typescript 7.13.0).

Established Patterns

  • Optional capability interfaces type-asserted by the consumer (RelationExtendOptionsQuery follows Phase 9 D-13).
  • Fail-loud boot on malformed plugin YAML (DisallowUnknownField): applies to messages and toolbar.buttons.
  • Committed generated artifact + drift check (OpenAPI doc; now also dist/ and the admin OpenAPI doc).
  • Framework stays app-agnostic: no Płytarium names in summercms.go, including SPA code.

Integration Points

  • cabana mount: prefix from config (D-03), SPA static serving + index.html base injection + history fallback (D-01, D-02), same prefix.
  • backend guard in the bouncer registry: cookie extraction + CSRF header check (D-19).
  • /auth/me already returns name/email; the user menu needs the role name too.
  • fonoteka.go: YAML toolbar/messages updates, lucide icons, optional RelationExtendOptionsQuery on Albums (scope to active collection, consistent with Phase 9 D-14).

</code_context>

## Specific Ideas
  • The user wants the admin URL changeable per project for security ("/plytadmin", "/manage", "/horoadmin"), as in WinterCMS.
  • The user asked for "a best solution" to per-controller copy instead of Winter's _list_toolbar.htm partial: declarative actions plus translatable messages with proper Polish plurals.
  • The design was produced in claude.ai/design from a prompt listing three directions; the user chose Direction C v2 ("Generic modern dashboard", navy + sunny yellow) and treats it as the target.
  • The shell is multi-plugin by design (rail per registered main-menu item); Płytarium is one plugin among future ones (Użytkownicy, Media in the mock).
## Deferred Ideas
  • Phase 10.1: runtime admin extension point. AdminAssets() on controllers (Winter addJs/addCss), served under the admin prefix and loaded when the controller opens; type: widget rendered as custom elements (plugins ship plain JS, no Vue coupling); type: partial rendered server-side via html/template; custom toolbar actions; a dev-mode switch serving plugin assets from disk. Needs a roadmap entry.
  • Ctrl+K command palette across all plugins' side menus.
  • Generic badge/icon column type (Albums format pills with disc/cassette icons).
  • Playwright browser e2e for the admin SPA (joins the Phase 8 carried-forward Playwright follow-up).
  • Admin navigation for the user and media plugins (shown in the mock) arrives with those ports.

Phase: 10-admin-vue-spa Context gathered: 2026-09-26