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
+17 -3
View File
@@ -439,9 +439,23 @@ resources — the till already IS that copy. Role *templates stored in the DB*
(they change with the module), roles are data; keep that line.
**Status.** Moves 1, 2 and 3 built 2026-09-05 (see the Tills as-built below and [[shift]]
§Tills). Open: whether `booth-supervisor` should carry `subscription:*` by default; whether a
job should be *re-applicable* after a module update (today a chip only adds/removes the bundle
as it is now); an audit `config_change` on role edits.
§Tills). The three loose ends closed 2026-09-06:
- `booth-supervisor` DOES carry `subscription:read/create/update` (plus `tariff:read`,
`validation:read`) — it already did; the note was stale. Decided: a supervisor sells and
edits subscriptions by default.
- **Jobs are remembered and re-appliable.** A role stores the jobs it follows (`role_jobs`:
the chips on at save, plus any bundle fully present). `jobsBehind(jobs, has)` in
`@parking/shared` lists a followed job whose bundle has GROWN past the role (a newer
release added a permission); the roles list shows a "behind <job>" badge with a one-click
"Update to job" (the union; nothing removed), the editor lints it. Deliberately NOT a
runtime union: what a role may do is always the explicit grid, and a software update never
changes it without an admin's click — see the threat model. The first failure of this kind
was the wash operator's empty price list (the settings read needed `site:read`; now
`carwash:read` OR `site:read`, `requireAnyPermission`).
- **Role edits are signed.** Create/update/delete each append one `config_change`
(`setting: role.<id>`, `value`/`prev` = name + sorted permissions + jobs, `operator`); a
no-op resave signs nothing. A role edit is a privilege change and was the one setting an
admin could alter without a trace.
## Tills: shifts per money-taking module — BUILT (raised + built 2026-09-05)