server+web: capacity / FULL gate (occupancy fold + transient refuse)

Occupancy is a fold over the signed ledger (entries minus exits per identity);
getOccupancy returns {count, capacity, free, full}. Capacity is a single-row
site_config table (admin-set; null = uncapped; migration 0001, additive).

FULL gate lives in the transient entry flow: when full, refuse (no ticket, no
vehicle_entry, no open) and sign an anomaly. Permit entry is NOT gated --
subscribers are admitted past transient-full (their own maxConcurrent still
applies), so occupancy can read over capacity by design (reserve-for-permits).

Routes: GET /api/occupancy + GET /api/site-config (any role), PUT
/api/site-config (admin; non-negative int or null). Web SiteSettings: live
occupancy + FULL badge (everyone), capacity editor (admin).

Verified: fill to cap -> 3rd transient refused; permit admitted past full; exit
frees a slot; RBAC (operator can't set, -5 -> 400); verifyChain ok. Physical
FULL-sign relay output deferred.
This commit is contained in:
2026-06-16 08:13:06 +02:00
parent 644bfa1462
commit e579fe5b6e
13 changed files with 983 additions and 1 deletions
+12
View File
@@ -101,6 +101,17 @@ export const setupState = sqliteTable("setup_state", {
completedAt: text("completed_at"),
});
// Single-row site settings (admin-configurable). The home for site-wide knobs;
// `capacity` is the nominal space count the FULL gate refuses transient entry at
// (null = no cap). See wiki/concepts/capacity-occupancy.md.
export const siteConfig = sqliteTable("site_config", {
id: integer("id").primaryKey(), // always 1
capacity: integer("capacity"), // null = no capacity limit
updatedAt: text("updated_at")
.notNull()
.default(sql`(current_timestamp)`),
});
// --- Tariffs (composable, versioned) -------------------------------------
// A `tariffs` row is a logical rate card; its pricing lives in immutable, effective-
// dated `tariff_versions`. Editing prices PUBLISHES a new version, never mutates one.
@@ -215,6 +226,7 @@ export type LedgerEventRow = typeof ledgerEvents.$inferSelect;
export type DeviceEventRow = typeof deviceEvents.$inferSelect;
export type LaneDeviceRow = typeof laneDevices.$inferSelect;
export type SetupStateRow = typeof setupState.$inferSelect;
export type SiteConfigRow = typeof siteConfig.$inferSelect;
export type TariffRow = typeof tariffs.$inferSelect;
export type TariffVersionRow = typeof tariffVersions.$inferSelect;
export type PermitRow = typeof permits.$inferSelect;