20 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12.1-user-plugin-admin-screens | 01 | admin |
|
|
|
|
|
ca9e9c0557 |
71073bc8a2 |
|
|
|
|
|
38min | 2026-10-04 | complete |
Phase 12.1 Plan 01: Framework actions Summary
Declared bulk actions, record actions with an applicability rule, list row states and cabana.ForbiddenError (403), each from the pact contract through a cabana route to the admin SPA, with the neutral acme.roster fixture.
Performance
- Duration: 38 min
- Started: 2026-10-04T21:14:48Z
- Completed: 2026-10-04T21:53:44Z
- Tasks: 4 of 4
- Files modified: 61 source files, plus the rebuilt
modules/boardwalk/dist(66 paths in total)
Accomplishments
- A plugin declares bulk actions in Go and in
config_list.yaml; the admin runs one on the selected rows from a menu. Ids are resolved and locked through the list scope before plugin code runs, and plugin code receives records, never ids. - A plugin declares record actions with their own permissions and an
Appliesrule; the show response offers only the ones that fit the record and the admin, and the route answers 404 outside the form scope and 409 when the action does not apply. - A controller marks list rows with states from a fixed set in one call per page; the SPA shows each as a text badge with a text style.
- Hooks and actions refuse a write with a 403 the admin can read; the transaction rolls back, and the form shows a persistent banner and keeps what was typed.
Contract names (inputs to plans 02 to 05)
All names match "Artifacts this phase produces" in the plan. Additions and exact signatures:
| Name | Shape |
|---|---|
pact.AdminBulkAction |
Name, Label, Confirm string; Permissions []string; Run func(ctx, AdminBulkActionInput) (AdminBulkActionResult, error) |
pact.AdminBulkActionInput |
Records []any |
pact.AdminBulkActionResult |
Message string; Affected int |
pact.HasAdminBulkActions |
AdminBulkActions() []AdminBulkAction |
pact.AdminRecordAction |
Name, Label, Confirm string; Permissions []string; Applies func(ctx, record any) (bool, error); Run func(ctx, AdminRecordActionInput) (AdminRecordActionResult, error) |
pact.AdminRecordActionInput |
RecordID uint64; Record any |
pact.AdminRecordActionResult |
Message string |
pact.HasAdminRecordActions |
AdminRecordActions() []AdminRecordAction |
pact.RowState |
string; RowStateDeleted, RowStateNegative, RowStateDisabled |
pact.ListRowStates |
ListRowStates(ctx, db *gorm.DB, records []any) ([][]RowState, error) |
cabana.ForbiddenError |
Message string; Details map[string]any; pointer receiver Error() |
cabana.BulkActionResult |
Message (json message), Affected (json affected) |
cabana.RecordAction |
Name, Label, Confirm (json confirm, omitempty) |
cabana.BulkAction |
gains Confirm (json confirm, omitempty) |
cabana.CRUDService.BulkAction |
(ctx, cc, name string, in BulkDeleteInput) (BulkActionResult, error) |
cabana.CRUDService.RecordAction |
(ctx, cc, id any, name string) (AdminActionResult, error) |
CompiledController.BulkActions, .RecordActions |
maps keyed by action name |
RecordMeta.Actions |
json actions, omitempty; only the show response fills it |
ListMeta.RowStates |
json row_states, omitempty; keyed by row id as a decimal string |
ListMessages.RowStateDeleted, .RowStateNegative, .RowStateDisabled |
YAML and JSON keys rowStateDeleted, rowStateNegative, rowStateDisabled |
| Swag stubs | cabana.AdminBulkAction, cabana.AdminRecordAction |
| TS aliases | BulkAction, BulkActionResult, RecordAction |
Routes: POST {prefix}/api/v1/{vendor}/{plugin}/{controller}/bulk/{action} and POST {prefix}/api/v1/{vendor}/{plugin}/{controller}/{id}/actions/{action}.
SPA: ListToolbar.vue props bulkActions, bulkBusy, event bulk, exposed focusBulk(); DataTable.vue props rowStates, stateLabels; ListView.vue onBulkAction; FormErrorBanner.vue prop forbidden; RecordActions.vue props source, recordId, actions, disabled and events busy, done, stale, gone.
Phrase keys added in en and pl: backend::lang.list.{bulk_actions, bulk_confirm, bulk_done, bulk_stale, action_forbidden}, backend::lang.form.{action_confirm, action_done, action_stale, forbidden}, backend::lang.messages.list.{row_state_deleted, row_state_negative, row_state_disabled}.
Fixture: modules/cabana/testdata/roster/ (plugin acme.roster, controller acme.roster.people, permissions acme.roster.access and acme.roster.manage), rosterPlugin, rosterController, rosterSpy, newRosterEnv, rosterInsert, rosterLoad in phase121_fixture_test.go. rosterPlugin.fsys replaces the fixture tree for boot-error tests.
Task Commits
- Task 1: declared bulk actions (tracer) -
a879d63(feat) - Task 2: record actions -
e0ccced(feat) - Task 3: row state -
61d5fc7(feat) - Task 4: ForbiddenError -
71073bc(feat)
The tracer gate after Task 1 was the interactive, end-of-phase, automated-only case: the verify was re-run at the commit and passed, so execution continued without a checkpoint.
Measured run times (for VALIDATION.md)
| Command | Time |
|---|---|
go vet ./modules/cabana/ ./modules/pact/ plus the named go test ./modules/cabana -run '^(TestForbidden|TestBulkAction|TestRecordAction|TestRowState|TestPhase10OpenAPIConformance)' |
15 s (the cabana run itself 7 s) |
go test ./modules/cabana -count=1 |
30 to 45 s |
go vet ./... && go test ./... -count=1 |
48 to 50 s |
npm --prefix admin test -- tests/smoke/actions |
6 s |
npm --prefix admin run typecheck && npm --prefix admin test |
39 s |
| Task 1 verify chain end to end (Go subset, SPA, OpenAPI check, dist check, docs checks) | 68 s |
Decisions Made
CRUDService.BulkActionandRecordActiontake the action name, not the action value. The handler resolves it for the permission check and the service resolves it again, so a direct service call cannot run an undeclared action.ListRowStatesis skipped for an empty page.- The bulk outcome (toast, clearing the selection, reload) is handled after the confirm dialog closes. Focus then returns to the menu trigger while the selection still keeps it enabled; the request still runs with the dialog open and busy.
- On a 403 save the forbidden banner replaces the 422 banner. Field messages render on their fields, and messages for keys that are not form fields are listed inside the forbidden banner.
RecordActions.vueemitsdonewith the toast text and does not show it; it shows the 409, 403 and other failure toasts itself. Plan 02 shows the success toast after it reloads the record.- A framework permission denial on save (403 with the framework's fixed text) now also shows as the banner, because UI-SPEC S6 covers every 403 on create or update.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] Route gates outside files_modified
- Found during: Tasks 1 and 2
- Issue: Each new route is also counted by
TestPhase10OpenAPIConformance(needs a real call on theacme.conformfixture),TestPhase10CSRF(fixed count) andTestPhase10Coverage(fixed list of unsafe routes). The plan listed onlysecurity_coverage_test.go. - Fix: Added a bulk action
touchand a record actionpingto the conformance fixture with one case each; raised the CSRF count from 23 to 25 and extended the unsafe-route list. - Files modified: modules/cabana/openapi_conformance_test.go, modules/cabana/phase10_csrf_test.go, modules/cabana/phase10_coverage_test.go
- Committed in:
a879d63,e0ccced
2. [Rule 3 - Blocking] Existing goldens and fixtures for the three new list messages
- Found during: Task 3
- Issue:
ListMessagesgained three required keys, so the default-messages golden inlist_schema_test.goand the typed SPA list-schema fixtures no longer matched. - Fix: Extended the golden and added the keys to
widgets.list-schema.jsonandextension.list-schema.json; the roster fixtures are typed throughadmin/tests/fixtures/typed.ts. - Files modified: modules/cabana/list_schema_test.go, admin/tests/fixtures/typed.ts, admin/tests/fixtures/widgets.list-schema.json, admin/tests/fixtures/extension.list-schema.json
- Committed in:
a879d63,e0ccced,61d5fc7
3. [Rule 1 - Changed behaviour] Existing SPA test expected a toast for a 403 save
- Found during: Task 4
- Issue:
edit.smoke.test.tsused a 403 as its example of "other errors show a toast". UI-SPEC S6 makes a 403 save a banner. - Fix: The test now uses a 409 for the toast path and asserts that no forbidden banner shows; the 403 cases live in
actions.smoke.test.ts. - Files modified: admin/tests/smoke/edit.smoke.test.ts
- Committed in:
71073bc
4. [Rule 2 - Missing critical] Server-side log for a failed action
- Found during: Task 1
- Issue: A bulk or record action's unexpected error becomes the opaque 500; without a log line the cause would be lost (T-12.1-06 expects a server-side log).
- Fix:
actionFailurelogs the controller, action and error once and returns the opaque lifecycle error. - Files modified: modules/cabana/crud.go
- Committed in:
a879d63
Other departures from the plan text
- Docs fences.
docs/backend/admin-controllers.mdshowsexample_actions_test.goonce, in "Bulk actions"; "Record actions" refers to it and adds theconfig_form.yamlfence instead of repeating the whole file. The row state example is its own file,modules/cabana/example_rowstate_test.go, so "Row state" inlists-and-filters.mdhas a short fence. Docs fences can only show whole files for Go methods. - Example controller.
example_actions_test.goregisters two bulk actions (activate,archive), the two the rosterconfig_list.yamldeclares, not one. - Widget refusal case.
phase101_actions_test.gogained arefusedbranch in its lookup action soTestForbiddenSmokecoversrunAction(toolbar and widget actions share it). - Update write.
savenow callstx.Unscoped().Save.TestSoftDeletedRecordSmokepasses with it; I did not run the test against the previoustx.Save, so whether the old path refused the update is not established.
Total deviations: 4 auto-fixed (2 blocking, 1 changed behaviour, 1 missing critical) and 4 departures from the plan text. Impact on plan: No scope added. Every extra file is a test, fixture or example the new routes and keys require.
Issues Encountered
go test ./...failed once after Task 1 on the two route-count gates described above; fixed before the commit.- The focus-return truth needed the outcome handling moved after the dialog closes; a successful run clears the selection, which disables the trigger, and a disabled button cannot take focus.
Known Stubs
| File | Reason |
|---|---|
| admin/src/components/form/RecordActions.vue | Built and tested, not mounted anywhere yet. Plan 12.1-02 mounts it in the preview footer (UI-SPEC S2: record actions render on the preview screen only), and adds the boot rule "recordActions needs a preview". Until then meta.actions is served but no screen shows the buttons. |
Recorded in .planning/WINDOWS.md.
Not verified here
- Relation hook refusals (
RelationBeforeLinkand the six child hooks returningcabana.ForbiddenError) go throughlifecycleFailureand the newRelationService.transaction, but no test drives one. Plan 05 owns coverage. - Visual checks of the bulk menu, badges and banner in a browser (layout, wrapping, dark mode).
scripts/check-phase10.shand the other phase gates were not run; only the commands the plan names.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
- Plan 12.1-02 can mount
RecordActions.vuein the preview footer and build onmeta.actions,ForbiddenErrorand the roster fixture. - No Go module and no npm package changed:
git diff --stat ca9e9c0 -- go.mod go.sum admin/package.json admin/package-lock.jsonis empty. - The application builds against the tree:
go -C ../fonoteka.go build ./... && go -C ../fonoteka.go vet ./...pass. - Commits are local on
master; nothing was pushed.
Self-Check: PASSED
- Created files exist: the roster fixture tree,
phase121_fixture_test.go,phase121_actions_test.go,example_actions_test.go,example_rowstate_test.go, the three SPA components,actions.smoke.test.ts,modules/boardwalk/dist/index.html. - Commits exist:
a879d63,e0ccced,61d5fc7, 71073bc;git rev-list --count ca9e9c0..HEADis 4. - Key links:
requireAjax(s.bulkAction)andrequireAjax(s.recordAction)inhttp.go;lockScopedinCRUDService.BulkAction;onBulkActioninListView.vue;ForbiddenErrorclassified inwriteCRUDError,lifecycleFailureandrunAction. - At
71073bc:go vet ./...andgo test ./... -count=1pass;npm --prefix admin run typecheckandnpm --prefix admin testpass (61 files, 804 tests);scripts/check-admin-openapi.sh --checkandscripts/check-admin-dist.share clean;go test ./cmd/summer -run TestDocsTreeandsummer docs:build --checkpass.
Phase: 12.1-user-plugin-admin-screens Completed: 2026-10-04