feat(modules): venue-module registry — entitled ∩ activated, requireModule, Setup panel

Groundwork for the Car Wash pilot (wiki/decisions/venue-modules.md, build-order
steps 1 + 3). No Car Wash code yet; validation is the first module behind the
seam, unchanged in behaviour.

- @parking/shared: MODULE_IDS, ModuleManifest, MODULES (parking required;
  validation dependsOn parking), parseEntitledModules / resolveModuleActivation
  / effectiveModules as pure functions.
- DB: site_config.modules_json (migration 0026, hand-written + journal;
  additive, nullable = everything entitled).
- Server: modules.ts (entitledModules from MODULES_ENTITLED env, activated
  from site_config, effective set, requireModule preHandler → 403
  module_disabled); modules/index.ts registers folder-based modules by
  iterating the registry (modules/validation); site-config GET exposes
  modules/modulesEntitled/modulesActivated, PUT takes the full desired set,
  enforces entitlement + dependency rules (400 with reason) and signs one
  config_change per module that actually flips; /api/auth/me carries the
  effective set; validation routes guarded requireModule → requirePermission.
- Web: lib/modules.ts + modules/{index,validation}; router.tsx spreads
  WEB_MODULES into nav + route tree (validate route no longer named there);
  Setup → Site "Modules" panel (required shown disabled, dependencies as
  hints, server refusal shown verbatim); validation sections + programs fetch
  gated on the module; App invalidates the router whenever the session
  changes (route-context consumers only re-read on navigation — the nav was
  stale after a flip, and after every other setUser too).
- Lavazh validation station retired (STATIONS = ["bar"]; rows untouched).
- Deploy: MODULES_ENTITLED=parking,validation explicit in both booth stacks;
  documented in .env.example.
- Tests: modules.test.ts (7); suite 329/329; web build clean; Playwright
  round-trip on /setup/site verified live.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-05 11:04:39 +02:00
