diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index dca02f60..bb8be606 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1423,9 +1423,11 @@ Plans: 2. Privilege rules hold on the server: only a superuser can create, edit or grant superuser; an admin cannot raise their own role or permissions; the last active superuser cannot be deactivated, demoted or deleted. 3. The record-action seam on the Administrators list is usable by plugins (19.7's link action), and unit tests cover the privilege rules. -**Plans:** 4/4 plans executed +**Plans:** 5/5 plans executed Plans: +- [x] 19.6.1-05-PLAN.md + **Wave 1** - [x] 19.6.1-01-PLAN.md — server side: the pact action-extension seam, the internal summercms.cabana pseudo-plugin with the Administrators and Roles screens, every privilege rule in the save transaction (superuser hiding, ceilings, self-edit, outranking, last-superuser lock, revocation), lang keys and docs @@ -1595,7 +1597,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 19.4.1. Early public demo on demo.summercms.io | 5/5 | In Progress| | | 19.5. Social login in sm-user-plugin | 5/5 | Complete | 2026-10-21 | | 19.6. Two-factor auth and device flow in sm-user-plugin | 0/TBD | Not started | - | -| 19.6.1. Backend Administrators screen (INSERTED) | 4/4 | In Progress| | +| 19.6.1. Backend Administrators screen (INSERTED) | 5/5 | In Progress| | | 19.7. Generic MCP module | 0/TBD | Not started | - | | 19.8. Console commands overhaul (INSERTED) | 6/6 | In Progress| | | 20. Demo instance with every shared plugin | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index cb579c78..ce5ba709 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,19 +1,19 @@ --- msd_state_version: "1.0" milestone: v1.0 -current_phase: 19.4.1 -current_phase_name: Early public demo on demo.summercms.io -status: Ready for verification — 6/6 plans executed, UAT in progress (19.3-UAT.md) -stopped_at: Phase 19.4.1 context gathered -last_updated: "2026-10-20T01:33:22.399Z" -last_activity: 2026-10-20 -last_activity_desc: "Completed quick task 261020-42e: branded HTML error pages for browsers" -state_head: 2b5f0b2c92e0bc38070c7f4a8870f64e2c430bb8 +current_phase: 19.6.1 +current_phase_name: Backend Administrators screen (INSERTED) +status: executing +stopped_at: Completed 19.6.1-05-PLAN.md +last_updated: "2026-10-22T12:43:33.722Z" +last_activity: 2026-10-22 +last_activity_desc: Phase 19.6.1 execution started +state_head: 51e1a1ea6ed3f5e5aa08d07210452bf3e6622f8a progress: - total_phases: 43 - completed_phases: 35 - total_plans: 217 - completed_plans: 208 + total_phases: 45 + completed_phases: 36 + total_plans: 238 + completed_plans: 229 gsd_state_version: "1.0" milestone_name: milestone --- @@ -25,22 +25,22 @@ milestone_name: milestone See: .planning/PROJECT.md (updated 2026-10-12) **Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. -**Current focus:** Phase 19.3 — Framework transport and infrastructure for WavePath (UAT in progress) +**Current focus:** Phase 19.6.1 — Backend Administrators screen (INSERTED) ## Current Position -Phase: 19.4.1 (Early public demo on demo.summercms.io) — READY TO EXECUTE -Plan: 6 of 6 -Status: Ready for verification — 6/6 plans executed, UAT in progress (19.3-UAT.md) -Last activity: 2026-10-20 - Completed quick task 261020-42e: branded HTML error pages for browsers +Phase: 19.6.1 (Backend Administrators screen (INSERTED)) — EXECUTING +Plan: 2 of 5 +Status: Ready to execute +Last activity: 2026-10-22 — Phase 19.6.1 execution started -Progress: [████████░░] 83% +Progress: [████████░░] 82% ## Performance Metrics **Velocity:** -- Total plans completed: 105 +- Total plans completed: 110 - Average duration: 21 min - Total execution time: 104 min @@ -66,6 +66,7 @@ Progress: [████████░░] 83% | 19 | 7 | - | - | | 19.1 | 6 | - | - | | 19.4 | 4 | - | - | +| 19.5 | 5 | - | - | **Recent Trend:** @@ -263,6 +264,17 @@ Progress: [████████░░] 83% | Phase 19.4 P02 | 50min | 5 tasks | 54 files | | Phase 19.4 P03 | 71 min | 7 tasks | 61 files | | Phase 19.4 P04 | 65 min | 5 tasks | 42 files | +| Phase 19.4.1 P01 | 70min | 4 tasks | 24 files | +| Phase 19.4.1 P02 | ~2h45m | 7 tasks | 43 files | +| Phase 19.4.1 P03 | ~1h50m | 8 tasks | 58 files | +| Phase 19.4.1 P04 | 2h25m | 7 tasks | 85 files | +| Phase 19.4.1 P05 | 70min | 7 tasks | 46 files | +| Phase 19.8 P01 | 30 min | 3 tasks | 86 files | +| Phase 19.8 P02 | 28 min | 3 tasks | 38 files | +| Phase 19.8 P03 | 3h 3m (segment B) | 8 tasks | 120 files | +| Phase 19.8 P05 | 40min | 7 tasks | 33 files | +| Phase 19.6.1 P04 | 18 min | 3 tasks | 10 files | +| Phase 19.6.1 P05 | 4 min | 2 tasks | 7 files | ## Accumulated Context @@ -297,6 +309,8 @@ Progress: [████████░░] 83% - Phase 19.7 inserted after Phase 19.6: Generic MCP module: opt-in endpoint, plugin-declared tools, one connector per person (from /msd-explore 2026-10-17) - Phase 20 reordered: Roadmap reordered (/msd-explore 2026-10-18): new Phase 20 Demo instance with every shared plugin; Płytarium cutover moved 20 → 30; Chat polish against WavePath clients moved 19.2 → 31; 21–29 added as their goals firm up - Phase 19.4.1 inserted after Phase 19.4: Early public demo on demo.summercms.io: today's 16 shared plugins on rome, demo accounts, journal/presentations/quizzes/OIDC pages, backend by request; Phase 20 narrows to the chat family (URGENT) +- Phase 32 added: Console commands overhaul: diary logging module, bonfire global flags and Artisan-grade help, sweep of all commands, tests +- Phase 19.6.1 inserted after Phase 19.6: Backend Administrators screen: admins and roles CRUD in cabana (gap found while planning 19.7: no screen to manage backend users or set the frontend link) (URGENT) ### Decisions @@ -893,6 +907,38 @@ Recent decisions affecting current work: - [Phase 19.4]: Phase gate stages split by Docker need; the application's unrelated admin route test stays outside the gate (deferred-items) - [Phase 19.4]: PHP method map: 171 methods, 146 ported, 25 adapted with reasons, none not-applicable - [Phase 19.4]: Parity NOT RUN points to the WavePath cutover todo and does not hold the verdict (D-14 amended); PHASE194_REQUIRE_PARITY=1 fails +- [Phase 19.4.1]: Roadmap decks use the new summer deck theme (summercms.io look: navy, sun yellow, Roboto), chosen at the deck review; fonts vendored from the summercms.io build in sm-bm-app, no new npm dependency +- [Phase 19.4.1]: golem15.presentations gets summer as an additive theme option; plan 04 must port summer.css and accept summer in the demo deck API +- [Phase 19.4.1]: Admin public at https://demo.summercms.io/smbackend, login required, /backend 404 (user, 2026-10-20). Replaces the phase goal's and D-04's 'admin backend stays private'. Plans 03-05 recheck: plan 03 CTA/contact form wording, CONTEXT deployment note, threat row T-19.4.1-06 (new mitigation: login required, credentials on request, 5/min login throttle per real IP via Cloudflare real-IP, then 429), seeded FAQ 'The admin is private on this demo' (seed.go and the live row). — The user asked for the admin at the Task 7 review of the first live release; the throttle was checked live behind Cloudflare. +- [Phase 19.4.1]: The 'Want to see the backend? Contact Golem15' CTA stays a contact CTA and does not link to /smbackend (user, 2026-10-20). The admin URL is public, its credentials are not; Golem15 hands them out on request. Plan 03's contact form (D-04) still applies; only its wording and the threat model change. — Resolves the open question raised by the /smbackend decision. +- [Phase 19.4.1]: Plugin release tags for the demo: paymentgateway v0.1.0; sitemap, menu, faq, oauthserver v0.1.0; pgsub v0.1.2 and pgstripe v0.1.2 re-tagged on paymentgateway v0.1.0; translate v0.1.2 and presentations v0.1.1. sm-demo-app go.mod requires tags only (summercms v0.3.0), no replace; release builds use GOWORK=off. — D-03; tags confirmed with git ls-remote after each push. +- [Phase 19.4.1]: 19.4.1-03: tags sm-user-plugin v0.1.3 (7b9faa3) and sm-journal-plugin v0.2.1 (fe1bbc2); sm-demo-app requires both, other apps' pins untouched; Phase 19.5 takes the next free sm-user-plugin tag +- [Phase 19.4.1]: 19.4.1-03: Turnstile is opt-in in the core user plugin (golem15.user.turnstile, empty default adds nothing, open|closed on /login and /register only, invalid value fails boot); the demo sets closed +- [Phase 19.4.1]: 19.4.1-04: the demo runs its own Centrifugo v6.5.1 on rome (127.0.0.1:8010, /usr/local/bin/centrifugo, supervisor summercms-demo-centrifugo) with its own secrets; nothing shared with bm (D-17). Assumption A3 held: static_headers under channel.proxy.subscribe.http is accepted. +- [Phase 19.4.1]: 19.4.1-04: logged in, the home page's Presentations card and a header link go to My presentations (/presentations); logged out the card goes straight to the deck of the language. SSR reads the session cookie, so server and hydrated links agree. +- [Phase 19.4.1]: 19.4.1-04: on a computer the remote opens in a 390x844 named popup (speaker view Open remote, My presentations Remote); opened without noopener to detect blocking, opener cut at once; blocked popups fall back to a tab; phones open it in place. No QR code exists (would need an approved package). +- [Phase 19.4.1]: 19.4.1-04: the demo deck API refuses edits to a published deck (403), keeping the seeded roadmap decks intact. +- [Phase 19.4.1]: Phase 19.4.1 database tests boot the whole demo on a fresh database cloned from a migrated template per plugin list (internal/demotest, one postgres:15 container per test binary) +- [Phase 19.4.1]: bm's frontend library tests run byte-identical in vue-demo-app under vitest 3.2.7; aliases map bm's @/api, @/deck and ../../deck/ layout +- [Phase 19.4.1]: sm-demo-app scripts/check-all.sh is the phase gate: every suite, the release build, pin/deploy/smoke checks, local browser checks when the stack is up, and check-live +- [Phase 19.8]: Charm v2 stack on charm.land paths (decision note 2026-10-21-console-charm-stack.md); plan 19.8-01 installed lipgloss v2.0.6 and colorprofile v0.4.3 only +- [Phase 19.8]: diary owns the process logger: config/log.yaml channels (console, stderr, daily, stack, null), per-component levels, sunscreen on every leaf, daily files on by default (storage/logs, 14 days) +- [Phase 19.8]: wristband and bouncer now import diary for their log lines, amending their dependency-free leaf status (confirmed by the user 2026-10-21: diary is the one allowed framework import, and wristband is required to use it) +- [Phase 19.8]: 19.8-02: bonfire.NewRoot sends the error block to stderr when its output is os.Stdout; Output.Error keeps its stream, so no command output moves +- [Phase 19.8]: 19.8-02: bonfire.Execute resets persistent flags per run and routes a call without a command word (globals, -h, --help) to the Artisan list; fang runs without version, man page and completions +- [Phase 19.8]: 19.8-02: --no-ansi on an interactive terminal uses the plain line prompts (no huh, no selector); -n and -q return prompt defaults; ConfirmProceed never asks off an interactive terminal +- [Phase 19.8]: 19.8-02: one live view at a time; output and diary log lines printed while it runs go above it via bonfire.PrintAbove (Program.Send, safe after the program ends) +- [Phase 19.8]: 19.8-02: builtin list and help carry Long and Examples so the framework CheckDocs test in plan 3 covers them +- [Phase 19.8]: 19.8-03: SummerCMS v0.4.0 tagged at 99b6df4 and pushed with its changelog post; commits other sessions made during the run stay local +- [Phase 19.8]: 19.8-03: the ten plugins applications mount got patch tags (user v0.1.6, journal v0.2.2, pgstripe v0.1.3, the rest v0.1.1) because sm-demo-app refuses pseudo-version pins +- [Phase 19.8]: 19.8-03: chat and forum require sm-user-plugin v0.1.2 (classes.DeletingEvent); plugins log through a package-level pluginLog = diary.For(code) +- [Phase 19.8]: 19.8-05: CR-01 prints above live views by sending tea.Println messages straight to the program queue under liveMu; unregister/finish/wait as one locked, deferred step +- [Phase 19.8]: 19.8-05: WR-01 handled inside runLive (relay SIGINT/SIGTERM while a view runs, Kill, re-raise); generated main unchanged +- [Phase 19.8]: 19.8-05: CR-02 uses the additive bonfire.WithExitCode carrier; CR-03 delegates get os.Stdout/os.Stderr on a terminal, stderr kept apart otherwise +- [Phase 19.8]: 19.8-05: dev.ShutdownGrace (15s) shared by summer dev and delegates; daily files retry after a minute or on a new date; replaced daily writers are retired +- [Phase 19.8]: 19.8-05: bonfire.Sanitize (additive) replaces diary's sanitizer; Migrate iterates migrationSets +- [Phase 19.6.1]: Roles create and update refuse any new code that rolePermissions attaches grants to, including when the editor is a superuser — D-16 stricter reading from the gap contract: a role never becomes a code-declared system role through the Roles screen +- [Phase 19.6.1]: cabana wraps contributed Administrators Runs; guardOutranked stays unexported and contributors do not own the check — D-19 gap contract: wrap in cabana so a contribution to summercms.cabana.users cannot skip outranking ### Pending Todos @@ -979,6 +1025,8 @@ Recent decisions affecting current work: | 261019-hgw | Translate hook ValidationError phrase keys in the request locale | 2026-10-19 | ce8d7ef | [261019-hgw-translate-hook-validationerror-phrase-ke](./quick/261019-hgw-translate-hook-validationerror-phrase-ke/) | | 261020-13e | Admin image previews of public files fall back to the authenticated thumb route | 2026-10-19 | 1ab7db4 | [261020-13e-admin-file-upload-widget-image-preview-a](./quick/261020-13e-admin-file-upload-widget-image-preview-a/) | | 261020-42e | branded HTML error pages for browsers | 2026-10-20 | a34c251 | [261020-42e-branded-html-error-pages-for-browsers-un](./quick/261020-42e-branded-html-error-pages-for-browsers-un/) | +| 261020-sdt | Rotate the site-cache purge token and rename the layer variable (demo + summercms.io) | 2026-10-20 | 4270d05 | [261020-sdt-rotate-the-purge-token-and-rename-the-la](./quick/261020-sdt-rotate-the-purge-token-and-rename-the-la/) | +| 261022-4z8 | Fix the open 19.8 re-review findings (WR-09..WR-14, IN-14, IN-15) | 2026-10-22 | b1fba7e | [261022-4z8-fix-the-open-19-8-re-review-findings-wr-](./quick/261022-4z8-fix-the-open-19-8-re-review-findings-wr-/) | ## Deferred Items @@ -990,6 +1038,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-10-20T00:18:01.483Z -Stopped at: Phase 19.4.1 context gathered -Resume file: .planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-CONTEXT.md +Last session: 2026-10-22T12:43:32.317Z +Stopped at: Completed 19.6.1-05-PLAN.md +Resume file: None diff --git a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-PLAN.md b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-PLAN.md new file mode 100644 index 00000000..2f29621f --- /dev/null +++ b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-PLAN.md @@ -0,0 +1,247 @@ +--- +phase: 19.6.1-backend-administrators-screen +plan: 05 +type: execute +wave: 5 +depends_on: ["19.6.1-01", "19.6.1-02", "19.6.1-03", "19.6.1-04"] +files_modified: + - admin/src/components/form/formContext.ts + - admin/src/views/FormView.vue + - admin/src/components/relation/RelationChildModal.vue + - admin/src/components/form/fields/RelationField.vue + - admin/tests/form/RelationField.test.ts + - admin/tests/form/FormView.test.ts + - modules/boardwalk/dist +autonomous: true +gap_closure: true +gap_ids: [G-19.6.1-T2] +requirements: [] + +estimate: + tokens: 35000 + raw_tokens: 35000 + tasks: 2 + confidence: high + +must_haves: + truths: + - "G-19.6.1-T2: After picking a relation option on a tabbed form, switching tabs and coming back still shows that option's name, not String(id)." + - "A remounted RelationField seeds its id→label cache from the form's remembered labels, not only from the server meta.labels captured at load." + - "modules/boardwalk/dist matches a fresh admin build so the embedded SPA used by the local demo carries the fix." + artifacts: + - path: "admin/src/components/form/formContext.ts" + provides: "FORM_REMEMBER_LABEL injection key" + contains: "FORM_REMEMBER_LABEL" + - path: "admin/src/views/FormView.vue" + provides: "FormView persists picked relation labels on the labels ref" + contains: "FORM_REMEMBER_LABEL" + - path: "admin/src/components/form/fields/RelationField.vue" + provides: "choose() remembers the picked option on the form before emit" + contains: "rememberLabel" + - path: "admin/tests/form/FormView.test.ts" + provides: "tab-switch backstop for a picked relation label" + contains: "keeps a picked relation label after switching tabs" + - path: "admin/tests/form/RelationField.test.ts" + provides: "unit backstop: remount with remembered labels shows the name" + contains: "shows a picked label after remount when the form remembered it" + - path: "modules/boardwalk/dist" + provides: "embedded admin build includes the remount-safe labels" + contains: "index.html" + key_links: + - from: "admin/src/components/form/fields/RelationField.vue" + to: "admin/src/components/form/formContext.ts" + via: "choose injects FORM_REMEMBER_LABEL and writes the picked option before update:modelValue" + pattern: "FORM_REMEMBER_LABEL" + - from: "admin/src/views/FormView.vue" + to: "admin/src/components/form/fields/RelationField.vue" + via: "remembered labels flow back as props.labels so remount's watch seeds known" + pattern: "rememberRelationLabel" + prohibitions: + - statement: "Do not keep inactive tab panels mounted with v-show (or otherwise render every tab's fields). That would change trigger hide-tab, ML locale reset, and field mount cost. Persist labels; keep the current one-panel FormGrid." + status: unresolved + verification: grep + - statement: "Do not edit 19.6.1-01/02/03/04-PLAN.md. Do not change cabana Go, pact, phrasebook, YAML screens, or OpenAPI. No new npm dependency." + status: unresolved + verification: grep + - statement: "Framework code, READMEs, docs and tests name no consuming application." + status: unresolved + verification: grep +--- + +## Phase Goal + +ROADMAP Goal (not a user story; MVP mode off): cabana gets WinterCMS's Administrators screens with privilege rules on the server. + +This plan's slice (plan 5 of 5, wave 5, gap_closure): close UAT gap G-19.6.1-T2 (test 2, minor). After picking a role (or any relation) on the Administrators form, switching to another tab and back shows the option name, not the raw id. Save already assigned the role; this is labeling only. Ignore CR-01 / WR-03 (closed by plan 04) and IN-01..IN-06. + +## Why plan 04 did not close this + +Plan 04 `gap_ids` were `[G-19.6.1-1, G-19.6.1-2]` from VERIFICATION.md review findings (CR-01 code-declared role capture, WR-03 contributed Run skip of guardOutranked). The dropdown issue lived only in `19.6.1-UAT.md` ## Gaps, had no `gap_id`, and VERIFICATION.md called it "not a gap-closure item and not a failed must-have". Plan 04 also prohibited SPA changes. `--gaps-only` therefore never selected this truth. + +## Spec-less probe fallback + +Spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip). Edge row is the UAT diagnosis already on disk. + +## Discovery + +Level 0: existing RelationField `known` cache, FormView `labels` ref, FormView `panelFields` (active tab only). No new dependency. + +## Decisions taken in this plan + +- Persist picked labels on the form. Do not v-show inactive tabs. +- New inject `FORM_REMEMBER_LABEL`: `(field: string, option: RelationOption) => void`. FormView and RelationChildModal provide it. RelationField calls it from `choose` when `entry.value !== null` (single and multiple), using `entry.label` and `entry.locked`. A missing provider is a no-op (standalone mount, settings pages without labels). +- FormView helper `rememberRelationLabel` merges the option into `labels.value[field]` by `value` (replace same id, append otherwise). `adopt()` still replaces labels from the server response on load/save. +- RelationField's existing `watch(() => props.labels, remember, { immediate: true })` and `labelOf` stay. Remount seeds `known` from the updated `props.labels`. +- Test names: `keeps a picked relation label after switching tabs` (FormView.test.ts) and `shows a picked label after remount when the form remembered it` (RelationField.test.ts). + +## Source coverage (this gap plan) + +| Source | Item | Plan | Status | +|---|---|---|---| +| UAT Gaps | G-19.6.1-T2 / test 2 role dropdown id after tab switch | 05 | COVERED | +| VERIFICATION gaps | G-19.6.1-1 / CR-01 | 04 | COVERED (prior) | +| VERIFICATION gaps | G-19.6.1-2 / WR-03 | 04 | COVERED (prior) | + +## UI Considerations + +No new tokens, colours, or components. The combobox already shows `labelOf(id)`. This plan only keeps that map populated across a tab remount. + +| Category | Element | Status | Resolution | +|----------|---------|--------|------------| +| display | Relation single combobox after tab switch | covered | Form remembers the picked `{value,label}`; remount seeds `known` from `props.labels` | +| display | Relation multiple chips after tab switch | covered | same remember path from `choose` | + +## Artifacts this phase produces + +- `FORM_REMEMBER_LABEL` in `formContext.ts`. +- FormView + RelationChildModal providers; RelationField `choose` write. +- Two vitest names above. +- Rebuilt `modules/boardwalk/dist`. + + +Close G-19.6.1-T2: a picked relation option keeps its name when FormView unmounts the field on a tab switch and remounts it. + +Purpose: UAT test 2 labeling. Save is already correct. +Output: inject + two providers + choose() write, two vitest names, rebuilt boardwalk dist. No Go/pact/YAML. Skip git commit (orchestrator commits). Skip STATE.md raw edits. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md +@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-VERIFICATION.md +@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-04-PLAN.md +@.planning/phases/19.6.1-backend-administrators-screen/19.6.1-04-SUMMARY.md + +Concurrency and commits: other sessions may touch admin/ or boardwalk/dist. Before each task run `git status --porcelain -- admin modules/boardwalk/dist` and stop if a file this task edits carries someone else's uncommitted change. Stage by explicit path, never `git add -A`; leave STATE.md and state.json alone; no co-author tags. The orchestrator commits after the checker — do not git commit from this plan. + + +- RelationField.vue: `known` Record seeded by `watch(() => props.labels, remember, { immediate: true })`. `labelOf(id)` is `known[id] ?? String(id)`. `choose(entry)` emits `update:modelValue` (id or null, or appended ids in multiple) and `closeList()`. It does not write back to the parent labels today. +- FormView.vue: `labels` ref assigned in `adopt()` from `record.meta.labels`. `panelFields` filters `tabOf(field) === activeTab`. One FormGrid, so inactive tab fields unmount. `update(name, value)` writes `values` only. +- RelationChildModal.vue: own `labels` ref, `adopt(record, meta)`, FormGrid, same remount if the child form is tabbed. +- formContext.ts: existing injects FORM_VALUES, FORM_PATCH, FIELD_SCOPE, FORM_TRIGGER_NOTES, FORM_FEEDBACK. Add FORM_REMEMBER_LABEL next to FIELD_SCOPE. +- RelationOption: `{ label: string; locked?: boolean; value: number }` (schema.d.ts). +- Fixtures: widgets.form-schema.json has `maker` (relation, default tab) and `serial` (text, tab Details). widgets.record.json has maker 2 / "Bolt & Co". optionsFixture.makerField has Acme Works (1) and Bolt & Co (2). FormView.test.ts already has `tab(wrapper, key)` in the D-21 describe; reuse that helper or a local copy. mountApp + mockApi as existing tests. +- Dist: `npm --prefix admin run build` writes modules/boardwalk/dist; `scripts/check-admin-dist.sh` rebuilds into a temp dir and diffs. + + + + + + + Task 1: Persist picked relation labels across FormView tab remount + admin/src/components/form/fields/RelationField.vue has choose() and labelOf(); FormView.vue has labels ref, adopt(), panelFields, and a single FormGrid; formContext.ts exports FIELD_SCOPE; admin/tests/form/FormView.test.ts and RelationField.test.ts exist; git status --porcelain -- admin is empty of other sessions' edits. + SPA-only inject and label merge; undo is a revert. No API or on-disk format change. + admin/src/components/form/formContext.ts, admin/src/views/FormView.vue, admin/src/components/relation/RelationChildModal.vue, admin/src/components/form/fields/RelationField.vue, admin/tests/form/RelationField.test.ts, admin/tests/form/FormView.test.ts + admin/src/components/form/fields/RelationField.vue, admin/src/views/FormView.vue, admin/src/components/relation/RelationChildModal.vue, admin/src/components/form/formContext.ts, admin/src/components/form/FormGrid.vue, admin/tests/form/RelationField.test.ts, admin/tests/form/FormView.test.ts, admin/tests/fixtures/widgets.form-schema.json, admin/tests/fixtures/widgets.options.json, .planning/phases/19.6.1-backend-administrators-screen/19.6.1-UAT.md + Close G-19.6.1-T2 in one production-quality slice. Do not v-show inactive tabs. Do not change Go, pact, YAML, or OpenAPI. + +formContext.ts: export `FORM_REMEMBER_LABEL` as `InjectionKey<(field: string, option: RelationOption) => void>` (import RelationOption from api/types). Comment: form hosts persist a picked relation option so a remounted RelationField can seed its id→label cache from props.labels; a host without a provider is a no-op. + +FormView.vue: add `rememberRelationLabel(field, option)` that copies `labels.value`, replaces or appends in `labels.value[field]` by option.value, and assigns the new object. provide(FORM_REMEMBER_LABEL, rememberRelationLabel) next to the other form provides. adopt() still overwrites labels from the server. + +RelationChildModal.vue: the same helper against its `labels` ref and the same provide, so a tabbed child form does not regress. + +RelationField.vue: inject FORM_REMEMBER_LABEL (default undefined). In choose(), when entry.value !== null, call rememberLabel?.(props.field.name, { value: entry.value, label: entry.label, locked: entry.locked || undefined }) before emit('update:modelValue', ...). Empty (null) choice does not remember. Do not change labelOf, remember(rows), or the props.labels watch. + +Tests, no consuming-application names: + +RelationField.test.ts: `shows a picked label after remount when the form remembered it`. Mount with maker, modelValue 2, labels Bolt & Co only, and provide FORM_REMEMBER_LABEL that records calls into a labels bag. Open the list (mockApi routes as existing), pick Acme Works (value 1). Expect the bag to contain { value: 1, label: 'Acme Works' }. Unmount. Mount again with modelValue 1 and labels from that bag (plus any previous). The combobox value is 'Acme Works', not '1'. + +FormView.test.ts: `keeps a picked relation label after switching tabs`. mountApp on /acme/demo/widgets/1 with the widgets form schema (maker on the default tab, serial on Details) and GET maker options from optionsFixture.makerField. Combobox starts as Bolt & Co. Open it, pick Acme Works. Click the Details tab, then the first tab (same tab() pattern as the D-21 describe). The maker combobox value is 'Acme Works', not '1'. Do not assert save. + +Existing RelationField and FormView tests stay green, including D-21 hide-tab backstops (those prove we did not switch to v-show of every panel). + + + npm --prefix admin run test -- tests/form/RelationField.test.ts tests/form/FormView.test.ts + non-zero exit, or either new it() name is missing from the run output + npm --prefix admin run typecheck + non-zero exit + + + - `grep -c 'FORM_REMEMBER_LABEL' admin/src/components/form/formContext.ts` prints at least 1. + - `grep -c 'rememberRelationLabel\|FORM_REMEMBER_LABEL' admin/src/views/FormView.vue` prints at least 2. + - `grep -c 'FORM_REMEMBER_LABEL' admin/src/components/relation/RelationChildModal.vue` prints at least 1. + - `grep -n 'function choose' -A 20 admin/src/components/form/fields/RelationField.vue | grep -c 'rememberLabel'` prints at least 1. + - `grep -c 'v-show' admin/src/views/FormView.vue` prints 0 (do not keep inactive panels mounted). + - `grep -c 'keeps a picked relation label after switching tabs' admin/tests/form/FormView.test.ts` prints 1. + - `grep -c 'shows a picked label after remount when the form remembered it' admin/tests/form/RelationField.test.ts` prints 1. + - `grep -ciE 'fonoteka|plytarium|grzyby|wavepath' admin/src/components/form/formContext.ts admin/src/views/FormView.vue admin/src/components/form/fields/RelationField.vue admin/src/components/relation/RelationChildModal.vue admin/tests/form/RelationField.test.ts admin/tests/form/FormView.test.ts` prints 0 for each file. + + Picking a relation option, switching tabs, and coming back shows the option name. Proven by the FormView tab-switch test and the RelationField remount test. + + + + Task 2: Rebuild the embedded admin dist + Task 1 source is on disk; git status --porcelain -- modules/boardwalk/dist admin/src is only this plan's files or empty; admin/node_modules exists (never run npm ci). + Generated embed; undo is a dist revert. + modules/boardwalk/dist + scripts/check-admin-dist.sh, admin/package.json + Rebuild the committed SPA embed so the local demo binary (go.work → boardwalk) serves the remount-safe labels. Run `npm --prefix admin run build` (vue-tsc then vite into modules/boardwalk/dist with emptyOutDir). Run `scripts/check-admin-dist.sh`. Do not commit (orchestrator commits). Do not touch admin/src in this task. + + scripts/check-admin-dist.sh && go test -short ./modules/boardwalk + non-zero exit, or check-admin-dist reports stale dist + + + - `scripts/check-admin-dist.sh` exits 0 and prints that modules/boardwalk/dist matches a fresh build. + - `test -f modules/boardwalk/dist/index.html` succeeds. + + The embedded admin build matches the Task 1 SPA, so a `scripts/dev.sh` rebuild of sm-demo-app picks up the fix. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| admin SPA field → form labels ref | Picked option label is display-only; save still posts the id the combobox already emitted | +| options API → remember() | Labels come from the same options payload the administrator already saw | + +## STRIDE Threat Register + +Threat IDs continue after T-19.6.1-31. + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.6.1-32 | Tampering | FORM_REMEMBER_LABEL label string | low | accept | Display only; server still authorizes the posted id. Do not v-html the label (RelationField already interpolates text). | +| T-19.6.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | this plan installs no package | + + + + +- After Task 1: `npm --prefix admin run test -- tests/form/RelationField.test.ts tests/form/FormView.test.ts` and `npm --prefix admin run typecheck`. +- After Task 2: `scripts/check-admin-dist.sh` and `go test -short ./modules/boardwalk`. +- Do not run Phase 19.6 fonoteka/user-plugin commands. + + + +- After picking a relation option on a tabbed form, switching tabs and back shows the option name, not String(id). +- The two named vitest cases exist and pass. +- modules/boardwalk/dist matches a fresh admin build. +- Plans 01-04 are untouched. No Go/pact/YAML change. + diff --git a/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-SUMMARY.md b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-SUMMARY.md new file mode 100644 index 00000000..3c40757c --- /dev/null +++ b/.planning/phases/19.6.1-backend-administrators-screen/19.6.1-05-SUMMARY.md @@ -0,0 +1,155 @@ +--- +phase: 19.6.1-backend-administrators-screen +plan: 05 +subsystem: ui +tags: [admin-spa, relation-field, form-labels, boardwalk] + +requires: + - phase: 19.6.1-01 + provides: Administrators and Roles screens compiled from YAML + - phase: 19.6.1-02 + provides: privilege ceiling, permission locks, Roles form + - phase: 19.6.1-03 + provides: lifecycle tests and phase gate + - phase: 19.6.1-04 + provides: CR-01 / WR-03 gap closure +provides: + - FORM_REMEMBER_LABEL inject so form hosts persist a picked relation option + - FormView and RelationChildModal merge picked labels into the labels ref + - RelationField choose() writes the option before emit so a remount seeds known + - Vitest names for remount and tab-switch labeling + - Rebuilt modules/boardwalk/dist matching a fresh admin build +affects: [19.6.1 UAT G-19.6.1-T2, embedded admin SPA] + +actuals: + tokens: 2966 + tasks: 2 + commits: 3 +plan_head_before: ba0501c2746e7ea22920701792990537c4729284 +plan_head_after: 51e1a1ea6ed3f5e5aa08d07210452bf3e6622f8a + +tech-stack: + added: [] + patterns: + - "Form hosts persist picked RelationOption rows on the labels ref; remount seeds known from props.labels" + +key-files: + created: [] + modified: + - admin/src/components/form/formContext.ts + - admin/src/views/FormView.vue + - admin/src/components/relation/RelationChildModal.vue + - admin/src/components/form/fields/RelationField.vue + - admin/tests/form/RelationField.test.ts + - admin/tests/form/FormView.test.ts + - modules/boardwalk/dist + +key-decisions: + - "Persist picked relation labels on the form via FORM_REMEMBER_LABEL; keep the one-panel FormGrid (do not v-show inactive tabs)" + +patterns-established: + - "A remounted RelationField reads remembered labels from props.labels, not only server meta.labels from load" + +requirements-completed: [] + +coverage: + - id: D1 + description: "After picking a relation option on a tabbed form, switching tabs and coming back shows the option name, not String(id)" + verification: + - kind: unit + ref: "admin/tests/form/FormView.test.ts#keeps a picked relation label after switching tabs" + status: pass + - kind: unit + ref: "admin/tests/form/RelationField.test.ts#shows a picked label after remount when the form remembered it" + status: pass + human_judgment: false + - id: D2 + description: "modules/boardwalk/dist matches a fresh admin build so the embedded SPA carries the remount-safe labels" + verification: + - kind: other + ref: "scripts/check-admin-dist.sh" + status: pass + - kind: unit + ref: "go test -short ./modules/boardwalk" + status: pass + human_judgment: false + +duration: 4min +completed: 2026-10-22 +status: complete +--- + +# Phase 19.6.1 Plan 05: Persist relation labels across tab remount Summary + +**Picked relation option names persist across FormView tab remount via FORM_REMEMBER_LABEL** + +## Performance + +- **Duration:** 4 min +- **Started:** 2026-10-22T12:38:22Z +- **Completed:** 2026-10-22T12:42:52Z +- **Tasks:** 2 +- **Files modified:** 7 + +## Accomplishments +- Form hosts persist a picked relation option on the labels ref so a remounted RelationField seeds its id→label cache from props.labels +- RelationField `choose()` writes `{value,label,locked}` through `FORM_REMEMBER_LABEL` before emit; empty (null) choice does not remember +- Two named vitest cases prove remount and tab-switch labeling; inactive tabs stay unmounted +- Embedded `modules/boardwalk/dist` rebuilt and matched a fresh admin build + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Persist picked relation labels across FormView tab remount** - `14baafb` (feat) +2. **Task 2: Rebuild the embedded admin dist** - `51e1a1e` (chore) + +**Plan metadata:** recorded after this file is committed. + +_Note: `git rev-list` from `plan_head_before` to HEAD counted 3 commits because a concurrent `docs(19.7)` commit landed between the two task commits. This plan authored 2 production commits._ + +## Files Created/Modified +- `admin/src/components/form/formContext.ts` - exports `FORM_REMEMBER_LABEL` +- `admin/src/views/FormView.vue` - `rememberRelationLabel` + provide; `adopt()` still replaces labels from the server +- `admin/src/components/relation/RelationChildModal.vue` - same helper and provide for a tabbed child form +- `admin/src/components/form/fields/RelationField.vue` - `choose()` remembers a non-null option before emit +- `admin/tests/form/RelationField.test.ts` - remount backstop for a remembered label +- `admin/tests/form/FormView.test.ts` - tab-switch backstop for a picked maker label +- `modules/boardwalk/dist` - rebuilt embed (`index-BtP5LUQA.js` replaces `index-D7ALzCHi.js`; editor hash rename) + +## Decisions Made +- Persist picked labels on the form via `FORM_REMEMBER_LABEL`. Do not keep inactive tab panels mounted with `v-show`. + +## Deviations from Plan + +None - plan executed exactly as written. + +**Total deviations:** 0 auto-fixed +**Impact on plan:** None. + +## Issues Encountered +- A concurrent session committed `d47d618` (`docs(19.7): create phase plans`) between Task 1 and Task 2. Measured `commits:` is 3; this plan's production commits are `14baafb` and `51e1a1e`. Old hashed dist assets were deleted as part of the Vite rebuild (intentional). + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness +- G-19.6.1-T2 is closed in the SPA and the embedded dist. Phase 19.6.1 plans 01–05 are complete; ready for verify-work / UAT recheck of test 2. +- No blockers. Plans 01–04 were not edited. No Go, pact, YAML, or OpenAPI change. + +## Self-Check: PASSED + +- FOUND: admin/src/components/form/formContext.ts +- FOUND: admin/src/views/FormView.vue +- FOUND: admin/src/components/relation/RelationChildModal.vue +- FOUND: admin/src/components/form/fields/RelationField.vue +- FOUND: admin/tests/form/RelationField.test.ts +- FOUND: admin/tests/form/FormView.test.ts +- FOUND: modules/boardwalk/dist/index.html +- FOUND: 14baafb +- FOUND: 51e1a1e + +--- +*Phase: 19.6.1-backend-administrators-screen* +*Completed: 2026-10-22*