21 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | estimate | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10-admin-vue-spa | 04 | execute | 4 |
|
|
true |
|
|
|
Phase Goal
As a backend administrator, I want to open my project's own admin URL, log in and manage Albums, Artists, Collections, Genres and Styles through schema-driven lists, forms and the relation manager, so that I can administer the catalogue from one Go binary without the WinterCMS backend.
Complete the admin experience: the relation manager with its picker modal (Collections editors: search, link, unlink) and the shell polish from the design (collapsible panel with flyout below 1100px, user menu with role and logout, breadcrumbs, dark mode, toast polish).Purpose: Success criterion 3 (the Collections relation manager searches, links and unlinks an editor) plus the remaining D-06 fidelity items. Decisions implemented: D-05 (relation-manager renderer), D-06, D-10 (active rail and flyout), D-11 (icons already mapped in Plan 10-01 are reused); D-28 fixes this plan's scope.
Output: relation components, shell components, sidebar/theme state, smoke tests, rebuilt boardwalk/dist.
Repo: summercms.go only. Commit code separately from planning docs; never add co-author tags.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Artifacts this phase produces
RelationManager.vue,RelationPickerModal.vue;relation-managerregistered inregistry.tsSectionFlyout.vue,UserMenu.vue,Breadcrumbs.vue; updatedAppShell,PluginRail,SectionPanel,ToastuseSidebar()(viewport below 1100px via matchMedia plus persisted manual flag, localStorage keysummer-admin.sidebar),useAuth().logout(),app/theme.ts(applyColorScheme())- New
backend::langkeys for shell and relation strings; smoke teststests/smoke/relation.smoke.test.ts,tests/smoke/shell.smoke.test.ts
RelationManager.vue: load schema/relation/{name} once (label, view columns, view.toolbarButtons, view.showSearch, messages). Section header: the relation label at 17px/700 with the relation-manager field's comment as helper text when the YAML declares one, a 220px by 38px search input (300 ms debounce, placeholder from the list search prompt default), and the toolbar buttons in declared order at 38px height: link as the primary user-plus button labelled with the link message, unlink as the danger outline user-minus button labelled with the unlinkSelected message and disabled without a selection. The linked list reuses DataTable with 56px rows inside a 12px-radius bordered container: selection checkbox, the first view column as an initials avatar (30px) plus text at 600 weight, the remaining view columns muted, pagination from the list meta, and the relation empty message when nothing is linked. Unlink confirms with the plural unlinkConfirm message, POSTs unlink with {ids}, clears the selection, reloads and toasts the plural unlinked message.
RelationPickerModal.vue (Reka Dialog): 560px wide, radius 20, padding 24, backdrop from the overlay token, role="dialog" with aria-modal, title from the link message (20px/700), helper from linkHint, a 34px close button (x, subtle background, labelled from t), a 44px search input with autofocus and the candidateSearch placeholder, and the candidate list as a role="listbox" with aria-multiselectable="true" loaded from .../candidates with per_page=5, the search term and the page. Each option is a 54px row (radius 12, border): a 32px initials avatar, the first manage column at 600 weight above the second manage column at 13px muted, and a checkbox; a selected option has the sel background and a #e0b020 border. Selection is a set kept across pages and cleared on close. The pager shows :from–:to z :total and 34px previous and next buttons (radius 9). The footer has two equal-width buttons: Anuluj (outline) and Dodaj (:count) (primary, disabled when the set is empty). Confirming POSTs link with the ids, closes, reloads the linked list and toasts the plural linked message. Esc closes, focus stays inside while open, and focus returns to the triggering button on close. The SPA never filters candidates itself; the owner exclusion is the server's.
Add the new keys to both backend lang files, neutral relation fixtures (acme.demo.widgets relation members with columns name and email), and tests/smoke/relation.smoke.test.ts (hidden on create; linked list render and search; unlink confirm and body; modal paging at five, selection across pages, disabled at zero, link body, focus return, Esc), then rebuild boardwalk/dist.
npm --prefix admin run typecheck && npm --prefix admin test -- tests/smoke/relation.smoke.test.ts && go test ./phrasebook -run '^TestPhase10SPAKeysResolve$' -count=1 -v && scripts/check-admin-dist.sh && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestCollectionsAdmin(RelationSchema|Link|Unlink|CrossScope|RelationEdges)$' -count=1 -v)
<fails_when>Any command exits non-zero; vitest prints "No test files found" or any failed test; a go test output lacks its "--- PASS" lines or shows "no tests to run" or SKIP; check-admin-dist.sh prints a diff.</fails_when>
With summer serve running in ../fonoteka.go, open http://localhost:8080/plytadmin, log in, open Kolekcje, edit a collection and switch to the Edytorzy tab; search, add two editors in the modal, then remove one.
The tab is absent on the create form; the modal lists users five per page without the owner, Dodaj (2) links both and the toast confirms; removing asks for confirmation and the row disappears; focus returns to the Dodaj button after closing the modal.
<why_human>Focus management and the real link/unlink round trip in a browser are outside Vitest's DOM (D-23: no Playwright).</why_human>
<acceptance_criteria>
- The relation smoke test passes and covers every behaviour listed at the end of the action.
- grep -c "'relation-manager'" admin/src/components/form/registry.ts prints 1.
- grep -c 'aria-multiselectable' admin/src/components/relation/RelationPickerModal.vue prints at least 1.
- The fonoteka Collections relation suites still pass through the prefix.
</acceptance_criteria>
On an existing Collection an admin can search editors, add several through the picker and remove selected ones, with confirmations and toasts.
(1) Sidebar state: src/state/useSidebar.ts combines matchMedia('(max-width: 1099px)') (forces collapsed without overwriting the manual preference) with a manual flag persisted in localStorage under summer-admin.sidebar (boolean only). SectionPanel (224px) gets its header with the plugin label at 16px/700 and a 30px "Zwiń menu" button (panel-left-close); when collapsed only the 80px rail remains and a 40px "Rozwiń menu" button (panel-left-open) appears above Ustawienia.
(2) Flyout: SectionFlyout.vue opens in collapsed mode when a rail item is hovered or focused, or on Enter or ArrowDown: 230px wide, 8px right of the rail, aligned with the header area, surface background, border, radius 14, padding 8, shadow 0 16px 40px rgba(20,27,45,.18), a bold plugin title and 38px items (radius 9) with the panel's idle and active styles; role="menu" with menuitems, closes on Esc or about 200 ms after mouse-leave, and returns focus to the rail item. Clicking a rail item navigates to its first permitted side-menu controller (or the entry's controller); the active rail item is the one owning the route's vendor and plugin.
(3) Header: 64px surface header with Breadcrumbs.vue (plugin label, controller label, record title; parent crumbs are muted links, the current crumb is text colour at 600; chevron-right 14px separators) and UserMenu.vue (Reka DropdownMenu): a 44px button with a 34px accent avatar of initials (first and last name, else login), the name (600) above the role name from /auth/me (12px muted; both hidden at tablet width) and chevron-down; the 280px menu (radius 14, padding 8) holds a header with a 40px avatar, the bold name and muted email, a divider and a Wyloguj item (log-out). useAuth().logout() POSTs /auth/logout, clears the in-memory user and navigation, and routes to login even when the call fails.
(4) Theme and polish: src/app/theme.ts applies the .dark class on the document element from prefers-color-scheme: dark and follows changes (A6: no toggle); the rail keeps the dark side colour in both modes with the #222b40 right border in dark mode. Toasts keep one queue and show the most recent first; transitions are 150 ms ease-out for hover and background and 200 ms fade plus scale from 0.98 for dialogs; no decorative animation.
(5) Add the new keys to both backend lang files and tests/smoke/shell.smoke.test.ts (persisted collapse and viewport-forced collapse, flyout open by keyboard and Esc with focus return, user menu shows role and logout calls the API then routes to login even on failure, breadcrumbs for a record route, dark class follows a mocked matchMedia), then rebuild boardwalk/dist.
npm --prefix admin run typecheck && npm --prefix admin test -- tests/smoke && go test ./phrasebook -run '^TestPhase10SPAKeysResolve$' -count=1 -v && scripts/check-admin-dist.sh
<fails_when>Any command exits non-zero; vitest prints "No test files found" or any failed test; the go test output lacks "--- PASS: TestPhase10SPAKeysResolve" or shows "no tests to run"; check-admin-dist.sh prints a diff.</fails_when>
With summer serve running in ../fonoteka.go, compare http://localhost:8080/plytadmin (login, Albumy list, an album form, Kolekcje editors with the modal open) against .planning/phases/10-admin-vue-spa/design/Direction C v2.dc.html at 1280px and at about 900px width, in light and in dark system mode; log in once as a limited admin (only golem15.fonoteka.access_genres) and once as a superuser.
Tokens, typography, radii, control heights, focus rings and copy match the design; below about 1100px only the rail shows and the flyout opens from it; the user menu shows the role and Wyloguj returns to login; the limited admin sees only Gatunki in the Fonoteka side menu while the superuser sees every entry.
<why_human>Visual fidelity and responsive behaviour are judged against the design reference; D-23 excludes browser e2e.</why_human>
<acceptance_criteria>
- All smoke tests under tests/smoke pass, including the shell cases listed in item (5).
- grep -rn 'localStorage' admin/src | grep -v 'summer-admin.sidebar' | grep -v 'useSidebar' prints nothing.
- grep -c 'auth/logout' admin/src/state/useAuth.ts prints at least 1.
</acceptance_criteria>
The admin shell behaves like the design at desktop and tablet widths in light and dark mode, and an admin can see their role and log out.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| SPA → relation link/unlink | Selected ids cross into pivot writes |
| Browser storage | Anything persisted survives the session on a shared machine |
| UserMenu → logout | The session must end even when the network call fails |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-10-21 | Elevation of Privilege | relation link of candidates outside scope (owner, inactive users) | medium | transfer | Enforced server-side by Phase 9 (RelationExtendManageQuery, ExcludedRelatedIDs, TestCollectionsAdminForgedPivot/CrossScope); the SPA only posts ids chosen from the server's candidates and runs those suites as a regression in Task 1. |
| T-10-22 | Information Disclosure | localStorage | low | mitigate | Only the sidebar boolean is stored under summer-admin.sidebar; acceptance grep rejects any other localStorage use. |
| T-10-23 | Spoofing | logout on a shared browser | medium | mitigate | logout POSTs /auth/logout (server blacklists the jti and expires the HttpOnly cookie, Plan 10-01) and the SPA clears state and routes to login even on failure; shell smoke test covers both paths. |
| T-10-SC | Tampering | npm dependencies | high | mitigate | No new package; npm ci against the lockfile approved in Plan 10-01. |
| </threat_model> |
<success_criteria>
- The Collections editors relation manager searches, links and unlinks (SC-3).
- The shell matches the design's responsive, flyout, user menu, breadcrumb and dark-mode behaviour (D-06). </success_criteria>