Files
summercms/.planning/phases/10.1-runtime-admin-extension-point/10.1-02-SUMMARY.md
2026-09-29 02:15:15 +02:00

17 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals plan_head_before plan_head_after tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
10.1-runtime-admin-extension-point 02 ui
vue
admin-spa
custom-elements
openapi-fetch
tailwind
boardwalk
phrasebook
phase provides
10.1-runtime-admin-extension-point 10.1-01 widget, toolbar and partial routes, plugin asset route, schema keys assets/toolbarActions/headerPartial and widget/action/actionLabel/fill/path, typed in schema.d.ts
phase provides
10-admin-vue-spa admin SPA, typed openapi-fetch client, field registry, toast queue, hygiene and dist gates
admin/src/app/pluginAssets.ts (loadScript, loadStyles, activateStyles, loadControllerAssets, assetAllowed, assetPrefix, OWNER_ATTRIBUTE)
formContext InjectionKeys FORM_VALUES, FORM_PATCH, FORM_LOCALE, FORM_ASSETS and the summer-action / 5000 ms widget contract
WidgetField (type widget) and PartialField (type partial), both valueless and group-labelled, rendered on create and update
PartialHost with partialNodes.ts (PARTIAL_TAGS, PARTIAL_DROPPED_TAGS, partialAttrAllowed, renderPartialNodes)
ExtensionFailure box (UI-SPEC S5)
list headerPartial host with refetch after bulk delete and toolbar actions
ListToolbar actions/busyAction props and action event; ListView onAction posting {} to toolbar/{action}
partial style kit (.summer-partial, .summer-stats, .summer-stat, .summer-stat__label, .summer-stat__value) documented with the public --c-* variables in the cabana README
backend::lang.extension.{busy,widget_failed,partial_failed,action_failed} in en and pl
Vite dev proxy for {prefix}/assets
rebuilt modules/boardwalk/dist
10.1-03 application widget
partial and toolbar proof
10.1-04 unit tests and gate
14 Discogs
tokens tasks commits
17900 3 3
c3b76b1afb a5e7dac10f
added patterns
Plugin custom elements are created imperatively in a node Vue never renders children into; data goes in as attributes only and requests come out as a bubbling summer-action event
Server node trees are rebuilt with h() under a client copy of the server allowlist; no string is parsed as markup
Controller assets load when the controller's list or form schema arrives; stylesheet links are owned per controller and toggled on every open
created modified
admin/src/app/pluginAssets.ts
admin/src/components/form/formContext.ts
admin/src/components/form/fields/WidgetField.vue
admin/src/components/form/fields/PartialField.vue
admin/src/components/partial/PartialHost.vue
admin/src/components/partial/partialNodes.ts
admin/src/components/ui/ExtensionFailure.vue
admin/tests/fixtures/extension.form-schema.json
admin/tests/fixtures/extension.list-schema.json
admin/tests/fixtures/extension.partial.json
admin/tests/smoke/extension.smoke.test.ts
admin/src/api/types.ts
admin/src/components/form/registry.ts
admin/src/components/form/FormField.vue
admin/src/components/list/ListToolbar.vue
admin/src/views/FormView.vue
admin/src/views/ListView.vue
admin/src/styles/main.css
admin/vite.config.ts
admin/tests/fixtures/typed.ts
admin/tests/fixtures/lang.json
modules/phrasebook/backend/lang/en/lang.yaml
modules/phrasebook/backend/lang/pl/lang.yaml
modules/cabana/README.md
modules/boardwalk/dist/**
A widget posts only the current values of its fill keys as values, since the server passes nothing else to the action
loadControllerAssets resolves with the failed script URLs; a widget whose element is still undefined after a script failure shows the failure box at once instead of waiting 5000 ms
Stylesheet links are keyed by controller and URL, so two controllers of one plugin sharing a file each keep their own link
The client allowlist also drops the server's removed tags (script, style, svg and so on) with their subtree instead of unwrapping them
A widget or partial field without a label has no label row and no aria-labelledby; FormField renders the label as a span only when one is declared
Empty action messages are not toasted
Extension controls read form state through formContext injections with safe defaults, so settings forms without a provider still render
Smoke tests stub document.head.appendChild so plugin scripts and links never load in happy-dom
ADMIN-07
id description requirement verification human_judgment
D1 A type: widget field loads its controller script, mounts the custom element with attributes only, posts summer-action through the typed client once while busy, patches only returned fill keys without saving, and toasts success or failure ADMIN-07
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#widget field (D-04, D-05, D-07, D-08) pass
false
id description requirement verification human_judgment
D2 Plugin assets load only from {base}/assets/, once per URL with retry after failure, and stylesheet links of other controllers are disabled ADMIN-07
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#plugin assets (D-14, D-16) pass
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#controller stylesheets (D-14, UI-SPEC S6) pass
false
id description requirement verification human_judgment
D3 Header and form partials render the server node tree through the client allowlist with skeleton, empty, failure and keep-on-refetch states; hostile nodes never reach the DOM ADMIN-07
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#list header partial (D-03, D-11, UI-SPEC S1) pass
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#form partial (D-09, UI-SPEC S2) pass
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#partial node allowlist (D-17) pass
false
id description requirement verification human_judgment
D4 Registered toolbar actions render after delete for admins who may run them, post {} with busy state, toast, reload the list and refetch the header partial ADMIN-07
kind ref status
automated_ui admin/tests/smoke/extension.smoke.test.ts#toolbar actions (D-12, UI-SPEC S4) pass
kind ref status
unit admin/tests/list/ListToolbar.test.ts (Phase 10 create/delete behaviour unchanged) pass
false
id description requirement verification human_judgment
D5 Framework strings resolve in en and pl, the embedded dist matches a fresh build and the Phase 10 hygiene rules hold ADMIN-07
kind ref status
unit modules/phrasebook/phase10_test.go#TestPhase10SPAKeysResolve pass
kind ref status
other scripts/check-admin-dist.sh pass
kind ref status
other scripts/check-phase10.sh --hygiene pass
false
id description verification human_judgment rationale
D6 Partial style kit and failure box look native in light and dark mode, labels wrap at 768px with pl copy, plugin module scripts load under CSP script-src 'self' and through the Vite /assets dev proxy
true Visual layout, real module loading and the dev proxy need a real browser; happy-dom neither lays out nor loads module scripts (plan Task 3 human-check, UI-SPEC backstop rows)
17min 2026-09-29 complete

Phase 10.1 Plan 02: Runtime admin extension point (SPA half) Summary

The admin SPA now mounts plugin custom elements as form widgets and bridges their summer-action events to the typed widget POST. It renders header and form partials through a client-side h() allowlist, runs registered toolbar actions, and loads each controller's JS and CSS from {base}/assets/ with per-controller stylesheet scoping. The embedded dist is rebuilt.

Performance

  • Duration: 17 min
  • Started: 2026-09-28T23:56:52Z
  • Completed: 2026-09-29T00:14:07Z
  • Tasks: 3
  • Files modified: 29 (24 source and test files plus the dist)

Accomplishments

  • pluginAssets.ts loads each module script once per URL, and a failed script can retry on a later navigation. It refuses any URL outside {base}/assets/, including dot segments, backslashes and control characters. Stylesheet links belong to one controller, and opening another controller's list or form disables them.
  • WidgetField shows a skeleton while it waits for the controller's scripts and customElements.whenDefined, with a 5000 ms timeout. It then creates the element and sets attributes only: record-id, field-name, locale, fill-values, label and busy-label. fill-values stays in sync as the form changes. On summer-action it posts {record_id, values} through api, ignoring repeat events while busy is set. On success it patches only the declared fill keys the server returned, which makes the form dirty without saving it. On failure it shows a danger toast and sets state="error". A load failure shows the S5 failure box.
  • partialNodes.ts and PartialHost rebuild the server node tree with h() under the server's tag, attribute and URL lists. The first load shows an 80px (header) or 44px (field) skeleton. A refetch keeps the current nodes with aria-busy, zero nodes render nothing, and a failure shows the partial_failed box.
  • type: widget and type: partial are registered as valueless, group-labelled types. They never appear in the save body and they render on create.
  • The list's headerPartial sits between the heading and the list card. It refetches after a bulk delete or a toolbar action, but not on search, filter, sort or page changes.
  • Registered toolbar actions render after Delete as outline buttons and stay enabled whatever the selection. During their POST of {} they are disabled with aria-busy. Success shows a toast, reloads the list and refetches the partial. Failure shows a danger toast.
  • The partial style kit is in main.css. The cabana README documents it together with the public --c-* variables. The backend::lang.extension.* strings exist in en and pl. The Vite dev server proxies {prefix}/assets.

Task Commits

  1. Task 1: widget tracer (assets, form context, WidgetField, strings) - 107d820 (feat)
  2. Task 2: header and form partials, style kit, README - 9df9fae (feat)
  3. Task 3: toolbar actions, per-controller CSS on lists, dev proxy - a5e7dac (feat)

Files Created/Modified

  • admin/src/app/pluginAssets.ts: asset loader with the same-origin prefix check and per-controller stylesheet ownership
  • admin/src/components/form/formContext.ts: form injection keys and the widget event/timeout constants
  • admin/src/components/form/fields/WidgetField.vue: custom-element host and action bridge
  • admin/src/components/form/fields/PartialField.vue: form row wrapper around PartialHost
  • admin/src/components/partial/PartialHost.vue, partialNodes.ts: partial fetch, states and the client allowlist renderer
  • admin/src/components/ui/ExtensionFailure.vue: shared S5 failure box
  • admin/src/components/form/registry.ts, FormField.vue: widget/partial registration, valueless set, groupLabelled, span label
  • admin/src/views/FormView.vue: form context provider and background asset load
  • admin/src/views/ListView.vue, admin/src/components/list/ListToolbar.vue: header partial, toolbar actions, list asset load
  • admin/src/styles/main.css: partial style kit
  • admin/vite.config.ts: /assets dev proxy
  • modules/phrasebook/backend/lang/{en,pl}/lang.yaml: extension: group
  • modules/cabana/README.md: "Partial style kit and plugin CSS variables"
  • admin/tests/smoke/extension.smoke.test.ts and three extension.*.json fixtures
  • modules/boardwalk/dist: rebuilt

Decisions Made

  • A widget sends only its fill keys' current values. The server passes nothing else to the action, so sending more would only expose form data.
  • loadControllerAssets resolves with the list of failed script URLs. If a script failed and the element is still undefined, the widget shows its failure box at once instead of waiting out the 5000 ms timeout.
  • Stylesheet links are keyed by controller and URL. Two controllers of one plugin can share a CSS file without one disabling the other's link. loadControllerAssets creates links before toggling them, so every link's disabled state is set explicitly.
  • The client drops the server's removed-with-subtree tags (script, style, svg and so on) along with their subtree. Other unknown tags are unwrapped. This matches the server's behaviour and never turns script text into visible text.
  • A widget or partial field without a label has no label row and no aria-labelledby.

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Added a shared ExtensionFailure component

  • Found during: Task 1
  • Issue: WidgetField, PartialHost and PartialField all need the S5 box. The plan listed no shared component, and copying the markup three times would let the geometry drift.
  • Fix: Created admin/src/components/ui/ExtensionFailure.vue, which the smoke test imports to satisfy the hygiene rule.
  • Files modified: admin/src/components/ui/ExtensionFailure.vue
  • Committed in: 107d820

2. [Rule 3 - Blocking] Added a FORM_ASSETS injection key

  • Found during: Task 1
  • Issue: WidgetField has to wait for the form controller's scripts, but it knows neither the controller's assets nor the load promise, which FormView only creates after the schema arrives.
  • Fix: Added FORM_ASSETS next to the three planned keys. FormView provides a getter for its load promise.
  • Files modified: admin/src/components/form/formContext.ts, admin/src/views/FormView.vue
  • Committed in: 107d820

3. [Rule 3 - Blocking] Added the extension strings to the test bundle fixture

  • Found during: Task 1
  • Issue: Smoke tests render with tests/fixtures/lang.json, which lacked the new keys, so tests could not assert the Polish copy.
  • Fix: Added the four pl backend::lang.extension.* entries to the fixture.
  • Files modified: admin/tests/fixtures/lang.json
  • Committed in: 107d820

4. [Rule 3 - Blocking] Typed the partial fixture with an assertion

  • Found during: Task 2
  • Issue: TypeScript widens sibling attrs objects in a JSON array with key?: undefined, which the generated {[key]: string} index signature rejects.
  • Fix: extensionPartialFixture uses as instead of an annotation, with a comment explaining why. The assertion still refuses a fixture that does not overlap with the schema type.
  • Files modified: admin/tests/fixtures/typed.ts
  • Committed in: 9df9fae

5. [Rule 1 - Bug] Stylesheet activation order

  • Found during: Task 3
  • Issue: Calling activateStyles before loadStyles left newly created links without an explicit disabled state.
  • Fix: loadControllerAssets now creates the links first and then activates them.
  • Files modified: admin/src/app/pluginAssets.ts
  • Committed in: a5e7dac

Total deviations: 5 auto-fixed (4 blocking, 1 bug) Impact on plan: None changes the D-xx contract. The ListToolbar actions and busyAction props are optional with defaults, so the Phase 10 ListToolbar tests pass unchanged.

Issues Encountered

  • happy-dom tries to fetch appended <script> and <link> elements over the network. The smoke tests stub document.head.appendChild for those two element types, which also lets them assert the exact URLs and links.

Known Stubs

None.

User Setup Required

None. No external service configuration required.

Next Phase Readiness

  • Plan 10.1-03 can build the application widget (golem15-fonoteka-discogs-lookup), the stats header partial using the style kit classes, and the toolbar action against this SPA. Its JS must read label/busy-label and dispatch summer-action.
  • Plan 10.1-04 can unit-test pluginAssets (including the per-controller toggle), partialNodes (partialAttrAllowed, renderPartialNodes), WidgetField timeout and failure branches, and PartialHost state transitions directly.
  • Human check still outstanding (backstop): a real browser against summer serve should confirm that module scripts load under CSP script-src 'self', that the Vite /assets proxy works, and how the pl labels wrap at 768px.

Self-Check: PASSED

  • FOUND: every key-files.created path
  • FOUND commits: 107d820, 9df9fae, a5e7dac
  • Plan verification: npm --prefix admin run typecheck, npm --prefix admin test (49 files, 466 tests), scripts/check-admin-dist.sh, scripts/check-phase10.sh --hygiene, go test ./modules/phrasebook ./modules/boardwalk, and the full go vet ./... && go test ./...: all pass.

Phase: 10.1-runtime-admin-extension-point Completed: 2026-09-29