feat(devices): live device-status footer across all categories

Generalise printer-only monitoring to every configured device. New
DeviceMonitor polls all enabled devices each tick (default 8s): printers
via rich readStatus(), relays/readers/cameras via the generic healthCheck()
reachability probe, flattened to one traffic-light (ready/degraded/offline)
+ detail, deduped (emit on change only), fail-toward-offline.

- device-status bus event + GET /api/devices/status snapshot.
- Pushed over the existing /api/ws (hello carries the initial set;
  device-status frame per change).
- Web: live-store devices map, WS handler, DeviceFooter chip-per-device
  (role label not vendor; click a degraded/offline chip for an issues panel).

Verified roleKind resolution + change-only emit on a fresh DB.

Note: the footer's UI surface (api type, router mount, i18n devices) rides
in the subsequent subscription commit due to shared-file overlap.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-18 13:14:36 +02:00
parent 4e2e4feedb
commit f87e4c0d6b
11 changed files with 569 additions and 13 deletions
+21
View File
@@ -0,0 +1,21 @@
import type { FastifyInstance } from "fastify";
import { requireRole } from "../auth.js";
import type { DeviceMonitor } from "../device-monitor.js";
// Unified device-status snapshot for the booth footer. The DeviceMonitor polls all
// configured devices (relays/readers/cameras via healthCheck, printers via their
// rich readStatus) in the background; this exposes its cache. Live updates ride the
// booth WebSocket (kind:"device-status") — this REST route is the initial load /
// fallback. Any authenticated role may read (operational, not a setup action).
// See wiki/concepts/device-status-monitoring.md, booth-console.md.
export async function deviceStatusRoutes(
app: FastifyInstance,
monitor: DeviceMonitor,
): Promise<void> {
const guard = requireRole("admin", "operator", "cashier", "readonly");
app.get("/api/devices/status", { preHandler: guard }, async () => ({
devices: monitor.snapshot(),
}));
}