docs(12.1-01): complete framework actions plan
This commit is contained in:
@@ -649,12 +649,12 @@ Plans:
|
|||||||
5. The new code has unit tests, delivered in the phase's last plan.
|
5. The new code has unit tests, delivered in the phase's last plan.
|
||||||
|
|
||||||
**Open questions (discuss-phase):** impersonate user in or out of scope (security-sensitive); a separate permission for granting the `admin` group (it makes a site admin); whether convert-guest is needed for the application's data.
|
**Open questions (discuss-phase):** impersonate user in or out of scope (security-sensitive); a separate permission for granting the `admin` group (it makes a site admin); whether convert-guest is needed for the application's data.
|
||||||
**Plans:** 5 plans
|
**Plans:** 1/5 plans executed
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
|
|
||||||
**Wave 1**
|
**Wave 1**
|
||||||
- [ ] 12.1-01-PLAN.md — Framework actions (summercms.go): declared bulk actions, record actions, row state and `cabana.ForbiddenError` (403), with READMEs, docs, OpenAPI, TS types, `dist/` and the neutral `acme` fixture
|
- [x] 12.1-01-PLAN.md — Framework actions (summercms.go): declared bulk actions, record actions, row state and `cabana.ForbiddenError` (403), with READMEs, docs, OpenAPI, TS types, `dist/` and the neutral `acme` fixture
|
||||||
|
|
||||||
**Wave 2** *(blocked on Wave 1 completion)*
|
**Wave 2** *(blocked on Wave 1 completion)*
|
||||||
- [ ] 12.1-02-PLAN.md — Framework preview and form seams (summercms.go): preview context, `permissioneditor`, `password`, form virtual fields, per-operation rules, writable foreign key, locked relation options, `invisible` columns, `preset`; ends with the tag v0.1.3
|
- [ ] 12.1-02-PLAN.md — Framework preview and form seams (summercms.go): preview context, `permissioneditor`, `password`, form virtual fields, per-operation rules, writable foreign key, locked relation options, `invisible` columns, `preset`; ends with the tag v0.1.3
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ milestone: v1.0
|
|||||||
current_phase: "12.1"
|
current_phase: "12.1"
|
||||||
current_phase_name: User plugin admin screens
|
current_phase_name: User plugin admin screens
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Phase 12.1 UI-SPEC approved
|
stopped_at: Completed 12.1-01-PLAN.md
|
||||||
last_updated: "2026-10-04T17:41:02.519Z"
|
last_updated: "2026-10-04T21:55:37.374Z"
|
||||||
last_activity: 2026-10-03
|
last_activity: 2026-10-04
|
||||||
last_activity_desc: Phase 14 execution started
|
last_activity_desc: Phase 12.1 execution started
|
||||||
state_head: ea0fc6f191b74f54ac6defcf706075cde83eef40
|
state_head: 71073bc8a2c5a1f2030a49bbe351bf2baad1a480
|
||||||
progress:
|
progress:
|
||||||
total_phases: 22
|
total_phases: 22
|
||||||
completed_phases: 11
|
completed_phases: 11
|
||||||
total_plans: 123
|
total_plans: 123
|
||||||
completed_plans: 118
|
completed_plans: 119
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,14 +24,14 @@ milestone_name: milestone
|
|||||||
See: .planning/PROJECT.md (updated 2026-09-16)
|
See: .planning/PROJECT.md (updated 2026-09-16)
|
||||||
|
|
||||||
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
|
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
|
||||||
**Current focus:** Phase 14 — Domain jobs and external integrations
|
**Current focus:** Phase 12.1 — User plugin admin screens
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 12.1 (User plugin admin screens) — READY TO EXECUTE
|
Phase: 12.1 (User plugin admin screens) — EXECUTING
|
||||||
Plan: 6 of 6
|
Plan: 2 of 5
|
||||||
Status: Ready to execute
|
Status: Ready to execute
|
||||||
Last activity: 2026-10-03 — Phase 14 execution started
|
Last activity: 2026-10-04 — Phase 12.1 execution started
|
||||||
|
|
||||||
Progress: [██████░░░░] 60%
|
Progress: [██████░░░░] 60%
|
||||||
|
|
||||||
@@ -172,6 +172,7 @@ Progress: [██████░░░░] 60%
|
|||||||
| Phase 14 P04 | 69 min | 4 tasks | 144 files |
|
| Phase 14 P04 | 69 min | 4 tasks | 144 files |
|
||||||
| Phase 14 P05 | 100min | 3 tasks | 93 files |
|
| Phase 14 P05 | 100min | 3 tasks | 93 files |
|
||||||
| Phase 14 P06 | 132 min | 4 tasks | 26 files |
|
| Phase 14 P06 | 132 min | 4 tasks | 26 files |
|
||||||
|
| Phase 12.1 P01 | 38min | 4 tasks | 61 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -534,6 +535,11 @@ Recent decisions affecting current work:
|
|||||||
- [Phase 14]: 14-06: the sm-feedback-plugin routes are pinned and fuzzed by the plugin's own tests, so the fonoteka plugin does not require the shared plugin module
|
- [Phase 14]: 14-06: the sm-feedback-plugin routes are pinned and fuzzed by the plugin's own tests, so the fonoteka plugin does not require the shared plugin module
|
||||||
- [Phase 14]: 14-06: every mitigated threat has a removal row (43); --evidence requires one per mitigated threat
|
- [Phase 14]: 14-06: every mitigated threat has a removal row (43); --evidence requires one per mitigated threat
|
||||||
- [Phase 14]: 14-06: two Phase 14 functions are exempt from the 80% function floor with reasons (cover_importer fetch, validateDiscogsInput); internal/pgtest exempt from the package floor
|
- [Phase 14]: 14-06: two Phase 14 functions are exempt from the 80% function floor with reasons (cover_importer fetch, validateDiscogsInput); internal/pgtest exempt from the package floor
|
||||||
|
- [Phase 12.1]: 12.1-01: bulk, record and toolbar/widget actions are three namespaces per controller; create and delete are reserved in each
|
||||||
|
- [Phase 12.1]: 12.1-01: CRUDService.BulkAction and RecordAction take the action name and resolve declared-and-registered themselves
|
||||||
|
- [Phase 12.1]: 12.1-01: a 403 on save is the forbidden banner (not a toast) for every 403, plugin refusal or framework denial
|
||||||
|
- [Phase 12.1]: 12.1-01: plugin writes to a soft-deleted record must use tx.Unscoped(); cabana's update write does
|
||||||
|
- [Phase 12.1]: 12.1-01: RecordActions.vue is unmounted until 12.1-02 adds the preview footer; it emits done with the toast text for its host to show
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -571,6 +577,7 @@ Recent decisions affecting current work:
|
|||||||
| 2 | make admin SPA edit/create and settings forms full width | 2026-09-27 | 2585671 | — |
|
| 2 | make admin SPA edit/create and settings forms full width | 2026-09-27 | 2585671 | — |
|
||||||
| 260928-lf2 | Rewrite module READMEs and root README as professional app-agnostic docs | 2026-09-28 | fafb12f | [260928-lf2-rewrite-module-readmes-and-root-readme-a](./quick/260928-lf2-rewrite-module-readmes-and-root-readme-a/) |
|
| 260928-lf2 | Rewrite module READMEs and root README as professional app-agnostic docs | 2026-09-28 | fafb12f | [260928-lf2-rewrite-module-readmes-and-root-readme-a](./quick/260928-lf2-rewrite-module-readmes-and-root-readme-a/) |
|
||||||
| 261001-ddh | Replace lagoon hardcoded ICU pl-PL database locale with per-query COLLATE option | 2026-10-01 | 037dc53 | [261001-ddh-replace-lagoon-hardcoded-icu-pl-pl-datab](./quick/261001-ddh-replace-lagoon-hardcoded-icu-pl-pl-datab/) |
|
| 261001-ddh | Replace lagoon hardcoded ICU pl-PL database locale with per-query COLLATE option | 2026-10-01 | 037dc53 | [261001-ddh-replace-lagoon-hardcoded-icu-pl-pl-datab](./quick/261001-ddh-replace-lagoon-hardcoded-icu-pl-pl-datab/) |
|
||||||
|
| 261004-rou | Move API-token ownership from Fonoteka to sm-user-plugin | 2026-10-04 | d1abcab | [261004-rou-move-user-api-tokens-from-fonoteka-plugi](./quick/261004-rou-move-user-api-tokens-from-fonoteka-plugi/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
@@ -582,6 +589,6 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-10-04T15:29:20.760Z
|
Last session: 2026-10-04T21:55:36.836Z
|
||||||
Stopped at: Phase 12.1 UI-SPEC approved
|
Stopped at: Completed 12.1-01-PLAN.md
|
||||||
Resume file: .planning/phases/12.1-user-plugin-admin-screens/12.1-UI-SPEC.md
|
Resume file: None
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
schema_version: 1
|
schema_version: 1
|
||||||
open_count: 4
|
open_count: 5
|
||||||
waived_count: 0
|
waived_count: 0
|
||||||
fixed_count: 4
|
fixed_count: 4
|
||||||
total_count: 8
|
total_count: 9
|
||||||
last_updated: 2026-09-30T11:32:37.302Z
|
last_updated: 2026-10-04T21:55:30.405Z
|
||||||
---
|
---
|
||||||
|
|
||||||
# Broken Windows Ledger
|
# Broken Windows Ledger
|
||||||
@@ -23,6 +23,7 @@ last_updated: 2026-09-30T11:32:37.302Z
|
|||||||
| 6 | 10.1 | stub | plugins/golem15/fonoteka/controllers/albums_admin_controller.go | 71 | fonoteka.go: discogsLookup widget action is a D-02 stub answering fixed fill {year: 1977, format: LP}; Phase 14 replaces it with the Discogs client | open | | 2026-09-29T00:27:32.945Z | |
|
| 6 | 10.1 | stub | plugins/golem15/fonoteka/controllers/albums_admin_controller.go | 71 | fonoteka.go: discogsLookup widget action is a D-02 stub answering fixed fill {year: 1977, format: LP}; Phase 14 replaces it with the Discogs client | open | | 2026-09-29T00:27:32.945Z | |
|
||||||
| 7 | 10.1 | stub | plugins/golem15/fonoteka/controllers/albums_admin_controller.go | 82 | fonoteka.go: discogsSync toolbar action is a D-02 stub answering stub_not_implemented and changing nothing; Phase 14 replaces it with the Discogs sync | open | | 2026-09-29T00:27:33.153Z | |
|
| 7 | 10.1 | stub | plugins/golem15/fonoteka/controllers/albums_admin_controller.go | 82 | fonoteka.go: discogsSync toolbar action is a D-02 stub answering stub_not_implemented and changing nothing; Phase 14 replaces it with the Discogs sync | open | | 2026-09-29T00:27:33.153Z | |
|
||||||
| 8 | 11 | skipped-test | parity/broadcast_goldens_test.go | | fonoteka.go: TestBroadcastGoldens/created and /updated t.Skip as pending; Phase 12 turns the recorded PHP created/updated goldens into assertions (album subtree, literal created_at/updated_at and artist id handling) | open | | 2026-09-30T11:32:37.302Z | |
|
| 8 | 11 | skipped-test | parity/broadcast_goldens_test.go | | fonoteka.go: TestBroadcastGoldens/created and /updated t.Skip as pending; Phase 12 turns the recorded PHP created/updated goldens into assertions (album subtree, literal created_at/updated_at and artist id handling) | open | | 2026-09-30T11:32:37.302Z | |
|
||||||
|
| 9 | 12.1 | stub | admin/src/components/form/RecordActions.vue | | RecordActions.vue is built and tested but not mounted; plan 12.1-02 mounts it in the preview footer, until then meta.actions is served and no screen shows the buttons | open | | 2026-10-04T21:55:30.405Z | |
|
||||||
|
|
||||||
````json
|
````json
|
||||||
[
|
[
|
||||||
@@ -129,6 +130,19 @@ last_updated: 2026-09-30T11:32:37.302Z
|
|||||||
"recorded_at": "2026-09-30T11:32:37.302Z",
|
"recorded_at": "2026-09-30T11:32:37.302Z",
|
||||||
"resolved_at": null,
|
"resolved_at": null,
|
||||||
"milestone": "v1.0"
|
"milestone": "v1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"kind": "stub",
|
||||||
|
"phase": "12.1",
|
||||||
|
"file": "admin/src/components/form/RecordActions.vue",
|
||||||
|
"line": null,
|
||||||
|
"description": "RecordActions.vue is built and tested but not mounted; plan 12.1-02 mounts it in the preview footer, until then meta.actions is served and no screen shows the buttons",
|
||||||
|
"status": "open",
|
||||||
|
"reason": "",
|
||||||
|
"recorded_at": "2026-10-04T21:55:30.405Z",
|
||||||
|
"resolved_at": null,
|
||||||
|
"milestone": "v1.0"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
````
|
````
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
phase: 12.1-user-plugin-admin-screens
|
||||||
|
plan: 01
|
||||||
|
subsystem: admin
|
||||||
|
tags: [cabana, pact, admin-spa, bulk-actions, record-actions, row-state, forbidden, vue, openapi]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 10.1-runtime-admin-extension-point
|
||||||
|
provides: pact.HasAdminActions, toolbar and widget action routes, allowAction, runAction
|
||||||
|
- phase: 12.2-admin-form-fields
|
||||||
|
provides: lagoon.Transaction write paths, relation child routes, confirm.ask(request, run)
|
||||||
|
provides:
|
||||||
|
- "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"
|
||||||
|
affects: [12.1-02, 12.1-03, 12.1-04, 12.1-05, sm-user-plugin admin controllers]
|
||||||
|
|
||||||
|
actuals:
|
||||||
|
tokens: 316512 # chars/4 over the whole realized diff; 46665 without dist, admin.json and schema.d.ts
|
||||||
|
tasks: 4
|
||||||
|
commits: 4
|
||||||
|
plan_head_before: ca9e9c055703f25409445f30673ee4df8b46b617
|
||||||
|
plan_head_after: 71073bc8a2c5a1f2030a49bbe351bf2baad1a480
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
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"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- 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
|
||||||
|
modified:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "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"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "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"
|
||||||
|
|
||||||
|
requirements-completed: []
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
- id: D1
|
||||||
|
description: "Declared bulk actions: pact contract, bulkActions YAML, scoped and locked route, per-principal list schema, bulk menu in the SPA"
|
||||||
|
requirement: SC-1
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestBulkActionTracer"
|
||||||
|
status: pass
|
||||||
|
- kind: unit
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestListSchemaBulkActionsBoot"
|
||||||
|
status: pass
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/openapi_conformance_test.go#TestPhase10OpenAPIConformance"
|
||||||
|
status: pass
|
||||||
|
- kind: automated_ui
|
||||||
|
ref: "admin/tests/smoke/actions.smoke.test.ts#bulk actions menu (UI-SPEC S1, D-09)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: true
|
||||||
|
rationale: "Menu placement, wrapping of long labels and Reka collision handling in a real browser are visual; happy-dom does not lay out."
|
||||||
|
- id: D2
|
||||||
|
description: "Declared record actions with Applies: pact contract, recordActions YAML, meta.actions on show, scoped route with 404 and 409, RecordActions.vue"
|
||||||
|
requirement: SC-1
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestRecordActionSmoke"
|
||||||
|
status: pass
|
||||||
|
- kind: unit
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestFormSchemaRecordActionsBoot"
|
||||||
|
status: pass
|
||||||
|
- kind: automated_ui
|
||||||
|
ref: "admin/tests/smoke/actions.smoke.test.ts#record actions (UI-SPEC S2, D-10)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D3
|
||||||
|
description: "Row state: one ListRowStates call per page, meta.row_states with the fixed set, badge labels as list messages, DataTable badges and text styles"
|
||||||
|
requirement: SC-1
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestRowStateSmoke"
|
||||||
|
status: pass
|
||||||
|
- kind: automated_ui
|
||||||
|
ref: "admin/tests/smoke/actions.smoke.test.ts#row state (UI-SPEC S4, D-12)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: true
|
||||||
|
rationale: "Badge colours, strike-through and truncation of a long first cell are visual checks in light and dark mode."
|
||||||
|
- id: D4
|
||||||
|
description: "A soft-deleted record a controller's scopes include can be shown, updated, acted on and permanently deleted through the admin API"
|
||||||
|
requirement: SC-3
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestSoftDeletedRecordSmoke"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
- id: D5
|
||||||
|
description: "cabana.ForbiddenError: 403 with localized message and details from hooks and actions, rollback, opaque 500 for other errors, forbidden banner on save"
|
||||||
|
requirement: SC-3
|
||||||
|
verification:
|
||||||
|
- kind: integration
|
||||||
|
ref: "modules/cabana/phase121_actions_test.go#TestForbiddenSmoke"
|
||||||
|
status: pass
|
||||||
|
- kind: automated_ui
|
||||||
|
ref: "admin/tests/smoke/actions.smoke.test.ts#forbidden save (UI-SPEC S6, D-27)"
|
||||||
|
status: pass
|
||||||
|
human_judgment: false
|
||||||
|
|
||||||
|
duration: 38min
|
||||||
|
completed: 2026-10-04
|
||||||
|
status: 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*
|
||||||
@@ -99,5 +99,5 @@
|
|||||||
"label": "Advance to the next step",
|
"label": "Advance to the next step",
|
||||||
"reason": "Phase 12.1 of 22 · executing"
|
"reason": "Phase 12.1 of 22 · executing"
|
||||||
},
|
},
|
||||||
"updated_at": "2026-10-04T17:41:02.544Z"
|
"updated_at": "2026-10-04T21:55:31.329Z"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user