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