Files
summercms/.planning/phases/12.1-user-plugin-admin-screens/12.1-01-SUMMARY.md
2026-10-04 23:55:37 +02:00

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
cabana
pact
admin-spa
bulk-actions
record-actions
row-state
forbidden
vue
openapi
phase provides
10.1-runtime-admin-extension-point pact.HasAdminActions, toolbar and widget action routes, allowAction, runAction
phase provides
12.2-admin-form-fields lagoon.Transaction write paths, relation child routes, confirm.ask(request, run)
pact.HasAdminBulkActions / AdminBulkAction / AdminBulkActionInput / AdminBulkActionResult
pact.HasAdminRecordActions / AdminRecordAction / AdminRecordActionInput / AdminRecordActionResult
pact.ListRowStates, pact.RowState with RowStateDeleted, RowStateNegative, RowStateDisabled
cabana.ForbiddenError (403), cabana.BulkActionResult, cabana.RecordAction
cabana.CRUDService.BulkAction and CRUDService.RecordAction
config_list.yaml bulkActions and messages.rowState*; config_form.yaml recordActions
POST .../{controller}/bulk/{action} and POST .../{controller}/{id}/actions/{action}
SPA: BulkActionsMenu.vue, RowStateBadges.vue, RecordActions.vue, forbidden banner
neutral acme.roster fixture (modules/cabana/testdata/roster) and newRosterEnv
12.1-02
12.1-03
12.1-04
12.1-05
sm-user-plugin admin controllers
tokens tasks commits
316512 4 4
ca9e9c0557 71073bc8a2
added patterns
Three action namespaces per controller (toolbar and widget, bulk, record); create and delete reserved in each
Service-level transaction helper that localizes a ForbiddenError leaving the transaction
Row state as typed list meta (meta.row_states), never as row data
Bulk outcome handled after the confirm dialog closes, so focus returns to an enabled trigger
created modified
modules/cabana/testdata/roster/
modules/cabana/phase121_fixture_test.go
modules/cabana/phase121_actions_test.go
modules/cabana/example_actions_test.go
modules/cabana/example_rowstate_test.go
admin/src/components/list/BulkActionsMenu.vue
admin/src/components/list/RowStateBadges.vue
admin/src/components/form/RecordActions.vue
admin/tests/smoke/actions.smoke.test.ts
admin/tests/fixtures/roster.list-schema.json
admin/tests/fixtures/roster.list.json
admin/tests/fixtures/roster.record.json
modules/pact/capabilities.go
modules/cabana/actions.go
modules/cabana/crud.go
modules/cabana/query.go
modules/cabana/http.go
modules/cabana/extension.go
modules/cabana/list_schema.go
modules/cabana/form_schema.go
modules/cabana/messages.go
modules/cabana/admin_openapi.go
admin/src/views/ListView.vue
admin/src/views/FormView.vue
admin/src/components/list/ListToolbar.vue
admin/src/components/list/DataTable.vue
admin/src/components/form/FormErrorBanner.vue
CRUDService.BulkAction and RecordAction take the action name and resolve it themselves, so the exported service enforces declared-and-registered too
ListRowStates is not called for an empty page
A 403 on save shows only the forbidden banner; the 422 banner is not shown next to it, and messages for keys that are not fields are listed inside it
RecordActions.vue emits done with the toast text and leaves showing it to its host
The update write is tx.Unscoped().Save, so a record loaded through a scope that includes soft-deleted rows is written through the same scope
Plugin write code on a soft-deleted record uses tx.Unscoped(); documented on the Lists and filters page
A new admin route needs five test entries: phase09Routes, phase09ProtectedCalls, the conformance case, the CSRF count and the unsafe-route list
id description requirement verification human_judgment rationale
D1 Declared bulk actions: pact contract, bulkActions YAML, scoped and locked route, per-principal list schema, bulk menu in the SPA SC-1
kind ref status
integration modules/cabana/phase121_actions_test.go#TestBulkActionTracer pass
kind ref status
unit modules/cabana/phase121_actions_test.go#TestListSchemaBulkActionsBoot pass
kind ref status
integration modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance pass
kind ref status
automated_ui admin/tests/smoke/actions.smoke.test.ts#bulk actions menu (UI-SPEC S1, D-09) pass
true Menu placement, wrapping of long labels and Reka collision handling in a real browser are visual; happy-dom does not lay out.
id description requirement verification human_judgment
D2 Declared record actions with Applies: pact contract, recordActions YAML, meta.actions on show, scoped route with 404 and 409, RecordActions.vue SC-1
kind ref status
integration modules/cabana/phase121_actions_test.go#TestRecordActionSmoke pass
kind ref status
unit modules/cabana/phase121_actions_test.go#TestFormSchemaRecordActionsBoot pass
kind ref status
automated_ui admin/tests/smoke/actions.smoke.test.ts#record actions (UI-SPEC S2, D-10) pass
false
id description requirement verification human_judgment rationale
D3 Row state: one ListRowStates call per page, meta.row_states with the fixed set, badge labels as list messages, DataTable badges and text styles SC-1
kind ref status
integration modules/cabana/phase121_actions_test.go#TestRowStateSmoke pass
kind ref status
automated_ui admin/tests/smoke/actions.smoke.test.ts#row state (UI-SPEC S4, D-12) pass
true Badge colours, strike-through and truncation of a long first cell are visual checks in light and dark mode.
id description requirement verification human_judgment
D4 A soft-deleted record a controller's scopes include can be shown, updated, acted on and permanently deleted through the admin API SC-3
kind ref status
integration modules/cabana/phase121_actions_test.go#TestSoftDeletedRecordSmoke pass
false
id description requirement verification human_judgment
D5 cabana.ForbiddenError: 403 with localized message and details from hooks and actions, rollback, opaque 500 for other errors, forbidden banner on save SC-3
kind ref status
integration modules/cabana/phase121_actions_test.go#TestForbiddenSmoke pass
kind ref status
automated_ui admin/tests/smoke/actions.smoke.test.ts#forbidden save (UI-SPEC S6, D-27) pass
false
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 Applies rule; 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

  1. Task 1: declared bulk actions (tracer) - a879d63 (feat)
  2. Task 2: record actions - e0ccced (feat)
  3. Task 3: row state - 61d5fc7 (feat)
  4. 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.BulkAction and RecordAction take 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.
  • ListRowStates is 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.vue emits done with 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 the acme.conform fixture), TestPhase10CSRF (fixed count) and TestPhase10Coverage (fixed list of unsafe routes). The plan listed only security_coverage_test.go.
  • Fix: Added a bulk action touch and a record action ping to 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: ListMessages gained three required keys, so the default-messages golden in list_schema_test.go and the typed SPA list-schema fixtures no longer matched.
  • Fix: Extended the golden and added the keys to widgets.list-schema.json and extension.list-schema.json; the roster fixtures are typed through admin/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.ts used 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: actionFailure logs 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.md shows example_actions_test.go once, in "Bulk actions"; "Record actions" refers to it and adds the config_form.yaml fence instead of repeating the whole file. The row state example is its own file, modules/cabana/example_rowstate_test.go, so "Row state" in lists-and-filters.md has a short fence. Docs fences can only show whole files for Go methods.
  • Example controller. example_actions_test.go registers two bulk actions (activate, archive), the two the roster config_list.yaml declares, not one.
  • Widget refusal case. phase101_actions_test.go gained a refused branch in its lookup action so TestForbiddenSmoke covers runAction (toolbar and widget actions share it).
  • Update write. save now calls tx.Unscoped().Save. TestSoftDeletedRecordSmoke passes with it; I did not run the test against the previous tx.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 (RelationBeforeLink and the six child hooks returning cabana.ForbiddenError) go through lifecycleFailure and the new RelationService.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.sh and 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.vue in the preview footer and build on meta.actions, ForbiddenError and 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.json is 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..HEAD is 4.
  • Key links: requireAjax(s.bulkAction) and requireAjax(s.recordAction) in http.go; lockScoped in CRUDService.BulkAction; onBulkAction in ListView.vue; ForbiddenError classified in writeCRUDError, lifecycleFailure and runAction.
  • At 71073bc: go vet ./... and go test ./... -count=1 pass; npm --prefix admin run typecheck and npm --prefix admin test pass (61 files, 804 tests); scripts/check-admin-openapi.sh --check and scripts/check-admin-dist.sh are clean; go test ./cmd/summer -run TestDocsTree and summer docs:build --check pass.

Phase: 12.1-user-plugin-admin-screens Completed: 2026-10-04