User.FilterScope handed the filter value to the database as it came. A
value such as abc failed the query, so the Users list answered 500.
- groupFilterID reads a positive whole number, as a number or its text
- any other value narrows the list to nothing
- TestAdminUsersGroupFilterValue fails with a 500 without the change
- PermissionSet reads the stored JSON tolerantly and writes an object
of integers; FrontendPermission is the catalogue row
- MergedPermissions, HasPermission and UserHasPermission port PHP
getMergedPermissions and Winter's hasPermission: groups merge with
most positive wins, the user's own values override, held means
exactly 1, wildcards as in Winter
- User gains Permissions (never serialized) and declares the avatar
attachment relation the user API already writes
- users.permissions and users.last_seen as additive columns
- golem15_user_frontend_permissions (code, label, tab, comment)
- User gains LastSeen, read-only CreatedAt and UpdatedAt and the
filterByGroup list scope; none of them is serialized
- migration tests roll back by position past the three new steps
- user_groups (seeded with the guest and registered groups) and the
users_groups pivot, folded from the PHP v1.1.1 and v2.8.0 migrations
into migration 202610020001_create_user_groups
- models.UserGroup and a User.Groups relation that is never serialized
- classes.UserGroupCodes and classes.HasGroupCode for server-side checks
such as the site-admin group code admin
- additive only: no route, payload or existing column changes; the user
payload still answers "groups":[]
Match PHP payloads, already-activated Winter 500 HTML, and direct-DB
seeding so the corpus can flip those routes to ported without onboarding.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Move models, classes, controllers, middleware, and updates into subpackages
- Add self-registering models/updates/classes registries so later plans add files
- Keep the genres route and shipped migration SQL unchanged