feat(roles): roles remember the jobs they follow (re-appliable), every role edit is signed

Closes the permissions-matrix loose ends (venue-modules.md §Permissions matrix):

- `role_jobs` (migration 0029): a role stores the manifest jobs it was composed from
  (chips on at save + any bundle fully present). `jobById` / `jobsBehind` in
  @parking/shared surface a followed job whose bundle grew past the role in a later
  release; the roles list shows a "behind <job>" badge with a one-click "Update to job"
  (the union, nothing removed) and the editor lints it. Never a runtime union: the grid
  stays the explicit enforcement layer and an update never widens a role without a click.
- Every role create/update/delete appends a `config_change` (`role.<id>`, prev/value =
  name + sorted permissions + jobs, operator); a no-op resave signs nothing. roleRoutes
  now takes the ledger.
- booth-supervisor already carries subscription:*; the stale open note is closed.

Tests: routes/roles.test.ts. Wiki: venue-modules status, local-jwt-auth, log.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-06 12:52:47 +02:00
parent e14e31a840
commit 50c18405b6
14 changed files with 329 additions and 17 deletions
+18
View File
@@ -53,6 +53,24 @@ export const rolePermissions = sqliteTable(
}),
);
/** The JOBS a role follows (venue-modules.md §"Permissions matrix", move 2): the
* manifest job presets the admin composed it from. Remembered so a later release that
* grows a job's bundle can be surfaced ("this role is behind the Wash operator job")
* and re-applied with one click — never expanded silently at runtime: what a role may
* do is always the explicit `role_permissions` grid. */
export const roleJobs = sqliteTable(
"role_jobs",
{
roleId: text("role_id")
.notNull()
.references(() => roles.id),
jobId: text("job_id").notNull(),
},
(t) => ({
uniq: unique().on(t.roleId, t.jobId),
}),
);
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
username: text("username").notNull().unique(),