--- type: concept tags: [parking, device, monitoring, reliability, ui] sources: [] updated: 2026-06-18 status: open --- # Device status monitoring (the booth footer) The booth shows a **fixed footer** with the live status of every configured device — relays, readers, cameras, printers — so an operator sees at a glance that the barrier relay is reachable, the exit scanner is up, and the ticket printer has paper. This generalises the printer-only [[printer-status-monitoring]] to **all four [[device-adapter-pattern|device categories]]**. A reliability control, not a threat-model one. (Built 2026-06-18.) ## What gets polled, and how Every **enabled** row in `devices` is polled on an interval, regardless of category — the monitor talks only to the adapter interfaces ([[device-adapter-pattern]]), never a driver SDK: - **Printers** → their rich `MonitorableDevice.readStatus()` (paper end / near-end, cover open, cutter error, off-line) — the same capability the existing [[printer-status-monitoring|PrinterMonitor]] uses. The footer surfaces the fault detail. - **Relays / readers / cameras** → the generic `Device.healthCheck()` **reachability** probe every adapter implements (`ready | degraded | offline`). This is presence/up-ness, not a deep fault model — a relay either answers or it doesn't. Both collapse to one **traffic-light**: `ready | degraded | offline`, plus a `detail` string. Fail **toward "there's a problem"**, never false-healthy: a probe that throws or times out reads `offline` (consistent with [[printer-status-monitoring]]'s fail-safe mapping); a driver that's no longer registered reads `offline` ("driver not registered") rather than vanishing. ## The monitor (server) `DeviceMonitor` (`apps/server/src/device-monitor.ts`), modelled on the PrinterMonitor: - re-reads the device set each tick (a newly-assigned/removed device appears/disappears without a restart); drops cached status for devices that are gone or disabled; - polls every `DEVICE_POLL_MS` (default **8000ms**), never overlapping ticks; - caches the latest unified status per device id; - emits a `device-status` bus event **only when a device's state or detail changes** (deduped). > **Relationship to the PrinterMonitor.** Both run. The PrinterMonitor stays the authority for the > printer-specific live detail + its SSE stream (`/api/printers/status*`) that the entry flow may > later depend on for [[printer-roles-failover]]. The DeviceMonitor is the **unified footer feed** > across all categories. They poll independently (printers get probed by both — cheap HTTP reads); > the small duplication is deliberate, to avoid coupling the footer to printer internals. Could be > consolidated later if the overlap ever matters. ## API / live UI - `GET /api/devices/status` — cached snapshot of all devices (no device round-trip). Any authenticated role (operational, not a setup action). - Live updates ride the **one booth [[booth-console|WebSocket]]** (`/api/ws`): the `hello` frame carries the initial device-status set; a `device-status` frame is pushed per change. The web [[booth-console|live store]] holds the set keyed by device id; the REST snapshot seeds it / fills in if the socket is briefly down. - **`DeviceFooter`** (`apps/web/src/ui/DeviceFooter.tsx`) renders one **compact** chip per device — a coloured dot + a **role label, never the vendor** — ordered access → reader → camera → printer, with a right-aligned roll-up ("N with issues" / "all ready"). Mounted in the app shell so it's visible on every screen. ### Label = role, not vendor (refinement 2026-06-18) The chip shows **what the device does, not who made it**: the localised category + a role/direction suffix → `Lexuesi hyrje`, `Printer kabina`, `Kamera dalje`. The server sends a structured **`roleKind`** token (not a composed string), the client localises it: - **reader / camera** → the direction inherited from its bound relay (`directionOf()` in [[entry-exit-points|device-resolve]]): `entry | exit | both`. - **access controller** → `entry | exit | both` from its `relays[]`, or **`mixed`** when it spans more than one direction; `null` if it declares none yet. - **printer** → `lane` (entry-dispenser) | `booth` (booth-receipt) — the [[printer-roles-failover]] role. - `null` → the chip shows the category alone. ### Detail does NOT pollute the footer (refinement 2026-06-18) Chips stay short — **no inline fault text**. A device that is `degraded`/`offline` is clickable (so is the roll-up); clicking opens a small **issues panel** anchored above the footer that lists only the problem devices with their role label, state, the `detail` string, and the last-checked time. `ready` chips are non-interactive. The panel closes on outside-click / Escape (a lightweight popover — no extra dependency; only Radix Dialog is installed). ## Verified (2026-06-18) On a fresh DB seeded with a stub relay, a TCP reader, and two printers (one reachable, one not): relay + reader → `ready` via `healthCheck`; the unreachable printer → `offline` (with a detail string, never threw); the bus emitted once per device on first observation, and a second unchanged tick was silent (change-only emit). Server + web build clean. ## Open / not yet done - **Reachability ≠ correctness.** `healthCheck()` says a relay/reader answers, not that it's wired to the right barrier or reading cards — that's a setup/precondition concern ([[first-run-setup]], the Dingtian [[access-controller-button-flow|precondition checks]]). - **No per-device history / alerting.** The footer is point-in-time; a flapping device isn't tracked over time. Reconciliation-style alerting is out of scope here. - **Cameras** only expose `healthCheck` reachability today; a "last snapshot age" health signal could be richer ([[lpr-camera]], [[opencv-anpr-service]]). - Possible later **consolidation** of PrinterMonitor + DeviceMonitor (see the note above).