19 KiB
Phase 12.1: User plugin admin screens - Context
Gathered: 2026-10-04 Status: Ready for planning
## Phase BoundaryBackend admins manage frontend users, user groups and organisations in the admin SPA, so the PHP backend is not needed for user administration after cutover. The PHP user plugin's Users, UserGroups and Organisations screens are ported to golem15.user (sm-user-plugin, mounted in fonoteka.go at plugins/golem15/user), driven by its fields.yaml / columns.yaml.
The discussion widened the framework half of the phase. summercms.go (modules/cabana, modules/pact, the admin SPA, READMEs and docs/) gains five generic admin features the screens need: declared bulk actions, record actions, a preview context, row state, and a permissioneditor field type. That work lands first and is tagged v0.1.3; the plugin screens build on the tag.
Out of scope: impersonating a user, the guest concept and convert-guest, MailBlocker / block-mail, a username column, the user plugin's Settings screen, and any change to the user API payloads (the Nuxt app's contract stays byte-identical).
Impersonate and guests
- D-01: Impersonate is not ported: no action, no button. PHP impersonation is session-based and never reaches the JWT-driven frontend, so nothing is lost at cutover.
- D-02: The permission
golem15.users.impersonate_useris still registered by the Go plugin, so backend roles imported at cutover that reference it stay valid and a later phase can use it. The plugin registers all four PHP codes:golem15.users.access_users,access_groups,access_settings,impersonate_user. - D-03: The guest concept is dropped entirely: no convert-guest action, no
is_guestcolumn or list column, no guest hint. The seeded Guest group row stays as data.
Group membership and privileged groups (T-12-18)
- D-04:
golem15.users.access_usersis enough to change a user's membership of ordinary groups, as in PHP. Adding or removing a privileged group additionally requires a new backend permission. The check is on the server and covers every path that writesusers_groups(the user form's groups field, any relation manager, bulk or record actions). — Reversibility: costly — the permission code is stored in backend roles; renaming it later means rewriting role rows in every host application. - D-05: Privileged groups are a config list of group codes under the
golem15.userconfig namespace, default[admin]. The plugin is shared across projects, so an application adds a code without a plugin change. - D-06: The User Groups screen is gated by
golem15.users.access_groups. Creating a group with a privileged code, changing a code to or from a privileged one, and deleting a privileged group all require the same extra permission as D-04. Ordinary groups are edited freely. - D-07: For an admin without the extra permission, privileged groups are shown in the user form's groups field but locked (disabled), so the admin can see that a user is a site admin. A save that tries to change a privileged membership anyway is refused with a forbidden error and changes nothing (no partial save).
- D-08:
User.Groupsstays out of every user API payload (P12 D-25). Only the admin API reads or writes it.
Actions, delete semantics and preview (framework + plugin)
- D-09: cabana gains declared bulk actions, generic for every plugin: a controller registers named bulk actions, the list declares which ones it offers, the SPA sends the selected ids, and the framework resolves them through the controller's list scope inside one transaction before calling the plugin. Ids outside the scope never reach the plugin. Bulk delete keeps working as today. — Reversibility: costly — list YAML and the
pactaction contract grow; every plugin's list config may come to depend on the shape. - D-10: cabana gains record actions: named actions shown as buttons for one record, run against the record loaded through the controller's form scope, each with its own permissions on top of the controller's, and each able to say whether it applies to the record's current state. — Reversibility: costly — same contract growth as D-09.
- D-11: cabana gains Winter's preview context: a read-only record screen with its own toolbar, fields with
context: preview, and arecordUrlthat may point at it. The Users list opens preview first, then Update, as in PHP. Status hints (not activated, banned, suspended, deleted) and the record actions live on the preview screen. — Reversibility: costly — a new screen and route in the SPA and a new context value in the form schema. - D-12: cabana gains row state: a controller returns a state per list row from a fixed framework-defined set (Winter's
listInjectRowClass, limited to known states such as deleted, negative, disabled, not free CSS classes). The SPA styles states with design tokens. Users: deleted when trashed, negative when banned, disabled when not activated. - D-13: Delete semantics follow PHP
Users.phpexactly. The Users list and form include trashed users (withTrashed).deactivateis the soft delete,restorebrings a user back, anddelete(form button and bulk) is a permanent force delete. No extra typed confirmation beyond the standard confirm. - D-14: Bulk actions on Users, as in PHP
index_onBulkAction: delete, activate, deactivate, restore, ban, unban. Record actions: activate, unban, unsuspend. Ban and suspend state lives in the existinguser_throttletable.
Fields and columns
- D-15: Frontend permissions are ported in full: the
golem15_user_frontend_permissionstable (code, label, tab, comment), theusers.permissionscolumn, the existinguser_groups.permissionscolumn, the Permissions tab on the user form (radio mode: allow / deny / inherit) and the group form (checkbox mode), and a Go resolver equivalent to PHPUser::getMergedPermissions()that a plugin can call to ask whether a user holds a permission. Nothing in the application calls the resolver yet; unit tests prove it. No plugin-declared registry: rows come from migrations or seed, as in PHP. — Reversibility: costly — additive migrations in the shared user plugin; the stored JSON shape must match PHP for the cutover import. - D-16: The editor is a built-in framework field type,
permissioneditor, in cabana and the SPA: tabbed, with a radio or checkboxmode, and an option list supplied by the controller. It replaces the PHP YAML'sGolem15\User\FormWidgets\FrontendPermissionEditorclass name. It is built to be reusable for backend role permissions later. The 10.1 widget contract (scalar fill keys only) is not widened. — Reversibility: costly — a new field type in the typed schema and the generated TS types. - D-17:
last_seenis added tousersas an additive column, updated by the Go auth path on login or token refresh, and shown in the Users list as in PHP. It must not appear in any user API payload unless PHP already returns it. - D-18:
usernameis dropped from the ported YAML and no column is added (login is by email). - D-19: The user form carries the PHP create/update behaviour: password with confirmation on create, password reset on update,
send_invite(default on, create only) that sends the invitation mail, andcreated_ip_address/last_ip_addresson preview. - D-20: Avatars use the Phase 12.2
fileuploadfield (mode: image) on both the user form (260x260) and the organisation form (120x120). They must bind to the same attachment the user API already serves, so an avatar set in the admin shows in the app and the other way round. - D-21:
block_mailand MailBlocker are left out. A todo records them for the mailing work (mail settings, templates).
Screens, as ported
- D-22: Organisations: list plus form (name, slug with preset from name, description, avatar) gated by
golem15.users.access_users, and amembersrelation manager that assigns existing users (PHPadd|removeon the hasMany, which sets or clears the user'sorganisation_id). The user form'sorganisationfield is a belongsTorelationpicker. - D-23: Users filters follow PHP
config_filter.yaml: groups (scopefilterByGroup), created date (daterange) and activated (switch). The PHPconditions:strings are re-expressed as model scopes, since aconditionskey is a boot error (P9). - D-24: User Groups list keeps the
users_countcolumn.
Release and ordering
- D-25: Framework first. The cabana and SPA work (D-09 to D-12, D-16) lands in the early plans with module READMEs,
docs/pages, the admin OpenAPI document, generated TS types and the rebuiltdist/, and is tagged v0.1.3 (v0.1.2already exists). The plugin screens build on that tag. Framework fixtures use neutral names and never name the application.
Plan-count checkpoint (confirmed 2026-10-04, after research)
- D-26: The phase has five plans: (01) framework actions: declared bulk actions, record actions, row state and a 403 error type; (02) framework preview context,
permissioneditorand the form seams, ending in tag v0.1.3; (03) plugin foundation and the Users screen; (04) User Groups and Organisations screens, the privileged-group guard (T-12-18) and the application's bump to v0.1.3; (05) unit tests, the phase gate script and the security review. - D-27: All seven extra framework seams from RESEARCH.md "Framework Gaps Beyond CONTEXT.md" are accepted into v0.1.3: G1
passwordfield type, G2 form virtual fields, G3 writable foreign-key opt-in on a relation field, G4cabana.ForbiddenErrorand locked relation options, G5 admin validation rules, G6invisiblelist columns, G7preset. — Reversibility: costly — each grows thepact/cabana contract or the typed schema. - D-28: G5 is solved with an optional controller interface that returns the rule set per operation, not with a wrapper record type in the plugin.
- D-29: The research recommendations for the remaining open questions are accepted: User Groups keep the standard form delete, guarded per D-06, with pivot cleanup;
last_seenis written on login and on refresh, at most once per five minutes, and a failed write never fails auth; a user created in the admin starts not activated and only theactivateactions setis_activated; bulkactivateskips users that are already active and reports the affected count.
Plan-check decision (confirmed 2026-10-04)
- D-30: A user who is a member of a privileged group (D-05) is protected against takeover. Changing that user's password or email, and permanently deleting them (form delete and bulk delete), additionally requires the D-04 permission. Without it the request is refused with a forbidden error and changes nothing (no partial save; a bulk delete containing such a user is refused as a whole). This deliberately differs from PHP, where
access_usersalone is enough. — Reversibility: cheap — a server-side check in the plugin; no contract or schema change.
Claude's Discretion
- The code of the extra permission in D-04 (for example
golem15.users.manage_privileged_groups), its label and tab, and the config key name for the privileged list. - YAML keys and Go interface names for bulk actions, record actions, preview and row state, provided they follow the existing fail-loud rules (unknown keys and unregistered actions are boot errors), sit under the
{prefix}/api/v1/{vendor}/{plugin}/{controller}/...scheme, userequireAjaxon writes and carry swag annotations. - The exact set of row states and their token styling; whether a state is also announced in text for accessibility.
- What the preview screen shows beyond status hints, preview-context fields and actions (PHP's scoreboard is optional).
- Whether
last_seenis written on login only or also on refresh, and any throttling of that write. - How force delete cleans up what hangs off a user (attachments, throttle, groups pivot), following PHP
Userdelete behaviour. - Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Run the security-review agent: the phase touches authorization (T-12-18), the plugin API (
pact) and destructive bulk operations.
<canonical_refs>
Canonical References
Downstream agents MUST read these before planning or implementing.
Roadmap and prior decisions
.planning/ROADMAP.md§ "Phase 12.1" — goal, five success criteria, open questions (now answered by D-01 to D-07).planning/phases/12-p-ytarium-api-collections-and-albums/12-01-PLAN.md— user groups (D-25) and threat T-12-18, which this phase revisits.planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md— D-25 site-admin predicate (group codeadmin).planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md— typed schema, fail-loud boot, permissions (D-03), hooks (D-13), filters and scopes.planning/phases/10-admin-vue-spa/10-CONTEXT.md— SPA stack, FieldRenderer registry, toolbar buttons (D-14), cookie auth and CSRF, OpenAPI to TS types (D-15), committeddist/.planning/phases/10.1-runtime-admin-extension-point/10.1-CONTEXT.md—AdminActioncontract, why toolbar actions carry no record ids (D-12), widget fill limits (D-05 to D-07).planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/12.2-CONTEXT.md—fileupload,attach.Relation, relation child CRUD and parent scoping (D-15)
PHP reference (read-only): /media/nvme/dev/golem15/fonoteka/plugins/golem15/user
controllers/Users.php— actions, bulk actions,withTrashed,listInjectRowClass, force deletecontrollers/users/—config_list.yaml,config_form.yaml,config_filter.yaml,config_relation.yaml,_list_toolbar.htm,_preview_toolbar.htm,_hint_*.htmcontrollers/UserGroups.php,controllers/usergroups/,controllers/Organisations.php,controllers/organisations/(incl.config_relation.yaml)models/user/fields.yaml,models/user/columns.yaml,models/usergroup/*.yaml,models/organisation/*.yamlmodels/User.php—getMergedPermissions,attemptActivation,ban/unban,unsuspend,sendInvitation, delete behaviourmodels/FrontendPermission.php,formwidgets/FrontendPermissionEditor.phpand its partials,updates/v2.8.0/— frontend permissions (D-15, D-16)Plugin.php—registerPermissions,registerNavigation
Go code this phase touches
../fonoteka.go/plugins/golem15/user/(reposm-user-plugin) —plugin.go,models/,classes/,updates/,lang/,README.mdmodules/cabana/—actions.go,list_schema.go,form_schema.go,filter_schema.go,crud.go,http.go,relation*.go,README.mdmodules/pact/capabilities.go—AdminAction,HasAdminActions,Permission,NavigationItemadmin/src/— form field registry, list view and toolbar, relation manager../fonoteka.go/plugins/golem15/fonoteka/controllers/*_admin_controller.go— working examples of admin controllers in an application plugindocs/backend/—admin-controllers.md,forms.md,lists-and-filters.md,relation-manager.md,users-and-permissions.md;go test ./cmd/summer -run TestDocsTreeandsummer docs:build --checkmust pass
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
sm-user-pluginalready hasmodels.User(soft delete viaDeletedAt,Groupsmany2many never serialized),models.UserGroup,models.Organisation,models.Throttle(is_banned,is_suspended), andclasses.UserGroupCodes/HasGroupCode. It has no admin controllers, permissions or navigation yet.- cabana: list, form, filters (
switch,daterange, model scopes),relationandrelation-managerfields with child CRUD and parent scoping,fileupload,datepicker,widget,partial, toolbar actions, bulk delete. pact.AdminAction/HasAdminActions: the existing action contract that bulk and record actions extend.- The Go user API already stores user avatars; D-20 reuses that attachment.
Established Patterns
- Optional capabilities are interfaces the framework type-asserts (
Has*, admin hooks). - Unknown YAML keys, unknown types, unregistered actions and a
conditions:filter fail boot. - Every write route uses
requireAjax, sits under thebackendguard and enforcesRequiredPermissions; an action's own permissions are checked on top. - Toolbar actions deliberately take no record ids, so an id list can never become an unscoped lookup. Bulk actions (D-09) must keep that property by resolving ids through the list scope.
- Framework stays app-agnostic: contract tests use a nameless fixture plugin; the real screens live in the plugin repo.
- Framework changes update the module README,
docs/, the admin OpenAPI document, generated TS types and the committeddist/in the same change.
Integration Points
sm-user-pluginplugin.go: addpact.HasAdminControllers,HasPermissions, navigation, and embed the admin YAML.- New additive migrations in
sm-user-plugin/updates:users.permissions,users.last_seen,golem15_user_frontend_permissions. The fonoteka.go parity schema allow-list needs matching entries where the frozen PHP snapshot differs. - The Go auth path (login / refresh) writes
last_seen. - fonoteka.go bumps the submodule pointer and the framework version (v0.1.3).
</code_context>
## Specific Ideas- "Framework first, but I believe it's v0.1.3, v0.1.2 already exists" — the tag for this phase's framework work is v0.1.3.
- The user chose the fuller option over the lean one in three places: a real preview screen in the framework (not status on the update form), the frontend permission editor ported now (not deferred), and
last_seenadded. Treat PHP parity of the admin experience as the bar, not minimum viable. - Block-mail is wanted, but together with the mailing work (mail settings, templates), not here.
- Impersonate a frontend user from the admin by minting a short-lived user JWT, with an audit record and a Nuxt entry point. The permission is already registered (D-02).
- MailBlocker and the block-mail checkbox: recorded as todo
mailblocker-with-mailing.md, to be done with the mailing development. - Guest users and convert-guest, for a project that uses guests.
- A
usernamecolumn and login by username. - A plugin-declared frontend permission registry synced at boot.
- Using
permissioneditorfor backend role permissions. - The user plugin's Settings screen (
access_settings).
Reviewed Todos (not folded)
nest-framework-packages-under-modules.md,per-module-readmes-after-nest.md,readme-go-fences-src.md,rewrite-summercms-readme.md,refresh-fonoteka-readme.md: repo and README housekeeping; keyword matches only.2026-10-01-benchmark-the-application-on-wintercms-vs-summercms.md: benchmarking, unrelated.backend-admin-api-tokens.md: backend admin auth, not frontend user administration.orphan-pending-routes.md: covered by Phase 14.1.scaffold-admin-controller-incomplete.md,scaffold-same-second-migration-order.md,scaffold-generated-header-and-command-deps.md,bonfire-duplicate-command-names.md: CLI scaffolding bugs; worth knowing if the planner usesmake:admin-controller, but not this phase's scope.lagoon-readme-after-commit-callback-order.md,sitemap-plugin-port.md: unrelated.
Phase: 12.1-user-plugin-admin-screens Context gathered: 2026-10-04