docs(12.1-03): complete plugin foundation and Users screen plan
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
---
|
||||
schema_version: 1
|
||||
open_count: 4
|
||||
open_count: 5
|
||||
waived_count: 0
|
||||
fixed_count: 5
|
||||
total_count: 9
|
||||
last_updated: 2026-10-05T10:41:16.319Z
|
||||
total_count: 10
|
||||
last_updated: 2026-10-05T11:31:36.781Z
|
||||
---
|
||||
|
||||
# Broken Windows Ledger
|
||||
@@ -24,6 +24,7 @@ last_updated: 2026-10-05T10:41:16.319Z
|
||||
| 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 | |
|
||||
| 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 | fixed | | 2026-10-04T21:55:30.405Z | 2026-10-05T10:41:16.319Z |
|
||||
| 10 | 12.1 | unmet-truth | .planning/phases/12.1-user-plugin-admin-screens/deferred-items.md | | fonoteka.go: four application tests pin lists the user plugin's admin work changes (TestAdminMetadataFiltering nav list, TestHiddenNeverMarshals user model count, parity TestMigrateSeedsCanonicalGenres and TestRollbackLastIsolatesFonoteka migration ids); with the two from plan 12.1-02 they keep plan 12.1-03's application verify red until plan 12.1-04 updates them | open | | 2026-10-05T11:31:36.781Z | |
|
||||
|
||||
````json
|
||||
[
|
||||
@@ -143,6 +144,19 @@ last_updated: 2026-10-05T10:41:16.319Z
|
||||
"recorded_at": "2026-10-04T21:55:30.405Z",
|
||||
"resolved_at": "2026-10-05T10:41:16.319Z",
|
||||
"milestone": "v1.0"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"kind": "unmet-truth",
|
||||
"phase": "12.1",
|
||||
"file": ".planning/phases/12.1-user-plugin-admin-screens/deferred-items.md",
|
||||
"line": null,
|
||||
"description": "fonoteka.go: four application tests pin lists the user plugin's admin work changes (TestAdminMetadataFiltering nav list, TestHiddenNeverMarshals user model count, parity TestMigrateSeedsCanonicalGenres and TestRollbackLastIsolatesFonoteka migration ids); with the two from plan 12.1-02 they keep plan 12.1-03's application verify red until plan 12.1-04 updates them",
|
||||
"status": "open",
|
||||
"reason": "",
|
||||
"recorded_at": "2026-10-05T11:31:36.781Z",
|
||||
"resolved_at": null,
|
||||
"milestone": "v1.0"
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
---
|
||||
phase: 12.1-user-plugin-admin-screens
|
||||
plan: 03
|
||||
subsystem: admin
|
||||
tags: [sm-user-plugin, cabana, pact, admin, users, permissions, privileged-groups, last-seen, gormigrate]
|
||||
|
||||
requires:
|
||||
- phase: 12.1-user-plugin-admin-screens
|
||||
provides: "plans 01 and 02: bulk and record actions, row state, ForbiddenError, preview, password and form-only fields, FormRules, permissioneditor, writable foreign keys, invisible columns, controller filter options (framework tag v0.1.3, local)"
|
||||
- phase: 12-p-ytarium-api-collections-and-albums
|
||||
provides: "user_groups, users_groups, classes.UserGroupCodes and HasGroupCode (T-12-18)"
|
||||
provides:
|
||||
- "sm-user-plugin admin foundation: permissions, navigation, embedded admin YAML, AdminControllers"
|
||||
- "Users admin screen: controller golem15.user.users (list, filters, row states, preview, form, bulk and record actions, permanent delete)"
|
||||
- "takeover guard for privileged-group members (D-30): classes/privileged.go and config key golem15.user.privileged_groups"
|
||||
- "frontend permission data and resolver: models.PermissionSet, models.FrontendPermission, classes.MergedPermissions, HasPermission, UserHasPermission, FrontendPermissionOptions"
|
||||
- "classes user actions: ActivateUsers, DeactivateUsers, RestoreUsers, BanUsers, UnbanUsers, UnsuspendUser, ThrottleStates, ForceDeleteCleanup"
|
||||
- "users.last_seen written by login and refresh (classes.TouchLastSeen)"
|
||||
- "three additive migrations (users.permissions, users.last_seen, golem15_user_frontend_permissions)"
|
||||
- "plugin admin test harness (admin_harness_test.go)"
|
||||
affects: [12.1-04, 12.1-05, fonoteka.go application tests and schema allow-list, sm-user-plugin host applications]
|
||||
|
||||
# commits/plan_head_* are measured in summercms.go (the pinned root), where this
|
||||
# plan changed planning docs only. The code is in the sibling repository
|
||||
# sm-user-plugin; its measured values are the plugin_* keys.
|
||||
actuals:
|
||||
tokens: 54901 # chars/4 over the plugin diff 0fe5b91..5805c63 (40 files)
|
||||
tasks: 4
|
||||
commits: 0
|
||||
plan_head_before: b8cdb7cc924fa11ceee20ee1ec0383ba5bf87e43
|
||||
plan_head_after: b8cdb7cc924fa11ceee20ee1ec0383ba5bf87e43
|
||||
plugin_repo: sm-user-plugin (../fonoteka.go/plugins/golem15/user)
|
||||
plugin_commits: 10 # git -C <plugin> rev-list --count 0fe5b91..HEAD
|
||||
plugin_head_before: 0fe5b91b632a0ab784f3cecd2d5cf168528062ac
|
||||
plugin_head_after: 5805c6347f6a607287e7e47431419b18c0e0c662
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Admin controllers hold a lazy app accessor (func() *backpack.App): database handle, config, bucket and mailer are read per request"
|
||||
- "Class functions take the caller's transaction handle and start from an empty statement (fresh)"
|
||||
- "A guard that needs the principal and the write transaction lives at the top of the Form hook; refusals are cabana.ForbiddenError"
|
||||
- "Blob deletion and mail sending are registered with lagoon.AfterCommit inside the hook"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users_admin_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/admin_registry.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/request_db.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users/config_list.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users/config_form.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users/config_filter.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/users/_hint.htm
|
||||
- ../fonoteka.go/plugins/golem15/user/models/user/fields.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/models/user/columns.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/models/permission_set.go
|
||||
- ../fonoteka.go/plugins/golem15/user/models/frontend_permission.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/privileged.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/admin_actions.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/permissions.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/last_seen.go
|
||||
- ../fonoteka.go/plugins/golem15/user/admin.go
|
||||
- ../fonoteka.go/plugins/golem15/user/admin_permissions.go
|
||||
- ../fonoteka.go/plugins/golem15/user/admin_navigation.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/202610040001_add_users_permissions.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/202610040002_add_users_last_seen.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/202610040003_create_frontend_permissions.go
|
||||
- ../fonoteka.go/plugins/golem15/user/views/mail/invite.htm
|
||||
- ../fonoteka.go/plugins/golem15/user/views/mail/invite-en.htm
|
||||
- ../fonoteka.go/plugins/golem15/user/admin_harness_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/admin_users_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/last_seen_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/classes/permissions_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/admin_columns_test.go
|
||||
modified:
|
||||
- ../fonoteka.go/plugins/golem15/user/models/user.go
|
||||
- ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/user/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/user/config/config.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/lang/en/lang.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/lang/pl/lang.yaml
|
||||
- ../fonoteka.go/plugins/golem15/user/README.md
|
||||
- ../fonoteka.go/plugins/golem15/user/session_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/api_tokens_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/organisations_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/user_groups_test.go
|
||||
- ../fonoteka.go/plugins/golem15/user/updates/user_session_test.go
|
||||
|
||||
key-decisions:
|
||||
- "The form messages update, saved and deleted, and the list message deleted, name the framework's default keys; only texts with a PHP or UI-SPEC value are plugin keys"
|
||||
- "A user created in the admin gets has_self_set_password true, as a registered user does; only social-login accounts start without it"
|
||||
- "HasPermission treats a leading asterisk as a wildcard on the asked code only, as Winter does; the plan text said both sides"
|
||||
- "PermissionSet.Scan fails on content that is not a JSON object instead of reading it as empty, so a damaged row cannot silently lose its denials"
|
||||
- "The invitation mail carries the login and the activation link and states the configured code lifetime; it never carries the password"
|
||||
- "Bulk and record actions read ban and suspension with one ThrottleStates query through the write transaction"
|
||||
|
||||
patterns-established:
|
||||
- "Plugin admin tests: newAdminEnv boots the plugin alone with the admin API mounted; one environment is live at a time because party's registered plugin value is shared"
|
||||
- "A test that needs mail sets mail.driver log and reads the app logger (mailCapture); the mailer service cannot be replaced after party.Activate"
|
||||
|
||||
requirements-completed: [SC-1, SC-3] # copied from the plan; phase success criteria shared with plan 04, which adds the two remaining screens
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Users list in the admin API: navigation and permission gating, ported columns and filters, search on invisible columns, row states, update of name and email"
|
||||
requirement: SC-1
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUsersTracer"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Takeover guard (D-30): email, password and permanent delete of a privileged-group member need golem15.users.manage_privileged_groups; refusals are 403 and change nothing, a bulk delete as a whole"
|
||||
requirement: SC-3
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminPrivilegedMember"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Bulk actions activate, deactivate, restore, ban, unban; record actions activate, unban, unsuspend; preview status hint; none writes users_groups"
|
||||
requirement: SC-3
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserActions"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "Permanent delete (form and bulk) with cleanup of throttle rows, group memberships and attachments; blobs removed after the commit"
|
||||
requirement: SC-3
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserForceDelete"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "User form: password with confirmation, admin reset that ends sessions, invitation mail, organisation picker, frontend permission editor"
|
||||
requirement: SC-3
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserPassword"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserInvite"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserFormFields"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D6
|
||||
description: "Avatar field bound to the attachment the user API serves, in both directions"
|
||||
requirement: SC-1
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminAvatarSharedWithAPI"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D7
|
||||
description: "Frontend permission resolver and tolerant PermissionSet"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/classes/permissions_test.go#TestMergedPermissions"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "../fonoteka.go/plugins/golem15/user/classes/permissions_test.go#TestPermissionSetScan"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D8
|
||||
description: "users.last_seen written by login and refresh under the five-minute rule, never failing authentication, absent from user payloads; three additive migrations"
|
||||
requirement: SC-1
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/last_seen_test.go#TestLastSeen"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "../fonoteka.go/plugins/golem15/user/updates/admin_columns_test.go#TestAdminColumnsMigrations"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "go -C ../fonoteka.go test ./parity -run '^(TestUserAPINuxtFlows|TestParityCorpus)$' -count=1"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D9
|
||||
description: "The Users screens as an administrator sees them in the admin SPA (list badges, preview hint and footer, form tabs, permission editor), and the plugin README"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "No browser was opened in this plan: the screens are YAML-driven and were exercised through the admin API only. Layout, copy in context and the README's wording need a person."
|
||||
- id: D10
|
||||
description: "The application still boots and passes with the plugin's admin work"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin|TestPhase09|TestPhase10|TestPhase12Threats)' -count=1"
|
||||
status: fail
|
||||
human_judgment: true
|
||||
rationale: "Three tests of that run fail on fixed lists in the application repository (two known from plan 02, TestAdminMetadataFiltering new), and three more outside it; plan 04 owns the catch-up. See Issues Encountered."
|
||||
|
||||
duration: 46min
|
||||
completed: 2026-10-05
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 12.1 Plan 03: Plugin foundation and the Users screen Summary
|
||||
|
||||
**`golem15.user` now has an admin half: the Users screen (list, filters, row states, preview, form, bulk and record actions, permanent delete) on the framework contracts of v0.1.3, a guard that keeps a privileged-group member's email, password and row out of reach without `golem15.users.manage_privileged_groups`, the frontend permission resolver, and `last_seen`. Ten commits in sm-user-plugin, nothing pushed. Six application tests are red until plan 04 updates fixed lists in fonoteka.go.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 46 min
|
||||
- **Started:** 2026-10-05T10:46:28Z
|
||||
- **Completed:** 2026-10-05T11:32:00Z
|
||||
- **Tasks:** 4 of 4
|
||||
- **Files modified:** 40 in sm-user-plugin; 2 planning files in summercms.go besides this summary
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- An administrator with `golem15.users.access_users` opens Users from the navigation, searches (also by surname and IP address, which are not shown), filters by group, registration date and activation, and sees deactivated, banned and not activated users marked.
|
||||
- A row opens the preview with one status hint and the applicable actions (activate, unban, unsuspend); the list offers activate, deactivate, restore, ban, unban and the permanent delete.
|
||||
- The form creates a user with a password and an optional invitation, resets passwords (which ends the user's sessions), picks an organisation, sets the avatar the app shows, and edits frontend permissions.
|
||||
- A member of a privileged group (config `golem15.user.privileged_groups`, default `[admin]`) cannot be taken over by an administrator who lacks `golem15.users.manage_privileged_groups`: a changed email, a submitted password and a permanent delete answer 403 and change nothing.
|
||||
- Any plugin can ask `classes.UserHasPermission` whether a user holds a frontend permission.
|
||||
|
||||
## Plugin commits (sm-user-plugin, branch master, local)
|
||||
|
||||
| Task | Commit | Subject |
|
||||
|------|--------|---------|
|
||||
| 1 | `e22f766` | feat: add the admin columns and the frontend permissions table |
|
||||
| 1 | `b410a7f` | feat: register the Users admin screen with the privileged-member guard |
|
||||
| 1 | `4e17c0d` | test: cover the Users admin list, update and the privileged-member guard |
|
||||
| 1 | `f3f33b8` | style: gofmt the user model and the session test |
|
||||
| 1 | `ebaf3a0` | refactor: spell the five permission codes out in the catalog |
|
||||
| 2 | `970eba7` | feat: user preview, bulk and record actions and the permanent delete |
|
||||
| 3 | `cdea47a` | feat: frontend permissions and the merged-permission resolver |
|
||||
| 3 | `f7ca3c4` | feat: create users with a password and an invitation in the admin |
|
||||
| 4 | `c7d9d7d` | feat: stamp users.last_seen on login and token refresh |
|
||||
| 4 | `5805c63` | docs: describe the Users admin screen, its permissions and the privileged-member rule |
|
||||
|
||||
Plugin head before `0fe5b91`, after `5805c63`; the plugin tree is clean. The submodule pointer in fonoteka.go was not bumped (plan 04) and nothing was pushed in any repository. `classes/privileged.go` and `controllers/users_admin_controller.go` were both added in `b410a7f`, and the controller of that commit already contains the guard.
|
||||
|
||||
**Tracer gate (Task 1).** The slice works end to end (`TestAdminUsersTracer`, `TestAdminPrivilegedMember`, the plugin suite and `TestUserAPINuxtFlows` pass). The task's verify chain is not fully green: its application command fails on fixed lists in fonoteka.go, which this plan may not edit (see Issues Encountered). I continued to Tasks 2 to 4 rather than halt, because nothing in the slice was broken and the fix is outside the plan's write scope; this is a judgement the orchestrator may want to review.
|
||||
|
||||
## Contract names (inputs to plans 04 and 05)
|
||||
|
||||
All names match "Artifacts this phase produces" in the plan. Exact shapes:
|
||||
|
||||
| Name | Shape |
|
||||
|------|-------|
|
||||
| `controllers.AdminControllers` | `(app func() *backpack.App) []pact.AdminController` |
|
||||
| `controllers.PermissionAccessUsers` | `"golem15.users.access_users"` |
|
||||
| `usersAdminController` | implements `AdminPermissioned`, `AdminRecordSource`, `ListExtendQuery`, `FormExtendQuery`, `ListRowStates`, `FormRules`, `FilterOptions`, `FormVirtualFields`, `FormBeforeCreate`, `FormAfterCreate`, `FormBeforeUpdate`, `FormBeforeDelete`, `FormAfterDelete`, `HasAdminBulkActions`, `HasAdminRecordActions`, `AdminPartialData`, `cabana.FieldRelationProvider`, `cabana.PermissionEditorProvider` |
|
||||
| `classes.PermissionManagePrivilegedGroups` | `"golem15.users.manage_privileged_groups"` |
|
||||
| `classes.PrivilegedGroupCodes` | `(cfg *compass.Config) []string`; nil config or missing key gives `[admin]`; a present, empty list gives none |
|
||||
| `classes.IsPrivilegedCode` | `(codes []string, code *string) bool` |
|
||||
| `classes.PrivilegedGroupIDs` | `(ctx, db, codes []string) ([]uint, error)` |
|
||||
| `classes.IsPrivilegedMember` | `(ctx, db, codes []string, userID uint) (bool, error)` |
|
||||
| `classes.ThrottleStates` | `(ctx, db, userIDs []uint) (banned, suspended map[uint]bool, err error)`; the window comes from `classes.ContextWithThrottle` |
|
||||
| `classes.ActivateUsers`, `DeactivateUsers`, `RestoreUsers`, `BanUsers`, `UnbanUsers` | `(ctx, db, users []*models.User) (int, error)`; the int is the number changed |
|
||||
| `classes.UnsuspendUser` | `(ctx, db, userID uint) error` |
|
||||
| `classes.ForceDeleteCleanup` | `(ctx, db, bucket *blob.Bucket, user *models.User) error`; does not delete the user row |
|
||||
| `classes.MergedPermissions` | `(ctx, db, userID uint) (map[string]int, error)` |
|
||||
| `classes.HasPermission` | `(merged map[string]int, code string) bool` |
|
||||
| `classes.UserHasPermission` | `(ctx, db, userID uint, code string) (bool, error)` |
|
||||
| `classes.FrontendPermissionOptions` | `(ctx, db) ([]models.FrontendPermission, error)` |
|
||||
| `classes.TouchLastSeen` | `(ctx, db, userID uint) error` |
|
||||
| `models.PermissionSet` | `map[string]int` with `Scan`, `Value`, `GormDataType`; `models.ParsePermissionSet(raw []byte)` |
|
||||
| `models.User` | new fields `LastSeen`, `Permissions`, read-only `CreatedAt`, `UpdatedAt` (all json "-"); `AttachRelations`, `FilterScopes`, `FilterScope`; constants `models.FilterByGroup`, `models.AvatarField` |
|
||||
|
||||
Plugin methods `AdminFS`, `AdminControllers`, `Permissions`, `Navigation`. The embed list in `admin.go` names every admin file; plan 04 adds its files there.
|
||||
|
||||
### Harness helpers (`admin_harness_test.go`, package `user`)
|
||||
|
||||
`newAdminEnv(t, configure ...func(*compass.Config)) *adminEnv` (fields `h`, `app`, `gdb`, `bucket`, `mail`), `adminAPI(rel)`, `adminUsersPath`, `userPath(id, suffix)`, `adminStamp()`, `adminSessionKey(t)`, constants `permAccessUsers`, `permManagePrivileged`, `adminHarnessSecret`.
|
||||
|
||||
Methods on `adminEnv`: `admin(t, permissions...)` (mints a backend role and user, returns the bearer token; no permission gives an administrator who may open nothing), `call`, `send` (raw body and headers), `expect`, `users(t, token, query)`, `bulk(t, token, action, users...)` (`delete` goes to the built-in bulk delete), `offered(t, token, id)`, `upload`, `mails(to)`, `userLogin`, `seedUser`, `seedGroup`, `join`, `seedThrottle`, `seedAvatar`, `blobExists`, `hangers`, `reload`, `groupIDs`. Free helpers `adminDecode`, `adminInto`, `assertNoSecret`, `createdID`.
|
||||
|
||||
Two rules of use: only the most recently created environment is live (party's registered plugin value is shared, so a later `newAdminEnv` rebinds the controllers to its app), and the harness database is shared by all admin tests, so seed with unique names (`adminStamp`) and narrow lists with a search.
|
||||
|
||||
## Measured run times (for VALIDATION.md)
|
||||
|
||||
| Command | Time |
|
||||
|---------|------|
|
||||
| `go -C ../fonoteka.go vet ./plugins/golem15/user/...` | under 1 s |
|
||||
| `go -C ../fonoteka.go test ./plugins/golem15/user -run '^(TestAdminUsersTracer\|TestAdminPrivilegedMember)$' -count=1` | 6 to 10 s |
|
||||
| `go -C ../fonoteka.go test ./plugins/golem15/user -run '^(TestLastSeen\|TestLogin\|TestRefresh)' -count=1` | 7 s |
|
||||
| the eight `TestAdmin*` tests together | 9 s |
|
||||
| `go -C ../fonoteka.go test ./plugins/golem15/user/... -count=1` | 26 s (package `user` 20 s, `classes` 24 s, `updates` 9 s, in parallel) |
|
||||
| `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin\|TestPhase09\|TestPhase10\|TestPhase12Threats)' -count=1` | 20 s (3 tests fail, see below) |
|
||||
| `go -C ../fonoteka.go test ./parity -run '^(TestUserAPINuxtFlows\|TestParityCorpus)$' -count=1` | 50 s, pass |
|
||||
| `go -C ../fonoteka.go test ./... -count=1` (root module) | 74 s (3 tests fail, see below) |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Form messages `update`, `saved`, `deleted` and the list message `deleted` point at the framework's default keys. The plan lists them in the messages block without naming keys, and neither PHP nor the UI-SPEC has plugin copy for them.
|
||||
- `FormBeforeCreate` sets `has_self_set_password` to true. GORM writes the Go zero value on insert, which would have overridden the column default and made the user API's change-password answer 500 for every admin-created user.
|
||||
- The invitation text says the link activates the account. The PHP text ("choose your password") does not fit: in Go the administrator sets the password and the link is the existing activation link.
|
||||
- `send_invite` sends only when the value is `true` in the body. The default `true` is a schema default the SPA applies; a direct API create without the key sends nothing.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Existing migration tests roll back by position**
|
||||
- **Found during:** Task 1
|
||||
- **Issue:** `TestUserGroupsMigration`, the three tests in `user_session_test.go`, `TestOrganisationsMigration` and `TestAPITokenMigration` count `RollbackLast()` calls or compare a fixed history; three new migrations shifted every count.
|
||||
- **Fix:** counts raised by three and the history list extended; `TestAPITokenMigration` now rolls back its own migration with `RollbackMigration`.
|
||||
- **Files modified:** updates/user_groups_test.go, user_session_test.go, organisations_test.go, api_tokens_test.go
|
||||
- **Committed in:** e22f766
|
||||
|
||||
**2. [Rule 3 - Blocking] Assembling the plugin's routes now needs the admin secret**
|
||||
- **Found during:** Task 1
|
||||
- **Issue:** `TestLoginRouteGroup` failed with "admin.jwt.secret is empty": a plugin with admin controllers makes `surf.BuildRouter` mount the admin API.
|
||||
- **Fix:** the session test config sets `SUMMER_ADMIN__JWT__SECRET`. The same holds for every host application: documented in the README.
|
||||
- **Files modified:** session_test.go
|
||||
- **Committed in:** b410a7f
|
||||
|
||||
**3. [Rule 1 - Bug] GORM did not know the PermissionSet type**
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** every query on `models.User` logged "unsupported data type" and the admin create answered 500.
|
||||
- **Fix:** `PermissionSet.GormDataType()` returns `text`.
|
||||
- **Committed in:** cdea47a
|
||||
|
||||
**4. [Rule 2 - Missing critical] has_self_set_password on admin create**
|
||||
- See Decisions Made. **Committed in:** f7ca3c4
|
||||
|
||||
**5. [Rule 2 - Missing critical] A failed blob removal after a delete is logged**
|
||||
- **Found during:** Task 2
|
||||
- **Fix:** the after-commit callback of `ForceDeleteCleanup` logs a warning with the user id instead of dropping the error.
|
||||
- **Committed in:** 970eba7
|
||||
|
||||
### Other departures from the plan text
|
||||
|
||||
- **Acceptance check "three files match 2026100400".** It prints 4: the shipped `202610040001_create_api_tokens.go` (quick task 261004-rou, after the plan was written) matches too. The three new files are there. The plan's base commit `8a65890` is also one behind the real base `0fe5b91`; the shipped-migration diff is empty against both.
|
||||
- **Migration order.** By file name `202610040001_add_users_permissions.go` sorts before the shipped `202610040001_create_api_tokens.go`, so it sits before it in the slice and in the history. gormigrate applies whatever is missing, so a database that already has the API tokens table migrates normally; only "roll back the last N" counts are affected.
|
||||
- **Wildcards.** `HasPermission` is the line-by-line port: a trailing asterisk works on the asked and on the stored code, a leading asterisk on the asked code only. The plan said "leading on both sides"; Winter does not do that.
|
||||
- **Empty password in a body.** The plan expected `{"password": ""}` with a new name to answer 200. The framework's `confirmed` rule compares before `nullable` is applied, so a lone empty `password` without `password_confirmation` is a 422. Omitting the field (what the SPA does) or sending both empty answers 200; the tests cover those two.
|
||||
- **TestAdminUserFormFields** is an extra test for the organisation picker and the permission editor, which the plan allowed "in an existing or new test".
|
||||
- **Commits.** Task 1 has two small follow-up commits (gofmt, permission code literals for the acceptance grep) and Tasks 2 and 3 commit code and tests together, so every commit is green on its own.
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 5 auto-fixed (1 bug, 2 missing critical, 2 blocking) and the departures above.
|
||||
**Impact on plan:** No scope added in the plugin. No framework file changed; the tag `v0.1.3` is untouched.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
**Six application tests are red against the plugin's working tree.** None is in the plugin; all are fixed lists or counts in fonoteka.go, which this plan does not write to. The application's go.work uses the plugin checkout directly, so they fail now, before any pointer bump.
|
||||
|
||||
| Test | Module | Why | Known before |
|
||||
|------|--------|-----|--------------|
|
||||
| `TestPhase09SecurityRoutes` | plugins/golem15/fonoteka | route list lacks the two plan 01 routes | yes (plan 02) |
|
||||
| `TestPhase10ControllerCopy` | plugins/golem15/fonoteka | message list lacks the plan 01 keys | yes (plan 02) |
|
||||
| `TestAdminMetadataFiltering` | plugins/golem15/fonoteka | expects the developer navigation to be `[fonoteka]`; it is `[fonoteka user]` | new |
|
||||
| `TestHiddenNeverMarshals` | plugins/golem15/fonoteka/classes | pins five user models; `FrontendPermission` is the sixth. With the constant at 6 (through a `go test -overlay` copy) it passes | new |
|
||||
| `TestMigrateSeedsCanonicalGenres`, `TestRollbackLastIsolatesFonoteka` | parity | fixed migration id list lacks the three new ids | new |
|
||||
|
||||
`TestSchemaMatchesPHPSnapshot` also fails, as the plan states, until plan 04 adds the allow-list entry for `golem15_user_frontend_permissions`.
|
||||
|
||||
Consequence for this plan's verification: the command `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin|TestPhase09|TestPhase10|TestPhase12Threats)'` in every task's verify block cannot be green before plan 04. Every other test it selects passes, including `TestPhase12Threats`. Everything else in the verify blocks is green. Recorded in `deferred-items.md` and as WINDOWS entry 10.
|
||||
|
||||
The other workspace modules pass: `plugins/golem15/golem`, `plugins/golem15/feedback`, and `go -C ../fonoteka.go build ./... && vet ./...`.
|
||||
|
||||
## Not verified here
|
||||
|
||||
- No browser: the screens were exercised through the admin API only.
|
||||
- That each guard test fails when its guard is removed (plan 05's removal harness).
|
||||
- A create that rolls back after `FormAfterCreate` sends no invitation: covered by construction (`lagoon.AfterCommit`), tested only through validation failures, which stop before the hook.
|
||||
- The Polish invitation template is rendered by the catalog check at boot but no test reads its text.
|
||||
- `scripts/check-phase*.sh` gates were not run.
|
||||
|
||||
## Open items for plan 04 and plan 05's review
|
||||
|
||||
1. **Application catch-up** (table above), for plan 04.
|
||||
2. **Host applications must set `admin.jwt.secret`.** Any application that loads `golem15.user` now mounts the admin API and fails to start without the secret. This is the price of the plugin having admin screens; it is in the README. Applications that load the plugin without an admin need to know before they update.
|
||||
3. **A damaged `users.permissions` value blocks the user.** `PermissionSet.Scan` returns an error for content that is not a JSON object (by the plan, so denials are never dropped silently). Because the column is on `models.User`, such a row cannot be loaded at all: login, fetch and the admin form fail for that user until the value is repaired in SQL. The cutover import should validate the column.
|
||||
4. **Two readers of `user_groups.permissions` differ.** The existing `classes.UserPermissionGrants` (API token principals) counts `true` and `"true"` as granted; `PermissionSet` skips non-numeric values. A group row holding booleans grants through one and not the other.
|
||||
5. **`send_invite_comment`** keeps the PHP text "containing login and password information" (UI-SPEC: PHP value). The mail carries no password. The copy should probably change; I left the signed-off text alone.
|
||||
6. **Literal labels are logged as missing translations.** Group names (filter choices) and frontend permission labels, tabs and comments pass through the framework's translator, which logs `missing translation key` at warning level for each literal on every request and would translate a value that happens to be a phrase key. Cosmetic, but noisy; a framework matter.
|
||||
7. **Ban and the row without an IP address.** `BanUsers` creates a `user_throttle` row with a NULL address when the user has none. After an unban that row stays and becomes the fallback row for addresses the user never used, so failed attempts from new addresses share one counter. The login code already treated a NULL row this way; noted so the security review sees it.
|
||||
8. **Framework `confirmed` and `nullable`** (see departures): a framework follow-up, not a plugin one.
|
||||
9. T-12.1-26 (restore or activate re-enables a deactivated site admin) is accepted by the plan and unchanged.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None. `golem15.users.access_groups`, `access_settings` and `impersonate_user` are registered without a screen by decision (D-02); plan 04 uses the first.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None beyond the plan's threat model: no route was added (the screen uses the framework's generic admin routes), and the only new write on the user API path is the `last_seen` UPDATE (T-12.1-25).
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required. An application that loads the plugin needs `SUMMER_ADMIN__JWT__SECRET` (item 2 above); fonoteka.go already sets `admin.jwt.secret`.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Plan 12.1-04 can add the groups field, User Groups and Organisations on top of `usersAdminController`, `classes/privileged.go` and the harness, and must update the application's fixed lists, the schema allow-list and the submodule pointer.
|
||||
- No Go module changed: `git -C <plugin> diff 0fe5b91..HEAD -- go.mod go.sum` is empty.
|
||||
- Pending from plan 02, unchanged: push summercms.go `master` and tag `v0.1.3`; the plugin push waits for it.
|
||||
- The demo server on 127.0.0.1:8431 was left alone.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- Created files exist: all 28 listed under key-files.created.
|
||||
- Plugin commits exist: e22f766, b410a7f, 4e17c0d, f3f33b8, ebaf3a0, 970eba7, cdea47a, f7ca3c4, c7d9d7d, 5805c63; `git -C <plugin> rev-list --count 0fe5b91..HEAD` is 10; the plugin tree is clean.
|
||||
- Key links: `HasAdminControllers` asserted in `admin.go`; `classes.ActivateUsers`, `BanUsers` and `UnsuspendUser` called from the controller's actions; `IsPrivilegedMember` behind `FormBeforeUpdate` and `FormBeforeDelete`; `TouchLastSeen` twice in `api_controller.go`; `AttachRelations` on `models.User`.
|
||||
- At 5805c63: `go vet` and `go test ./plugins/golem15/user/... -count=1` pass; the named tests of all four tasks pass; `TestUserAPINuxtFlows` and `TestParityCorpus` pass.
|
||||
- Not green, as described: the application test command of the verify blocks.
|
||||
|
||||
---
|
||||
*Phase: 12.1-user-plugin-admin-screens*
|
||||
*Completed: 2026-10-05*
|
||||
@@ -9,3 +9,12 @@
|
||||
**Why not fixed here:** the fix is two fixed lists in the application repository (fonoteka.go); plan 12.1-02 writes to summercms.go only. No framework change is needed, so the tagged commit is not affected. The same catch-up was done once before (`549840d test(13-06): list the Phase 12.2 cabana admin routes in the Phase 9 route inventory`).
|
||||
**Why the gate missed it:** the plan's application gate `go -C ../fonoteka.go test ./... -count=1` runs the root module only; the go.work plugin modules (`plugins/golem15/{user,fonoteka,golem,feedback}`) are separate modules and are not matched by `./...`.
|
||||
**Suggested owner:** plan 12.1-04 (it already writes the application's parity allow-list entry and submodule pointer) or plan 12.1-05's gate script, which should run every workspace module.
|
||||
|
||||
- Four more application tests fail once the user plugin registers its admin screen, its three migrations and the FrontendPermission model
|
||||
status: open
|
||||
**Found:** plan 12.1-03 Tasks 1 and 3, 2026-10-05. The application's go.work uses the plugin's working tree, so these fail as soon as the plugin commits exist, before any submodule pointer bump.
|
||||
**What:** (1) `TestAdminMetadataFiltering` (`plugins/golem15/fonoteka/admin_metadata_test.go`) expects the developer role's navigation to be exactly `[fonoteka]`; it is now `[fonoteka user]`, because the four Winter permission codes of the user plugin default to the developer role (D-02, D-04). (2) `TestMigrateSeedsCanonicalGenres` and `TestRollbackLastIsolatesFonoteka` (`parity/migrate_test.go`) compare the user plugin's migration history with a fixed id list, which lacks `202610040001_add_users_permissions`, `202610040002_add_users_last_seen` and `202610040003_create_frontend_permissions`. In history order (by id) the first of them sorts before the shipped `202610040001_create_user_api_tokens`. (3) `TestHiddenNeverMarshals` (`plugins/golem15/fonoteka/classes/hidden_marshal_test.go`) pins `expectedUserModels = 5`; the user plugin now registers six models (`FrontendPermission` is new). With the constant at 6 (checked through a `go test -overlay` copy, nothing written to the repository) the test passes, so the marshalling checks themselves hold for the new model and the new `User` fields.
|
||||
**Already known, same run:** `TestSchemaMatchesPHPSnapshot` reports the new table `golem15_user_frontend_permissions` until plan 04 adds its allow-list entry (stated in the 12.1-03 plan).
|
||||
**Why not fixed here:** all of them are fixed lists or counts in the application repository (fonoteka.go). Plan 12.1-03 commits only inside the plugin checkout.
|
||||
**Effect on plan 12.1-03's own verify:** the command `go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin|TestPhase09|TestPhase10|TestPhase12Threats)'` cannot be green before plan 04: it matches `TestAdminMetadataFiltering` and the two tests of the entry above. Every other test that pattern selects passes.
|
||||
**Suggested owner:** plan 12.1-04, together with the entry above.
|
||||
|
||||
Reference in New Issue
Block a user