157 lines
18 KiB
Markdown
157 lines
18 KiB
Markdown
# Phase 10: Admin Vue SPA - Context
|
|
|
|
**Gathered:** 2026-09-26
|
|
**Status:** Ready for planning (after Phase 9 plans 09-11 and 09-12 complete)
|
|
|
|
<domain>
|
|
## 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.
|
|
|
|
</domain>
|
|
|
|
<decisions>
|
|
## 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.
|
|
|
|
</decisions>
|
|
|
|
<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>
|
|
|
|
<specifics>
|
|
## 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).
|
|
|
|
</specifics>
|
|
|
|
<deferred>
|
|
## 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.
|
|
|
|
</deferred>
|
|
|
|
---
|
|
|
|
*Phase: 10-admin-vue-spa*
|
|
*Context gathered: 2026-09-26*
|