147 lines
11 KiB
Markdown
147 lines
11 KiB
Markdown
---
|
|
phase: quick-261006-eyj
|
|
plan: 01
|
|
subsystem: admin-extension
|
|
tags: [cabana, pact, admin-spa, widgets, openapi]
|
|
status: complete
|
|
requires:
|
|
- Phase 10.1 widget action route and WidgetField.vue
|
|
provides:
|
|
- pact.AdminActionInput.Payload (json.RawMessage) and pact.AdminActionResult.Data (any)
|
|
- cabana widget route payload (64 KiB cap) and data (256 KiB cap) pass-through
|
|
- WidgetField.vue payload posting, data attribute and summer-result event
|
|
- root .swaggo type override for json.RawMessage
|
|
affects:
|
|
- modules/pact
|
|
- modules/cabana
|
|
- admin SPA (modules/boardwalk/dist)
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- "swag v1.16.6 type override file .swaggo (replace json.RawMessage any) so a RawMessage field does not drop the struct's other properties"
|
|
- "Action Data encoded once in runAction and embedded as json.RawMessage so the envelope never double-encodes"
|
|
key-files:
|
|
created:
|
|
- .swaggo
|
|
- modules/cabana/widget_payload_test.go
|
|
modified:
|
|
- modules/pact/capabilities.go
|
|
- modules/pact/README.md
|
|
- modules/cabana/actions.go
|
|
- modules/cabana/admin_openapi.go
|
|
- modules/cabana/README.md
|
|
- modules/cabana/phase101_actions_test.go
|
|
- modules/cabana/testdata/extension/assets/js/lookup.js
|
|
- admin/openapi/admin.json
|
|
- admin/src/api/schema.d.ts
|
|
- admin/src/components/form/formContext.ts
|
|
- admin/src/components/form/fields/WidgetField.vue
|
|
- admin/tests/form/WidgetField.test.ts
|
|
- modules/boardwalk/dist/
|
|
- docs/backend/partials-and-widgets.md
|
|
- docs/backend/admin-spa.md
|
|
decisions:
|
|
- "Payload is kept as json.RawMessage end to end (cabana.AdminActionRequest -> pact.AdminActionInput) so the action receives exactly the client's bytes; `null` is a 4-byte payload, not nil, so toolbar and record routes refuse {\"payload\":null} like any other payload"
|
|
- "Data bypasses the fill allowlist by design (T-Q261006-05 accepted) but is encoded once and capped at 256 KiB; larger or unencodable data is an opaque 500 with a server log, never echoed"
|
|
- "The SPA sets the data attribute and dispatches summer-result only on success and only while still mounted; the failure path is untouched"
|
|
metrics:
|
|
duration: 13 min
|
|
completed: 2026-10-06
|
|
actuals:
|
|
tokens: 15208
|
|
tokens_note: chars/4 over the realized diff excluding the rebuilt modules/boardwalk/dist bundle (287117 including it)
|
|
tasks: 3
|
|
commits: 3
|
|
plan_head_before: 964145628adeceb4dc93aae87001c3d900e69651
|
|
plan_head_after: e723c39
|
|
---
|
|
|
|
# Quick 261006-eyj: Widget field payload and data channel Summary
|
|
|
|
A `type: widget` admin field can now send its own JSON payload to its controller action (`summer-action` detail.payload -> body `payload` -> `pact.AdminActionInput.Payload`, 64 KiB cap) and receive structured data back (`pact.AdminActionResult.Data` -> response `data`, 256 KiB cap, not fill-filtered -> element `data` attribute plus a `summer-result` event), with every existing guarantee intact.
|
|
|
|
## What was built
|
|
|
|
**Task 1 (tracer, commit 6af88f9) - server contract end to end**
|
|
- `pact.AdminActionInput` gained `Payload json.RawMessage`; `pact.AdminActionResult` gained `Data any`, both documented in the type comments and the pact README.
|
|
- `cabana.decodeActionRequest` keeps strict decoding and adds a 64 KiB cap on `payload` (422 `validation_failed` on `body`, the action never runs). `widgetAction` assigns `input.Payload = in.Payload` without inspection. `toolbarAction` and `recordAction` refuse any payload (including `null`) with the same 422 they give `record_id`/`values`. `runAction` marshals `Data` once, rejects more than 256 KiB or an encode error with an opaque 500 plus `slog.Error("cabana: admin action data rejected", ...)`, and embeds the raw bytes so the envelope never double-encodes. Fill still passes `onlyFillScalars`.
|
|
- `cabana.AdminActionRequest.Payload` (`json:"payload,omitempty"`) and `cabana.AdminActionResult.Data` (`json:"data,omitempty"`) with swag doc comments; route descriptions updated.
|
|
- New root `.swaggo` with `replace json.RawMessage any`. Confirmed: without it swag emitted `cabana.AdminActionRequest` as a bare object; with it the regenerated `admin/src/api/schema.d.ts` has `payload?: unknown` next to `record_id?: number` and `values?:`, and `cabana.AdminActionResult` has `data?: unknown`, `fill`, `message`.
|
|
- Fixture `lookup` action (phase101_actions_test.go) decodes a payload and echoes it as `Data` next to a deliberately over-wide `Fill`; `"big"` and `"nan"` return rejected data.
|
|
- `TestWidgetPayloadAndData` (6 subtests): byte-exact pass-through and unfiltered data with filtered fill; every JSON value kind plus nil-when-absent; no `data` key when none returned (widget and toolbar); 65536-byte payload passes and 65537 is 422 with no action call; toolbar `{"payload":{}}`/`null`/`1` and record `activate` with `{"payload":1}` are 422 and the person stays inactive; `"big"`/`"nan"` are opaque 500s with no `xxxx` in the body.
|
|
- README/docs: cabana README bullet, route table and API rows; pact README rows; `docs/backend/partials-and-widgets.md` Go-contract paragraph; `docs/backend/admin-spa.md` mentions `.swaggo`.
|
|
|
|
**Task 2 (tdd, commit c6f68e4) - WidgetField.vue**
|
|
- `WIDGET_RESULT_EVENT = 'summer-result'` exported from formContext.ts.
|
|
- The listener reads `event.detail` and forwards the detail's own `payload` property (Object.hasOwn) or `undefined`; `onAction` spreads `payload` into the body only when defined, so payload-less bodies are byte-identical to before. On success, after the fill patch loop and before the toast, the component sets `data` to `JSON.stringify(answer.data)` when the response has an own `data` key (otherwise removes the attribute) and dispatches `summer-result` with `{data, fill, message}`, skipped when unmounted. Failure path unchanged.
|
|
- RED observed first: 7 new assertions failed on behaviour while all 22 existing tests passed; GREEN after the implementation: 89/89 across WidgetField, FormView and the extension smoke tests, `vue-tsc` clean, `modules/boardwalk/dist` rebuilt and matching `scripts/check-admin-dist.sh`.
|
|
|
|
**Task 3 (commit e723c39) - fixture widget and element-contract docs**
|
|
- `lookup.js` is now a reorder widget: button plus `<ol>`, in-memory items replaced from the observed `data` attribute and from a `summer-result` listener on itself, a click reverses the items and dispatches `summer-action` with `detail.payload.order`; textContent only, no HTTP, no innerHTML.
|
|
- `docs/backend/partials-and-widgets.md` gained a "Widget element contract" subsection (attributes, both events, the no-token/no-request rules, textContent guidance, pointer to the fixture). The cabana README bullet names both events.
|
|
|
|
## Verification
|
|
|
|
Commands run and outcomes (all at the final HEAD unless noted):
|
|
|
|
| Command | Result |
|
|
|---|---|
|
|
| `go vet ./...` | pass (after each task) |
|
|
| `go test ./modules/pact/...` | pass |
|
|
| `go test ./modules/cabana/ -run 'TestWidgetPayloadAndData\|TestPhase101Actions\|TestPhase10OpenAPIConformance\|TestRecordActionSmoke' -count=1` | pass (Task 1 gate) |
|
|
| `go test ./... -count=1` | pass, exit 0 (final gate, Docker-backed cabana tests included) |
|
|
| `scripts/check-admin-openapi.sh --check` | pass (after Task 1 and at HEAD) |
|
|
| `go test ./cmd/summer -run TestDocsTree` | pass (after Tasks 1 and 3) |
|
|
| `go run ./cmd/summer docs:build --check` | "no problems found" |
|
|
| `grep -c 'payload?: unknown' admin/src/api/schema.d.ts` | 1 |
|
|
| `grep -A12 '"cabana.AdminActionRequest": {' admin/src/api/schema.d.ts \| grep -c 'record_id?: number'` | 1 (see deviation note on `-A4`) |
|
|
| `npm --prefix admin test -- tests/form/WidgetField.test.ts tests/smoke/extension.smoke.test.ts tests/form/FormView.test.ts` | 89 passed |
|
|
| `npm --prefix admin test` (full suite) | 71 files, 1025 tests passed |
|
|
| `npm --prefix admin run typecheck` | pass |
|
|
| `scripts/check-admin-dist.sh` | matches a fresh build |
|
|
| `go test ./modules/boardwalk/...` | pass |
|
|
| `node --check modules/cabana/testdata/extension/assets/js/lookup.js` | pass |
|
|
| `! grep -q innerHTML modules/cabana/testdata/extension/assets/js/lookup.js` | pass |
|
|
| `test -z "$(git status --porcelain -- modules admin docs cmd internal scripts .swaggo)"` | clean |
|
|
|
|
## Deviations from Plan
|
|
|
|
**1. Verify command context width (Task 1).** The plan's literal check `grep -A4 '"cabana.AdminActionRequest": {' ... | grep -c 'record_id?: number'` returns 0 because openapi-typescript now emits the `Payload` doc comment (four lines) before `payload?: unknown`, pushing `record_id` past the 4-line window. The same check with `-A12` returns 1 and `values?:` is present too; the generated file was not hand-edited. The intent of the check (record_id and values survive the `.swaggo` override) is met.
|
|
|
|
**2. Task 2 TDD cycle committed as one commit.** RED (7 failing behaviour tests, 22 existing passing) was observed before implementing, then GREEN, but both landed in the single commit the plan prescribes ("one commit with formContext.ts, WidgetField.vue, WidgetField.test.ts and modules/boardwalk/dist"), keeping the project's green-at-every-commit rule. No separate `test(...)` commit exists.
|
|
|
|
**3. Fixture length.** `lookup.js` is about 60 lines, slightly over the plan's "roughly 50", to keep the observed-attribute parsing defensive (try/catch on JSON.parse, object filter on items).
|
|
|
|
None of these change behaviour or scope; the plan was otherwise executed as written.
|
|
|
|
## Threat model outcome
|
|
|
|
- T-Q261006-01 (payload DoS): mitigated, test "payload cap".
|
|
- T-Q261006-02 (data DoS): mitigated, test "data over 256 KiB or unencodable is an opaque 500".
|
|
- T-Q261006-03 (payload selecting the record): mitigated, record still loaded through `readScopedRecord`/FormExtendQuery; docs tell actions to re-scope ids in the payload; `TestPhase101Actions` out-of-scope 404s still pass.
|
|
- T-Q261006-04 (toolbar/record tampering): mitigated, test "toolbar and record routes refuse a payload".
|
|
- T-Q261006-05 (data bypasses fill): accepted and documented in README and docs.
|
|
- T-Q261006-06 (XSS via data): mitigated, SPA sets only an attribute string and dispatches an event; fixture and docs use textContent.
|
|
- T-Q261006-07, T-Q261006-SC: accepted; no new dependency, payload not logged.
|
|
|
|
No new threat surface beyond the plan's register.
|
|
|
|
## Known Stubs
|
|
|
|
None.
|
|
|
|
## Commits
|
|
|
|
| Hash | Subject |
|
|
|---|---|
|
|
| 6af88f9 | feat(cabana): widget action payload and data channel (quick-261006-eyj) |
|
|
| c6f68e4 | feat(admin): widget field payload and summer-result data channel (quick-261006-eyj) |
|
|
| e723c39 | docs(cabana): widget element contract and payload fixture (quick-261006-eyj) |
|
|
|
|
No `.planning` files and no co-author or "Generated with" trailers in any commit.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
- `.swaggo`, `modules/cabana/widget_payload_test.go` exist; all listed modified files are in the three commits.
|
|
- Commits 6af88f9, c6f68e4, e723c39 are on `master` (`git rev-list --count 964145628..HEAD` = 3).
|