parent db9c3e0e31
commit 23d6379be8
27 changed files with 848 additions and 57 deletions
+54 -2
View File
@@ -8,8 +8,8 @@ status: open
# Venue modules — Car Wash, Bar/Restaurant, and Parking as peers
**Status: OPEN.** Design captured from a working session with the user on 2026-09-04, after the
desktop-shell run closed. Nothing here is built. Decisions marked **(settled)** were stated by the
**Status: OPEN** (Car Wash not yet built; the module registry IS — see "As-built" below). Design
captured from working sessions with the user on 2026-09-04/05. Decisions marked **(settled)** were stated by the
user in that session; everything else is the proposed shape awaiting a go.
## The ask
@@ -219,6 +219,58 @@ platform already owns:
collected and labelled first; the bay-count signal can ship before it (presence only).
Rough size: four to six weeks including the registry.
## As-built: the registry (2026-09-05, build-order steps 1 + 3)
Built as the groundwork for the Car Wash pilot. `parking` and `validation` are registered;
no parking code moved (the seam exists, the code crosses it as each subsystem is touched).
- **`packages/shared/src/index.ts`** — `MODULE_IDS`, `ModuleManifest` {`id`, `required`,
`dependsOn`, `resources`, `ledgerEventTypes`}, the `MODULES` registry, and the rules as pure
functions: `parseEntitledModules(env)` (unset/blank = everything; required always in; unknown
ids reported), `resolveModuleActivation(entitled, requested)` (required always in; refuses
not-entitled and missing-dependency with a human-readable reason), `effectiveModules(entitled,
activated)` (required ∪ entitled ∩ activated, dependency-broken modules dropped).
- **DB** — `site_config.modules_json` (nullable JSON array; null = everything entitled),
migration `0026_site_modules` (hand-written + journal entry: `drizzle-kit generate` needs a
TTY and this repo's snapshots stop at 0003 — migrations have been hand-written since).
- **Server** — `apps/server/src/modules.ts`: `entitledModules()` (env, read per request),
`activatedModulesOf(row)`, `effectiveModulesFor(db)`, and the **`requireModule(db, id)`**
preHandler (403, `code: "module_disabled"`), composed BEFORE `requirePermission` in a
preHandler array so a disabled module answers identically for every role.
`apps/server/src/modules/index.ts` iterates `MODULES` and calls each folder-based module's
`register(app, deps)` (today: `modules/validation/index.ts` → `routes/validations.ts`,
unchanged location, now guarded); boot logs `{entitled, effective}` so "why is X missing" is
answerable from the container log. `routes/site.ts`: GET returns `modules` / `modulesEntitled`
/ `modulesActivated`; PUT accepts the full desired `modules` set, validates via the shared
rules (400 with the reason), and signs one `config_change` `{setting: "modules.<id>", value,
prev, operator}` per module whose effective state actually flips (no-op resaves sign nothing).
`routes/auth.ts` `sessionView` carries `modules` so the SPA can hide nav on first paint.
- **Web** — `apps/web/src/lib/modules.ts` (`moduleOn(user, id)`, `WebModule` {nav, routes(root)}),
`apps/web/src/modules/index.ts` (`WEB_MODULES`), `modules/validation/index.tsx` (the
`/validate` route + nav entry, gated on module-on + permission). `router.tsx` spreads
`WEB_MODULES` into the header nav and the route tree and no longer names the validate screen.
`SiteSettings.tsx`: a **Modules** panel listing the entitled modules (required ones shown
disabled, dependencies shown as a hint); each flip PUTs the full set and shows the server's
refusal reason verbatim; the merchant-validation section only renders when `validation` is
effective. i18n `modules.*` (en + sq).
**Gotcha found in the browser check:** route-context consumers (the header nav) only re-read
the router context on navigation, so `setUser(freshMe)` alone left the nav stale after a
flip — `App.tsx` now `router.invalidate()`s whenever `user` changes (fixes the same latent
issue for every other `setUser` caller). The programs fetch is also gated on the module being
effective, so opening Setup → Site with validation off no longer logs a 403 to app_logs.
Verified live (Playwright against the Vite dev server): flip off → "Validations" leaves the
header and the validation sections hide; flip on → both return, no reload.
- **Deploy** — `MODULES_ENTITLED=parking,validation` added explicitly to both booth stacks in
`komodo/resources.toml`; documented in `apps/server/.env.example`.
- **Lavazh station retired** (step 1): `STATIONS = ["bar"]`; existing `lavazh` program rows are
untouched data (the server accepts any kebab slug) — they simply have no checkbox now.
- **Tests** — `apps/server/src/modules.test.ts` (7): defaults; deactivate → 403
`module_disabled` + signed flip + reversible; required can't be deactivated; unknown id → 400;
no-op resave signs nothing; `MODULES_ENTITLED=parking` → not offered, not activatable, routes
403; required entitled even when omitted, unknown ids ignored. Full suite 329/329.
- **Acceptance test for Car Wash** (unchanged): one manifest entry, one `SERVER_MODULES` line,
one `WEB_MODULES` line, its two folders, its migration — nothing else in the core touched.
## Open questions to settle before building
- ~~Platform name~~ — **settled 2026-09-05: it stays `parking-system` / `com.parking.desktop`.**
+15
View File
@@ -2922,3 +2922,18 @@ for the Bar until a Bar module exists and absorbs it. Only the Lavazh station is
Wash ships (Car Wash sponsors parking via its own order event). Two sponsorship mechanisms
coexist for now, accepted. Build order updated; validation is registered as its own module in the
registry so Bar can later depend on or absorb it.
## [2026-09-05] feat | Venue-module registry built (pilot groundwork): entitled ∩ activated, requireModule, Setup panel
Implemented build-order steps 1 + 3 of [[venue-modules]]: Lavazh validation station retired
(STATIONS = ["bar"], rows untouched); @parking/shared gains MODULE_IDS / ModuleManifest / MODULES
(parking required, validation dependsOn parking) plus the pure rule functions; site_config
gets modules_json (migration 0026, hand-written — drizzle-kit generate needs a TTY and the
snapshots end at 0003); server modules.ts adds requireModule(db, id) (403 module_disabled,
composed before requirePermission), modules/index.ts registers folder-based modules by
iterating the registry (validation is the first), site-config GET/PUT expose and set the
activation with dependency rules and one signed config_change per module that actually flips,
/api/auth/me carries the effective set; web gains lib/modules.ts + modules/{index,validation}
and router.tsx spreads WEB_MODULES into nav + route tree, SiteSettings gets a Modules panel and
hides the validation section when the module is off; MODULES_ENTITLED set explicitly in both
booth stacks. 7 new server tests, suite 329/329, web build clean. Car Wash next.