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, app_repo, app_commits, app_head_before, app_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 | app_repo | app_commits | app_head_before | app_head_after | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12.1-user-plugin-admin-screens | 04 | admin |
|
|
|
|
|
fcc86ac45e |
fcc86ac45e |
sm-user-plugin (../fonoteka.go/plugins/golem15/user) | 4 | 5805c6347f6a607287e7e47431419b18c0e0c662 | 469315510809f703fc315aceaa9ef902794a0237 | fonoteka.go (../fonoteka.go) | 3 | d1abcab | 35a727f1ddfde0a443cf203193bf3705fe3158a1 |
|
|
|
|
|
|
32min | 2026-10-05 | complete |
Phase 12.1 Plan 04: Groups field, User Groups and Organisations Summary
An administrator now sets a user's groups from the user form, manages user groups with their frontend permissions, and manages organisations with their members. Making or unmaking a member of a privileged group (default code admin), and creating, re-coding or deleting such a group, needs golem15.users.manage_privileged_groups on the server. The application repository records the plugin head in a local commit and every workspace module passes. Nothing was pushed.
Performance
- Duration: 32 min
- Started: 2026-10-05T11:36:42Z
- Completed: 2026-10-05T12:09:00Z
- Tasks: 4 of 4, plus the application test-list catch-up the orchestrator assigned
- Files modified: 29 in sm-user-plugin, 6 test files in fonoteka.go besides the submodule pointer, 2 planning files in summercms.go besides this summary
Accomplishments
- The user form has a Groups field. It is the only place a membership changes; a save that does not send the field leaves the memberships alone.
- For an administrator without
golem15.users.manage_privileged_groupsthe privileged groups are locked in that field. A save that adds or removes one answers 403 withusers.privileged_group_forbidden, marksgroups, and writes nothing: no column of the user and no pivot row. This holds on create too. - User Groups is a screen behind
golem15.users.access_groups: name, code, creation date and member count in the list; name, code, description and a checkbox permission editor in the form; a delete that takes the group's memberships with it and leaves the members. - Organisations is a screen behind
golem15.users.access_users: name, slug (filled from the name when empty), description, avatar, and a Members tab that adds and removes users. - The application's six stale test lists are up to date, the schema allow-list names the new table, and the submodule pointer records the plugin head.
Commits
sm-user-plugin (branch master, local)
| Task | Commit | Subject |
|---|---|---|
| 1 | 9611708 |
feat: manage a user's groups from the user form, privileged groups locked |
| 2 | a399287 |
feat: User Groups admin screen with the users count and privileged codes protected |
| 3 | 58595fe |
feat: Organisations admin screen with the avatar and the members manager |
| 3 | 4693155 |
docs: describe the User Groups and Organisations screens and the privileged-group rules |
Plugin head before 5805c63, after 469315510809f703fc315aceaa9ef902794a0237. The tree is clean and 14 commits ahead of its origin. The field, the pivot model and the lock provider are all in 9611708; its parent has no AdminRelationLocks.
fonoteka.go (branch master, local)
| Purpose | Commit | Subject |
|---|---|---|
| Test lists (framework v0.1.3) | ca746fc |
test(12.1-04): list the Phase 12.1 cabana routes and row-state messages in the admin inventories |
| Test lists (user plugin) | b35442a |
test(12.1-04): expect the user plugin's admin navigation, models and migrations |
| Task 4 | 35a727f |
build: bump sm-user-plugin (admin screens for users, groups and organisations) |
Application head before d1abcab, after 35a727f1ddfde0a443cf203193bf3705fe3158a1, 3 commits ahead of its origin. 35a727f changes exactly parity/schema_diff_test.go and plugins/golem15/user; go.mod is unchanged.
summercms.go
Framework head fcc86ac45e1011c9bcb243cac965c8af44311ea1; the tag v0.1.3 (df5cace) is its ancestor. No framework file changed. Planning docs only.
Tracer gate (Task 1). Interactive, end-of-phase, automated-only: the verify chain was re-run at the commit and passed, so execution continued without a checkpoint. It could pass only because the application's stale lists were fixed first (see below).
Publication state and pending steps
| Item | Value |
|---|---|
Plugin remote head (git ls-remote origin refs/heads/master) at the start of Task 4 |
0fe5b91b632a0ab784f3cecd2d5cf168528062ac |
| The same at the end of Task 4 | 0fe5b91b632a0ab784f3cecd2d5cf168528062ac |
Framework tag on origin (git ls-remote --tags origin v0.1.3) |
not listed |
| Pushed by this plan | nothing, in any repository |
The pointer in fonoteka.go names a plugin commit that is not on the plugin's origin. That is safe only while fonoteka.go itself is unpushed. Pending, in this order:
- Push framework
masterandv0.1.3(pending since plan 02, the user's step). - Push sm-user-plugin: plan 05 Task 3, after the security review, and only when
git ls-remote --tags origin v0.1.3lists the tag. - Push fonoteka.go: the user, after step 2.
Application test lists (orchestrator scope)
Each failure was confirmed to be a stale list before the list changed.
| Test | Cause confirmed | Change |
|---|---|---|
TestPhase09SecurityRoutes |
The two unexpected routes are the plan 01 bulk action and record action routes; both are raw and carry the backend guard, which the test asserts for every listed route |
two entries in phase09AdminRoutes |
TestPhase10ControllerCopy |
The list messages gained the three row-state keys with resolved Polish defaults | three keys in phase10ListMessageKeys. The form half reads named keys only and needed no change |
TestAdminMetadataFiltering |
The developer role sees [fonoteka user] because the four Winter user codes default to that role. The same test's publisher half still passes with an empty list, so the user item is shown only to a principal who holds a permission for it |
expectation [fonoteka user] |
TestHiddenNeverMarshals |
The count was 5; the user plugin registers 7 models: FrontendPermission (plan 03) and the UsersGroup pivot (this plan). The marshalling checks pass for all of them |
expectedUserModels = 7 |
TestMigrateSeedsCanonicalGenres, TestRollbackLastIsolatesFonoteka |
The user plugin's history has the seven earlier ids plus the three additive ones of plan 03 | count 7 becomes 10 (three places) |
None was a real defect. deferred-items.md has both entries resolved and WINDOWS entry 10 is marked fixed.
The two list commits precede the pointer commit, so at ca746fc and b35442a the recorded pointer is still the old plugin head. The workspace builds from the plugin's working tree, so this affects only someone who checks out those two commits with the submodule at the recorded pointer. The order was chosen so the root module suite is green at the pointer commit, which Task 4 requires.
Test results per module (at plugin 4693155, application 35a727f, framework fcc86ac)
| Module | vet | test | Time |
|---|---|---|---|
fonoteka.go root (fonoteka, parity) |
pass | pass | 86 s |
plugins/golem15/user (3 packages) |
pass | pass | 28 s |
plugins/golem15/fonoteka (10 packages) |
pass | pass | 121 s |
plugins/golem15/golem (5 packages) |
pass | pass | 9 s |
plugins/golem15/feedback (2 packages) |
pass | pass | 12 s |
| summercms.go (36 packages with tests) | pass | pass, no FAIL line |
60 s |
Other measured times, for VALIDATION.md:
| Command | Time |
|---|---|
go -C ../fonoteka.go test ./parity -run '^(TestSchemaMatchesPHPSnapshot|TestUserAPINuxtFlows|TestParityCorpus)$' -count=1 -v |
54 s, 3 of 3 pass |
go -C ../fonoteka.go test ./plugins/golem15/user -run '^(TestAdminPrivilegedGroups|TestAdminUserGroupsField)$' -count=1 |
6 to 7 s |
go -C ../fonoteka.go test ./plugins/golem15/user -run '^(TestAdminGroups|TestAdminPrivilegedGroups)$' -count=1 |
7 s |
go -C ../fonoteka.go test ./plugins/golem15/user -run '^(TestAdminOrganisations|TestAdminOrganisationMembers)$' -count=1 |
6 s |
go -C ../fonoteka.go test ./plugins/golem15/fonoteka -run '^(TestAdmin|TestPhase09|TestPhase10|TestPhase12Threats)' -count=1 |
14 s |
| Task 1 verify chain end to end | 61 s |
Contract names (inputs to plan 05)
| Name | Shape |
|---|---|
usersAdminController.AdminRelationLocks |
(ctx, field string) (cabana.RelationLock, error); for groups the privileged group ids and the message golem15.user::lang.users.privileged_group_forbidden, empty for an administrator who passes cabana.Allows for the extra permission and for every other field |
models.UsersGroup |
UserID (user_id), UserGroupID (user_group_id), both primary key; table users_groups |
controllers.PermissionAccessGroups |
"golem15.users.access_groups" |
usergroupsAdminController |
ID golem15.user.usergroups; implements AdminPermissioned, AdminRecordSource, ListExtendQuery, FormBeforeCreate, FormBeforeUpdate, FormBeforeDelete, FormAfterDelete, cabana.PermissionEditorProvider |
organisationsAdminController |
ID golem15.user.organisations; implements AdminPermissioned, AdminRecordSource, FormBeforeCreate, FormBeforeUpdate, FormBeforeDelete, RelationExtendManageQuery, cabana.AdminRelationContractProvider (relation members, cabana.RelationHasMany, foreign key organisation_id, columns name and email) |
models.UserGroup |
new Fillable() (name, code, description), Rules() (name: required|between:3,64, code: required|unique:user_groups), read-only UsersCount int64 (users_count, json "-"); Permissions is still *string |
models.Organisation |
new Fillable(), Rules(), MorphName(), AttachRelations(), BeforeValidate(*gorm.DB) (pointer receiver), relation field Members []User (json "-"); constant models.OrganisationAvatarField |
models.Slugify |
(text string) string |
models.PermissionSet.Text |
() *string; nil for a set without a non-zero value |
models.ParsePermissionSet |
unchanged from plan 03: (raw []byte) (PermissionSet, error) |
| Phrase keys added (en and pl) | user.groups, user.empty_groups, users.privileged_group_forbidden; group.{id,name,code,code_comment,code_invalid,description_field,created_at,users_count}; groups.{all_groups,menu_label,list_title,new_group,list_empty,delete_confirm,privileged_forbidden}; organisation.{label_plural,menu_label,id,name,slug,slug_comment,slug_invalid,description,avatar,members,new,list_empty,add_member,remove_members,remove_members_confirm,members_empty} |
Harness additions (package user tests): adminGroupsPath, adminOrganisationsPath, permAccessGroups, groupPath(id), organisationPath(id, suffix), sortedIDs, and on adminEnv: groupOptions, recordGroups, group, sideMenu, organisation, seedOrganisation.
Decisions Made
Organisation.Members. The plan said no field is added to the model. The framework's relation manager requires the relation to be a field of the owner model, soMembers []Userwas added. It is a Go relation, not a column:updates/10_organisations.gois unchanged andTestSchemaMatchesPHPSnapshotpasses.- Format refusals.
group.code_invalidandorganisation.slug_invalidare two phrase keys beyond the plan's list, with Laravel's alpha_dash wording. Without them the 422 would be English only. - Organisation delete confirm. The framework default. The UI-SPEC is a signed copy contract and names none for organisations.
- List title of Organisations.
organisation.label_plural, the PHP key and value.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] The members relation needs a field on the owner model
- Found during: Task 3
- Issue: boot failed with "relation members is not on the owner model".
- Fix:
Organisation.Members []UserwithforeignKey:OrganisationIDand json "-". - Files modified: models/organisation.go
- Verification:
TestAdminOrganisationMembers,TestHiddenNeverMarshals,TestSchemaMatchesPHPSnapshot - Committed in: 58595fe
2. [Rule 3 - Blocking] Existing assertions pinned the old form and navigation
- Found during: Tasks 1 and 3
- Issue:
TestAdminUserFormFieldspinned the user form's field list withoutgroups;TestAdminUsersTracerpinned one side item for the users permission, which now also opens Organisations. - Fix: both expectations updated.
- Files modified: admin_users_test.go
- Committed in: 9611708, 58595fe
3. [Rule 2 - Missing critical] Localized refusal of a malformed code or slug
- See Decisions Made. Committed in: a399287, 58595fe
Other departures from the plan text
- "A user of another organisation moves when linked here." This does not happen. The framework's hasMany relation manager offers, and accepts, only related rows whose foreign key is NULL; linking a member of another organisation answers 422 (
ids: contains an ineligible target) and changes nothing. The test asserts that, and that the user can be linked after being removed from the other organisation. The user form's organisation field still moves a user directly. controllers/app_access.gois a new file outsidefiles_modified: one small accessor type for the two new controllers.usersAdminControllerkeeps its own accessors, so the file with the guards of plan 03 was changed only where the plan says.- The D-06 matrix runs on codes of its own. The harness database holds several groups coded
adminfrom other tests, and validation (unique code) runs before the hooks, so "create a group with code admin" would answer 422 there. The test overrides the privileged list to[admin, priv-<stamp>, priv2-<stamp>]; theadmingroup itself is covered for re-code and delete. - Commits. Tasks commit code and tests together so each commit is green; Task 3 has a separate docs commit for the README.
- Acceptance check with base
8a65890. As in plan 03, the real base is one commit later; the diff ofupdates/10_organisations.gois empty against both.
Total deviations: 3 auto-fixed (2 blocking, 1 missing critical) and the departures above.
Impact on plan: No scope added beyond the orchestrator's test-list catch-up. No framework file, migration, config file, classes/privileged.go or Go module changed (git -C <plugin> diff 5805c63..HEAD -- go.mod go.sum updates config classes is empty).
Issues Encountered
None that blocked. The first run of Task 1's verify chain was red on the three known application lists; they were fixed (orchestrator scope) before the Task 1 commit was accepted through the tracer gate.
Not verified here
- No browser: locked chips, the forbidden banner, the checkbox editor and the members tab were exercised through the admin API only.
- The guards were each switched off once by hand to see
TestAdminPrivilegedGroupsfail (the relation lock: fails at the options check; the group-code permission: fails at the first create). That is not the systematic removal harness of plan 05. - Concurrent saves of the same user's groups by two administrators.
- The avatar's blob removal when an organisation is deleted: the test checks the attachment row is written on save, not what the delete does with it.
scripts/check-phase*.shgates were not run.
Open items for plan 05's review
organisation_roleoutlives the membership. Unlinking a member clearsorganisation_idonly (the framework writes just the foreign key; PHP's relation controller does the same). The user keeps, for example,organisation_role = owner, and carries it into the next organisation they are added to. The application'sCanManageOrgreads that role. Deleting an organisation does clear the role. Decide whether the plugin should clear the role on unlink, which needs a framework unlink hook or a plugin-side route.- Deleting an organisation cascades into other plugins. The application's organisation credential tables reference
golem15_user_organisationswithON DELETE CASCADE, sogolem15.users.access_userscan remove an organisation's credentials by deleting the organisation. PHP's screen has the same delete. No confirm text says so. - A group's boolean permission values are dropped by an edit.
ParsePermissionSetskips non-numeric values, so a legacy row such as{"x":true}shows unchecked in the editor and is saved withoutx;classes.UserPermissionGrants(API token principals) counted it as granted. This extends item 4 of plan 03. - Validation answers before the guard. A create or re-code that would be refused with 403 answers 422 instead when the body also fails validation (for example a duplicate privileged code). Nothing is saved either way; the 422 confirms that the code exists, which the list shows anyway.
- The seeded groups can be deleted.
guestandregisteredare ordinary groups, sogolem15.users.access_groupsmay delete or re-code them. PHP has no delete button on groups; D-29 added one. No Go code outside the model reads the two codes today. - Relation locks and the relation manager (plan 02, property 4) stay unreachable for
users_groups: no groups relation manager exists, and the organisation manager writesusers.organisation_idonly, whichTestAdminOrganisationMembersasserts. - Open items 2 to 9 of 12.1-03-SUMMARY.md are unchanged.
Known Stubs
None. golem15.users.access_settings and impersonate_user remain registered without a screen by decision (D-02).
Threat Flags
None beyond the plan's threat model. The plugin still adds no route of its own; the organisation members link and unlink routes are the framework's, covered by T-12.1-32.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
- Plan 12.1-05 can add the full unit tests, the guard-removal harness, the gate script (it should run every workspace module, not only the root) and the security review, then publish the plugin.
- The demo server on 127.0.0.1:8431 was left alone.
Self-Check: PASSED
- Created files exist: all 17 listed under key-files.created.
- Commits exist: plugin 9611708, a399287, 58595fe, 4693155 (
rev-list --count 5805c63..HEADis 4); application ca746fc, b35442a, 35a727f (rev-list --count d1abcab..HEADis 3). - Key links:
AdminRelationLocksinusers_admin_controller.go(3 matches);IsPrivilegedCodeinusergroups_admin_controller.go(3 matches);golem15.user.organisationsonce inadmin_navigation.goand once in the controller;"golem15_user_frontend_permissions"once inschema_diff_test.go. - Acceptance criteria of all four tasks re-run and passing, including the same-commit check for the field, the pivot model and the guard, the unchanged
Permissions *string, and the unchanged plugin remote head. - Plan verification: plugin vet and tests green at each plugin commit's tree; application vet and full suite green at
35a727f; framework vet and suite green; pointer equals the plugin head; nothing pushed.
Phase: 12.1-user-plugin-admin-screens Completed: 2026-10-05