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 BoundaryPhase 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 DecisionsPackaging and serving
- D-01: The SPA source lives in
summercms.go/admin(Vite project). Its builtdist/is embedded throughembed.FSin 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 intoindex.htmlat 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/v1from 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). Theapisegment 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 ascheck-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 (
FieldRenderermapstypeto component). Any unregistered type renders the design'sUnsupportedFieldbox ("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.jsis 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 insidedist/, 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.goswitches 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.yamlandconfig_form.yamlaccept an optionalmessages:block of phrasebook keys (e.g.recordCount,searchPrompt,deleteConfirm,emptyon lists;create,savedon 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 withIntl.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"). Unknownmessageskeys fail at boot (Phase 9DisallowUnknownFieldrule). - D-14: Toolbar is declarative:
toolbar.buttons: [create, delete]lists built-in actions in display order (creategated by create permission;deleteis bulk delete, enabled with a selection, confirming withdeleteConfirm), plustoolbar.search. A string value (Winter'sbuttons: list_toolbarpartial) 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
cabanahandlers, generated insummercms.go, converted to OpenAPI 3 (reuse theswagger2openapi.goapproach), committed, and fed to openapi-typescript, with a drift check. The framework SPA never reads an app repo's document.fonoteka.go/docs/openapi.jsonremains 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 fromnameFrom, on the Phase 9 D-11 list contract. An optionalRelationExtendOptionsQuery(ctx, field, *gorm.DB) *gorm.DBhook on the admin controller lets apps scope options (e.g. to the active collection). Permission gating is the controller's. TheemptyOptionstays 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]}.cabanamaps belongs-to to the foreign key and syncs many-to-many through the Phase 5 join-table contract inside the save transaction, afterFormBeforeCreate/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). Thebackendguard accepts the cookie and still acceptsAuthorization: Bearerfor 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
HasSettingspages 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.yamlscopes of the three Phase 9 shapes (switch, daterange, model-backed) and drivesfilter[<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
cabanachange. Browser e2e (Playwright) is not part of Phase 10.
Plan-time resolutions (2026-09-27, after research)
- D-24:
messagesplaceholders use phrasebook/Winter syntax:count,:name,:term(with:Name/:NAMEcasing variants); D-13's braces were notation only. The SPA's interpolation mirrorsphrasebook.interpolateso server and client behave identically and Winter strings port verbatim. - D-25:
fonoteka.goadds a Collections side-menu item (collections, lucidelibrary, permissiongolem15.fonoteka.access_collections) as a documented deviation from the PHP navigation; navigation is not part of the API parity contract. - D-26: The Collections
ownerrelation field is read-only in Phase 10: relation fields whose foreign key is a protected fill key are rendered as a read-only label;protectedFillKeyandFormBeforeCreateare not widened. - D-27: Model-backed filter scopes (D-22) get their choices from an optional model capability
FilterOptions(scope string) []pact.Option, served atGET {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 servefor development. - Exact
messageskey 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
recordsPerPageand the Phase 9 cap). - Unsaved-changes confirmation, create-then-redirect behaviour (use
config_formcreate.redirect/redirectClosesemantics mapped to SPA routes), toast queue. - Whether admin paths are removed from
fonoteka.go/docs/openapi.jsononce the framework document exists (recommended: yes, one owner per path). - Package layout inside
summercms.gofor 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.mdand their SUMMARYs once written — final state of navigation/settings endpoints, security matrix and OpenAPI generation.cabana/http.go— route mounting (hardcoded/_admin/api/v1changed 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.shand../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 newmessagesand SPA string bundle (D-13, D-20); CLDR plural data matchesIntl.PluralRules.pact.DropdownOptionsProviderand 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.shpipeline (swag v1.16.6 → swagger2openapi → openapi-typescript 7.13.0).
Established Patterns
- Optional capability interfaces type-asserted by the consumer (
RelationExtendOptionsQueryfollows Phase 9 D-13). - Fail-loud boot on malformed plugin YAML (
DisallowUnknownField): applies tomessagesandtoolbar.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
cabanamount: prefix from config (D-03), SPA static serving + index.html base injection + history fallback (D-01, D-02), same prefix.backendguard in the bouncer registry: cookie extraction + CSRF header check (D-19)./auth/mealready returns name/email; the user menu needs the role name too.fonoteka.go: YAML toolbar/messages updates, lucide icons, optionalRelationExtendOptionsQueryon 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.htmpartial: 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).
- Phase 10.1: runtime admin extension point.
AdminAssets()on controllers (WinteraddJs/addCss), served under the admin prefix and loaded when the controller opens;type: widgetrendered as custom elements (plugins ship plain JS, no Vue coupling);type: partialrendered 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