docs(10.1): capture phase context
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# Phase 10.1: Runtime admin extension point - Context
|
||||
|
||||
**Gathered:** 2026-09-28
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Phase 10.1 opens the compiled admin SPA so a plugin can extend lists and forms without a Node rebuild: per-controller JS/CSS (Winter `addJs`/`addCss`), `type: widget` as custom elements (no Vue in plugins), `type: partial` via `html/template` (form fields and a list-header slot), and extra named toolbar actions.
|
||||
|
||||
Acceptance is three real Albums surfaces in `fonoteka.go`, plus a nameless fixture plugin in `summercms.go` for contract tests:
|
||||
|
||||
1. A statistics strip above the Albums list (list chrome, declared in `config_list.yaml`, not a fake form field).
|
||||
2. A `type: widget` on the Albums form that renders a “load from Discogs” button. The click POSTs to a stub; Phase 14 replaces the stub with the real Discogs client. The stub may return a fixture payload that patches YAML-declared `fill` fields so write-back is proven now.
|
||||
3. A third Albums toolbar action (named, registered on the controller) that POSTs to its own stub and toasts.
|
||||
|
||||
Not in scope: the real Discogs HTTP client and jobs (Phase 14); WASM (FW-06 / v2); a dev-mode disk override for plugin assets; Ctrl+K, badge columns, Playwright, user/media navigation (Phase 10 deferred, unchanged).
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Acceptance
|
||||
- **D-01:** Prove the phase on Płytarium Albums, not a toy-only milestone: stats strip on the Albums list, Discogs widget on the Albums form, one extra Albums toolbar button. A nameless fixture plugin in `summercms.go` still covers the framework contract (no Płytarium names in the framework repo).
|
||||
- **D-02:** The Discogs widget button is enabled in this phase. Click hits a stub endpoint that returns a clear not-implemented or fixture payload. Phase 14 replaces the stub. Do not implement the Discogs client here.
|
||||
- **D-03:** The statistics strip is list chrome above `DataTable`, not `type: partial` in `fields.yaml`. Form `type: partial` is still in this phase (boot must allow it); Płytarium may not have a form partial yet — the fixture plugin (or a tiny unused field) proves that path.
|
||||
|
||||
### Widget contract
|
||||
- **D-04:** Plugins ship plain JS custom elements. No Vue SFC, no Vue compiler, no import of admin SPA modules. App developers never need Node (Phase 10 D-04 stands).
|
||||
- **D-05:** The SPA owns HTTP. The widget dispatches a `CustomEvent`; `FieldRenderer` POSTs to the YAML-declared action with the existing cookie and `X-Requested-With` CSRF header. Widget JS must not `fetch` the admin API and must not read the JWT cookie. — **Reversibility:** costly — every widget and the CSRF matrix assume this split.
|
||||
- **D-06:** `fields.yaml` uses Winter-shaped `type: widget` plus a small key set: custom-element tag/path, POST `action`, and `fill: [keys]` for write-back. Unknown keys fail boot (`DisallowUnknownField`). — **Reversibility:** costly — every ported `fields.yaml` that uses a widget writes this shape.
|
||||
- **D-07:** On success the SPA patches only the `fill` keys onto the form model. Phase 10.1 may return a fixture payload so the save path is real; Phase 14 returns Discogs values into the same keys.
|
||||
- **D-08:** The SPA mounts the custom element with `record-id`, field name, locale, and a snapshot of the current `fill` values. No Vue instance, no token, no cookie on the element.
|
||||
|
||||
### Partials and toolbar
|
||||
- **D-09:** Both surfaces ship: list-header partials and form `type: partial`. Phase 9’s boot error for `type: partial` is lifted for a supported partial contract. `type: widget` is added to the allowed form types.
|
||||
- **D-10:** Partials render with `html/template` against a **curated view model** the controller supplies. The template must not receive a raw GORM model or the request. `html/template` escaping stays on; record fields are not trusted HTML.
|
||||
- **D-11:** The Albums stats strip is declared in `config_list.yaml` (e.g. `headerPartial:` naming the template). The controller implements the view-model method. Missing template or unknown YAML key fails boot.
|
||||
- **D-12:** Custom toolbar actions extend the D-14 string list: `toolbar.buttons: [create, delete, discogsSync]`. Unknown names fail boot unless the controller registers that action (label, permission, POST path). Click: SPA POSTs with CSRF, stub returns, toast. `create`/`delete` behavior is unchanged. — **Reversibility:** costly — list YAML and the toolbar compiler grow a registration table.
|
||||
|
||||
### Assets, serving, CSP
|
||||
- **D-13:** Controllers declare JS/CSS with a Go method (Winter `addJs`/`addCss`). `pact.AdminAssets` remains the YAML `embed.FS` — the JS/CSS interface gets a different name (planner). Files live in the plugin embed tree.
|
||||
- **D-14:** Assets load when that controller opens (list or form). Other plugins’ admin JS stay unloaded.
|
||||
- **D-15:** Production and dev both serve from `embed.FS`. No disk-override switch in v1. `air` / `summer watch` rebuilds the binary. — **Reversibility:** reversible
|
||||
- **D-16:** Files are served under the admin prefix, same origin as the SPA, e.g. `{backend.uri}/assets/{vendor}/{plugin}/…`. CSP stays `script-src 'self'`: no `unsafe-inline`, no extra script hosts. Cookie stays HttpOnly, Secure, SameSite=Strict (Phase 10 D-19). — **Reversibility:** costly — CSP hygiene gate and cookie threat model (T-10-01, T-10-16) assume this.
|
||||
|
||||
### Hygiene constraint (partial HTML in the SPA)
|
||||
- **D-17:** Phase 10 forbids unsanitized `v-html` / `innerHTML` (T-10-16, `--hygiene`). Partial HTML still has to appear in the list/form. Researcher/planner must pick a host that does not reopen that threat (dedicated sanitized slot, iframe under the admin prefix, or equivalent). Record data must not become executable HTML.
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact YAML key names (`headerPartial` vs another spelling, widget `path` vs `tag`, action path convention).
|
||||
- JS/CSS capability interface name (must not reuse `pact.AdminAssets`).
|
||||
- Stub JSON envelope, toast copy, and the fixture `fill` payload for the Discogs widget.
|
||||
- Which numbers the Albums stats strip shows (counts the controller already has vs a small new query).
|
||||
- How the form `type: partial` proof is wired if fonoteka has no real form partial (fixture plugin field vs a harmless unused Albums field).
|
||||
- Custom-element tag naming and `customElements.define` timing relative to asset load.
|
||||
- Whether the Discogs widget renders on create, update, or both (record-id may be empty on create).
|
||||
- Exact asset URL layout under `{backend.uri}/assets/`.
|
||||
- Partial-host implementation that satisfies D-17.
|
||||
- Whether the third toolbar action is named `discogsSync` or something else; label/permission strings.
|
||||
|
||||
### Reviewed Todos
|
||||
- Backend admin personal API tokens (deferred Apparatus PersonalApiToken) — keyword-only match on “admin”; not this phase.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Phase 10 contract this phase extends
|
||||
- `.planning/phases/10-admin-vue-spa/10-CONTEXT.md` — D-04 (embed, no Node for app plugins), D-05 (`FieldRenderer` seam), D-14 (`toolbar.buttons` create/delete only; custom actions were deferred here), D-19 (httpOnly cookie because plugin JS will run same-origin). Deferred section named this phase.
|
||||
- `.planning/phases/10-admin-vue-spa/10-RESEARCH.md` — original 10.1 note: `AdminAssets()` / `addJs`/`addCss`, custom elements, `type: partial`, custom toolbar, disk switch (disk switch was rejected in this discussion).
|
||||
- `.planning/phases/10-admin-vue-spa/10-SECURITY-REVIEW.md` — T-10-01 (cookie, no token in JS), T-10-16 (no raw HTML), T-10-SC (lockfile), CSP `script-src 'self'`.
|
||||
- `.planning/phases/10-admin-vue-spa/design/README.md` — `UnsupportedField`, list toolbar, form field chrome the new types must fit.
|
||||
|
||||
### Phase 9 schema pipeline (must change)
|
||||
- `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md` — D-06 allowed types; D-15 `type: partial` is a boot error (Collections editors became `relation-manager`).
|
||||
- `cabana/form_schema.go` — `formFieldTypes`, explicit reject of `type: partial`, unknown types fail boot.
|
||||
- `cabana/list_schema.go` — `toolbar.buttons` compiler (create/delete only; Winter string is a boot error).
|
||||
- `pact/capabilities.go` — `AdminAssets` is the YAML `fs.FS`, not JS/CSS.
|
||||
|
||||
### SPA extension seam
|
||||
- `admin/src/components/form/registry.ts` — `rendererFor` / `UnsupportedField`; comment already points at Phase 10.1.
|
||||
- `admin/src/components/form/FieldRenderer.vue` — mounts the selected control.
|
||||
- `admin/src/components/list/ListToolbar.vue` — renders declared button names.
|
||||
- `admin/src/views/ListView.vue` — splits `create` vs toolbar buttons; insertion point for the list-header partial.
|
||||
|
||||
### Project constraints
|
||||
- `.planning/ROADMAP.md` §Phase 10.1 — inserted after Phase 10; depends on Phase 10.
|
||||
- `.planning/REQUIREMENTS.md` — ADMIN-01..06 (ADMIN-06 complete). Planning should add an ADMIN-07 (or equivalent) for this extension point; do not silently reuse ADMIN-06.
|
||||
- `.planning/PROJECT.md` — compiled plugins only; two-repo split; WASM deferred (`.planning/seeds/wasm-extension-api.md`).
|
||||
- `.planning/research/STACK.md` — stdlib `html/template`; no new Go dependency unless a phase decision names it.
|
||||
|
||||
### Later phase that owns Discogs
|
||||
- `.planning/ROADMAP.md` §Phase 14 — Discogs client, CSV/Discogs jobs. Replaces the 10.1 stubs.
|
||||
|
||||
### PHP reference (read-only)
|
||||
- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/` — Winter `addJs`/`addCss` and any remaining partials (editors is already `relation-manager` in Go).
|
||||
- `../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/` — Go Albums YAML this phase will edit.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `FieldRenderer` + `registry.ts`: register `widget` and `partial` renderers; unknown types still fall back to `UnsupportedField`.
|
||||
- `cabana` form/list compilers: extend allowed types and toolbar action names; keep `DisallowUnknownField` and boot-fail behavior.
|
||||
- `boardwalk` / admin prefix serving: same-origin static files under `{backend.uri}` already exist for the SPA `dist/`.
|
||||
- Cookie + CSRF (`X-Requested-With`) on unsafe admin routes: widget and toolbar POSTs must reuse this, not invent a second auth.
|
||||
- `phrasebook` / `messages:`: toolbar label and toasts should be phrase keys, not hardcoded Polish in the SPA.
|
||||
- Plugin `embed.FS` (`pact.AdminAssets.AdminFS()`): YAML today; JS/CSS/templates should live in the same plugin tree or a sibling embed.
|
||||
|
||||
### Established Patterns
|
||||
- Fail-loud boot on unknown YAML keys and unsupported types.
|
||||
- Framework stays app-agnostic: fixture plugin in `summercms.go`, Albums proof in `fonoteka.go`.
|
||||
- Committed `dist/` + drift check: a new built-in field host (widget/partial chrome) needs a Node rebuild; plugin JS does not.
|
||||
- Optional capability interfaces type-asserted by the consumer (Phase 9/10 hooks).
|
||||
|
||||
### Integration Points
|
||||
- Lift `type: partial` / add `type: widget` in `cabana/form_schema.go`; extend `toolbarButtons` in `cabana/list_schema.go`.
|
||||
- New admin routes for widget action + toolbar action stubs, permission-gated like other Albums writes.
|
||||
- SPA: widget control, partial host (D-17), list-header slot in `ListView`, toolbar renderer for registered extra actions.
|
||||
- Asset routes under `{backend.uri}/assets/...`, loaded when Albums navigation/controller is active.
|
||||
- `fonoteka.go` Albums controller: view model for stats, widget + toolbar stub handlers, `addJs`/`addCss`, YAML edits.
|
||||
- OpenAPI: new action paths must be typed in the framework admin document (Phase 10 D-15); no hand-maintained TS types.
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- The user asked for a **statistics partial above the Albums list** as the real Płytarium proof, then a **custom widget in `fields.yaml`** that is a button to load data from Discogs. It is allowed to be a stub until full Discogs support (Phase 14); the point is that widgets render on forms.
|
||||
- “Above the list” was explicitly distinguished from `type: partial` in `fields.yaml`.
|
||||
- Winter `addJs`/`addCss` is the mental model for the Go method; plugins remain compiled and embedded.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **Phase 14:** Real Discogs client, jobs, and non-stub widget/toolbar payloads.
|
||||
- **Dev-mode disk override** for plugin JS/CSS — considered and rejected for v1 (embed only).
|
||||
- **WASM sandboxed extension API** — FW-06 / v2; not a substitute for this compiled-plugin extension point.
|
||||
- Phase 10 leftovers, still out of scope: Ctrl+K command palette, badge/icon column type, Playwright admin e2e, user/media admin navigation.
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
- Backend admin personal API tokens (deferred Apparatus PersonalApiToken) — matched only on the word “admin”; belongs with admin auth follow-up, not the SPA extension point.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 10.1-runtime-admin-extension-point*
|
||||
*Context gathered: 2026-09-28*
|
||||
@@ -0,0 +1,167 @@
|
||||
# Phase 10.1: Runtime admin extension point - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-09-28
|
||||
**Phase:** 10.1-runtime-admin-extension-point
|
||||
**Areas discussed:** Acceptance target, Widget contract, Partials and custom toolbar actions, Asset load and CSP
|
||||
|
||||
---
|
||||
|
||||
## Acceptance target
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Fixture plugin | Framework test plugin ships widget + partial + custom button | |
|
||||
| Wait for a real plugin | Seams only; first consumer is user/media | |
|
||||
| Fixture now, real consumer later | Fixture proves contract; real plugin is a later port | |
|
||||
| Other: fonoteka statistics + Discogs widget | Real Albums list stats strip and Albums form Discogs widget | ✓ |
|
||||
|
||||
**User's choice:** Add a statistics partial to the fonoteka plugin above the list, and a custom `fields.yaml` widget that is a button to load data from Discogs (stub until Phase 14).
|
||||
**Notes:** “Above the list” is list chrome, not `type: partial` in `fields.yaml`. Screens locked to Albums list + Albums form.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Click hits a stub endpoint | Enabled button, POST, Phase 14 replaces stub | ✓ |
|
||||
| Visible but disabled | No request until Phase 14 | |
|
||||
| Click only runs widget JS | No new backend action | |
|
||||
|
||||
**User's choice:** Stub endpoint.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Albums list + Albums form | One controller owns both proofs | ✓ |
|
||||
| Collections list + Albums form | Two controllers load different assets | |
|
||||
| You decide | Planner picks screens | |
|
||||
|
||||
**User's choice:** Albums list + Albums form.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Defer custom toolbar | create/delete only until a real third action | |
|
||||
| Add one Albums list action now | Third button, stub POST | ✓ |
|
||||
| Fixture-only custom action | No Płytarium toolbar change | |
|
||||
|
||||
**User's choice:** Add one Albums list action now.
|
||||
|
||||
---
|
||||
|
||||
## Widget contract
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| SPA owns HTTP | CustomEvent; FieldRenderer POSTs with CSRF | ✓ |
|
||||
| Tiny same-origin helper | SummerAdmin.request() | |
|
||||
| Widget fetch() itself | Widget must remember X-Requested-With | |
|
||||
|
||||
**User's choice:** SPA owns HTTP.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Patch declared fields | YAML `fill` keys; fixture payload allowed | ✓ |
|
||||
| Toast only | Form unchanged | |
|
||||
| Only this field’s value | Widget bound as one field | |
|
||||
|
||||
**User's choice:** Patch declared fields.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Winter-shaped type: widget | tag/path, action, fill; unknown keys fail boot | ✓ |
|
||||
| type + path only | Action and fill live in Go | |
|
||||
| You decide | Planner picks keys | |
|
||||
|
||||
**User's choice:** Winter-shaped `type: widget`.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Attributes + fill snapshot | record-id, field, locale, current fill values | ✓ |
|
||||
| record-id only | Widget cannot see current name/year | |
|
||||
| You decide | Planner picks attributes | |
|
||||
|
||||
**User's choice:** Attributes + fill snapshot.
|
||||
|
||||
---
|
||||
|
||||
## Partials and custom toolbar actions
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| List-header only | Leave form `type: partial` as a boot error | |
|
||||
| List-header and form type: partial | Both in this phase | ✓ |
|
||||
| You decide | Planner scopes it | |
|
||||
|
||||
**User's choice:** Both.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Curated view model | Typed struct from controller; auto-escaped | ✓ |
|
||||
| Record map + extras | More Winter-like; leak risk | |
|
||||
| You decide | Planner picks data shape | |
|
||||
|
||||
**User's choice:** Curated view model.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| config_list.yaml slot | e.g. headerPartial names the template | ✓ |
|
||||
| Controller Go API only | No YAML key | |
|
||||
| You decide | Planner picks declaration | |
|
||||
|
||||
**User's choice:** YAML slot.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Named action + stub POST | toolbar.buttons string list; controller registers extras | ✓ |
|
||||
| Inline map in YAML | strings or {action, label, confirm} | |
|
||||
| You decide | Planner picks YAML | |
|
||||
|
||||
**User's choice:** Named action + stub POST.
|
||||
|
||||
---
|
||||
|
||||
## Asset load and CSP
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| On controller open | Winter addJs/addCss timing | ✓ |
|
||||
| All registered assets at login | Simpler, more JS in the admin origin | |
|
||||
| Per widget/partial only | Finest grain, define() timing issues | |
|
||||
|
||||
**User's choice:** On controller open.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Embed only | air / summer watch rebuilds; no disk-in-prod | ✓ |
|
||||
| Dev-mode disk override | Original 10.1 note; rejected | |
|
||||
| You decide | Planner picks | |
|
||||
|
||||
**User's choice:** Embed only.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Under admin prefix, script-src 'self' | No unsafe-inline, no extra hosts | ✓ |
|
||||
| Allow nonce inline for widgets | Weaker than current hygiene | |
|
||||
| You decide | Planner picks URL layout | |
|
||||
|
||||
**User's choice:** Prefix + `script-src 'self'`.
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Go method on the controller | addJs/addCss; new interface name | ✓ |
|
||||
| YAML asset list | js:/css: in config | |
|
||||
| You decide | Planner picks Go vs YAML | |
|
||||
|
||||
**User's choice:** Go method.
|
||||
|
||||
---
|
||||
|
||||
## the agent's Discretion
|
||||
|
||||
- Exact YAML key names, JS/CSS interface name, stub payload/toast copy, stats numbers, form-partial proof vehicle, custom-element tag naming, create vs update for the widget, asset URL layout, PartialHost vs T-10-16, toolbar action identifier.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- Phase 14: real Discogs client and non-stub payloads
|
||||
- Dev-mode disk override (rejected for v1)
|
||||
- WASM extension API (v2)
|
||||
- Phase 10 leftovers: Ctrl+K, badge columns, Playwright, user/media nav
|
||||
Reference in New Issue
Block a user