docs(19.6.1-05): complete persist relation labels plan

This commit is contained in:
Jakub Zych
2026-10-22 14:43:34 +02:00
parent 51e1a1ea6e
commit 007c68bab2
4 changed files with 476 additions and 24 deletions

View File

@@ -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 | - |

View File

@@ -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

View File

@@ -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`.
<objective>
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.
</objective>
<execution_context>
@~/.claude/msd-core/workflows/execute-plan.md
@~/.claude/msd-core/templates/summary.md
</execution_context>
<context>
@.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.
<interfaces>
- RelationField.vue: `known` Record<number,string> 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.
</interfaces>
</context>
<tasks>
<task type="tracer">
<name>Task 1: Persist picked relation labels across FormView tab remount</name>
<precondition>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.</precondition>
<reversibility rating="reversible">SPA-only inject and label merge; undo is a revert. No API or on-disk format change.</reversibility>
<files>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</files>
<read_first>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</read_first>
<action>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&lt;(field: string, option: RelationOption) =&gt; void&gt;` (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 &amp; 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 &amp; 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).
</action>
<verify>
<automated>npm --prefix admin run test -- tests/form/RelationField.test.ts tests/form/FormView.test.ts</automated>
<fails_when>non-zero exit, or either new it() name is missing from the run output</fails_when>
<automated>npm --prefix admin run typecheck</automated>
<fails_when>non-zero exit</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Rebuild the embedded admin dist</name>
<precondition>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).</precondition>
<reversibility rating="reversible">Generated embed; undo is a dist revert.</reversibility>
<files>modules/boardwalk/dist</files>
<read_first>scripts/check-admin-dist.sh, admin/package.json</read_first>
<action>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.</action>
<verify>
<automated>scripts/check-admin-dist.sh &amp;&amp; go test -short ./modules/boardwalk</automated>
<fails_when>non-zero exit, or check-admin-dist reports stale dist</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>The embedded admin build matches the Task 1 SPA, so a `scripts/dev.sh` rebuild of sm-demo-app picks up the fix.</done>
</task>
</tasks>
<threat_model>
## 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 |
</threat_model>
<verification>
- 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.
</verification>
<success_criteria>
- 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.
</success_criteria>

View File

@@ -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*