Setup: manage multiple device instances per category (add/remove)

The data model was already multi-instance (lane_devices = one row per
instance; assign always inserts) -- the limitation was UI-only. Make the
whole flow support more than one of every category:

- Backend: add DELETE /api/setup/assign/:id (unassign by id). /state now
  redacts secrets (pushPassword/webPassword/relayPassword) via a shared
  redactSecrets() also used by /assign -- it was returning raw config rows.
- Web: SetupWizard reworked from one fixed slot per category into a list of
  assigned instances (driver/role/host + Remove) plus an "Add another" form.
  select-type config fields (e.g. printer role) now render as dropdowns.
- api.ts: add fetchState(), unassignDevice(), Assignment/SetupState types.

Verified via Fastify inject: two printers assigned to one lane both list,
no secret leak, delete -> 204, delete unknown -> 404, count drops to 1.
Full repo typechecks.

Wiki: first-run-setup documents multi-instance + delete + redaction.
This commit is contained in:
2026-06-14 20:39:39 +02:00
parent b2a0471b08
commit 39d4bac419
5 changed files with 299 additions and 61 deletions
+23 -7
View File
@@ -27,17 +27,33 @@ each device's connection config.
authenticated input push ([[device-input-flow]]) — the admin never touches the device's own web
UI. **Fails the save** (no DB row) if the device can't be configured, so there are no
orphan/half-configured rows. On success persists to `lane_devices`.
4. **Complete** — `POST /api/setup/complete` marks the single-row `setup_state`.
4. **Remove** — `DELETE /api/setup/assign/:id` (admin-only) drops one instance's row. Only our
row is removed; the device itself is not un-hardened/un-configured (a stale push from an
unknown device id is already rejected, and re-assigning reconfigures it).
5. **Complete** — `POST /api/setup/complete` marks the single-row `setup_state`.
## Config granularity
## Config granularity — multi-instance per category
Organized **per lane** — each lane gets an access controller, reader(s), and camera(s), each with
its own connection settings. Matches the architecture's "mixable per lane" reality (a lane can
serve permit holders via [[wiegand]] and casual via host-side reads on one relay — see
[[entry-exit-readers]]).
The data model is **multi-instance**: `lane_devices` holds **one row per instance**, keyed by a
generated `id`, with no one-per-(lane, category) constraint. So a lane can have **more than one of
every category** — e.g. two printers (an entry dispenser + a booth printer; see
[[printer-roles-failover]]), multiple readers, multiple cameras. `assign` always inserts a new row
(never an upsert), and `state` returns the full list.
The `SetupWizard` reflects this: each category shows the **list of assigned instances** for the
current lane (with **Remove**) plus an **Add another** form — not a single fixed slot. `select`-type
config fields (e.g. a printer's role) render as dropdowns.
Organized **per lane** — each lane gets its access controller(s), reader(s), camera(s), and
printer(s), each with its own connection settings. Matches the architecture's "mixable per lane"
reality (a lane can serve permit holders via [[wiegand]] and casual via host-side reads on one
relay — see [[entry-exit-readers]]).
## Security notes
- The assign/state/complete endpoints require the **admin** role ([[local-jwt-auth]]).
- The assign/state/delete/complete endpoints require the **admin** role ([[local-jwt-auth]]).
- Device **credentials are stored in `lane_devices.config`** — protect at rest
([[disk-os-hardening]]); device hosts belong on the isolated VLAN ([[network-isolation]]).
- **Secrets are stripped on the way out**: `assign` and `state` both redact `pushPassword`,
`webPassword`, and `relayPassword` from the returned config (the UI lists devices; it never
needs the stored secrets).