Files
summercms/.planning/quick/261006-eyj-widget-field-payload-and-data-channel/261006-eyj-SUMMARY.md

11 KiB

phase, plan, subsystem, tags, status, requires, provides, affects, tech-stack, key-files, decisions, metrics, actuals, plan_head_before, plan_head_after
phase plan subsystem tags status requires provides affects tech-stack key-files decisions metrics actuals plan_head_before plan_head_after
quick-261006-eyj 01 admin-extension
cabana
pact
admin-spa
widgets
openapi
complete
Phase 10.1 widget action route and WidgetField.vue
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
modules/pact
modules/cabana
admin SPA (modules/boardwalk/dist)
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
created modified
.swaggo
modules/cabana/widget_payload_test.go
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
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
duration completed
13 min 2026-10-06
tokens tokens_note tasks commits
15208 chars/4 over the realized diff excluding the rebuilt modules/boardwalk/dist bundle (287117 including it) 3 3
964145628a 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).