Files
summercms/.planning/phases/12.1-user-plugin-admin-screens/12.1-03-SUMMARY.md

31 KiB

phase, plan, subsystem, tags, requires, provides, affects, actuals, plan_head_before, plan_head_after, plugin_repo, plugin_commits, plugin_head_before, plugin_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 plugin_repo plugin_commits plugin_head_before plugin_head_after tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
12.1-user-plugin-admin-screens 03 admin
sm-user-plugin
cabana
pact
admin
users
permissions
privileged-groups
last-seen
gormigrate
phase provides
12.1-user-plugin-admin-screens 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 provides
12-p-ytarium-api-collections-and-albums user_groups, users_groups, classes.UserGroupCodes and HasGroupCode (T-12-18)
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)
12.1-04
12.1-05
fonoteka.go application tests and schema allow-list
sm-user-plugin host applications
tokens tasks commits
54901 4 0
b8cdb7cc92 b8cdb7cc92 sm-user-plugin (../fonoteka.go/plugins/golem15/user) 10 0fe5b91b632a0ab784f3cecd2d5cf168528062ac 5805c6347f6a607287e7e47431419b18c0e0c662
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
created modified
../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
../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
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
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
SC-1
SC-3
id description requirement verification human_judgment
D1 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 SC-1
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUsersTracer pass
false
id description requirement verification human_judgment
D2 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 SC-3
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminPrivilegedMember pass
false
id description requirement verification human_judgment
D3 Bulk actions activate, deactivate, restore, ban, unban; record actions activate, unban, unsuspend; preview status hint; none writes users_groups SC-3
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserActions pass
false
id description requirement verification human_judgment
D4 Permanent delete (form and bulk) with cleanup of throttle rows, group memberships and attachments; blobs removed after the commit SC-3
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserForceDelete pass
false
id description requirement verification human_judgment
D5 User form: password with confirmation, admin reset that ends sessions, invitation mail, organisation picker, frontend permission editor SC-3
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserPassword pass
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserInvite pass
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminUserFormFields pass
false
id description requirement verification human_judgment
D6 Avatar field bound to the attachment the user API serves, in both directions SC-1
kind ref status
integration ../fonoteka.go/plugins/golem15/user/admin_users_test.go#TestAdminAvatarSharedWithAPI pass
false
id description verification human_judgment
D7 Frontend permission resolver and tolerant PermissionSet
kind ref status
integration ../fonoteka.go/plugins/golem15/user/classes/permissions_test.go#TestMergedPermissions pass
kind ref status
unit ../fonoteka.go/plugins/golem15/user/classes/permissions_test.go#TestPermissionSetScan pass
false
id description requirement verification human_judgment
D8 users.last_seen written by login and refresh under the five-minute rule, never failing authentication, absent from user payloads; three additive migrations SC-1
kind ref status
integration ../fonoteka.go/plugins/golem15/user/last_seen_test.go#TestLastSeen pass
kind ref status
integration ../fonoteka.go/plugins/golem15/user/updates/admin_columns_test.go#TestAdminColumnsMigrations pass
kind ref status
integration go -C ../fonoteka.go test ./parity -run '^(TestUserAPINuxtFlows|TestParityCorpus)$' -count=1 pass
false
id description verification human_judgment rationale
D9 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
true 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 description verification human_judgment rationale
D10 The application still boots and passes with the plugin's admin work
kind ref status
integration go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin|TestPhase09|TestPhase10|TestPhase12Threats)' -count=1 fail
true 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.
46min 2026-10-05 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