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:
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import type { Db } from "@parking/db";
|
||||
import type { Role } from "@parking/shared";
|
||||
import { deviceEvents } from "../device-events.js";
|
||||
import type { DeviceMonitor } from "../device-monitor.js";
|
||||
import { getOccupancy } from "../occupancy.js";
|
||||
|
||||
// Live booth feed over a WebSocket. The booth UI opens ONE socket and receives
|
||||
@@ -49,11 +50,12 @@ function isAllowedOrigin(origin: string | undefined, host: string | undefined):
|
||||
}
|
||||
|
||||
type OutMsg =
|
||||
| { kind: "hello"; occupancy: ReturnType<typeof getOccupancy> }
|
||||
| { kind: "hello"; occupancy: ReturnType<typeof getOccupancy>; devices: unknown }
|
||||
| { kind: "ledger"; event: unknown; occupancy: ReturnType<typeof getOccupancy> }
|
||||
| { kind: "printer-status"; event: unknown };
|
||||
| { kind: "printer-status"; event: unknown }
|
||||
| { kind: "device-status"; event: unknown };
|
||||
|
||||
export async function wsRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
export async function wsRoutes(app: FastifyInstance, db: Db, deviceMonitor: DeviceMonitor): Promise<void> {
|
||||
app.get(
|
||||
"/api/ws",
|
||||
{
|
||||
@@ -83,8 +85,9 @@ export async function wsRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
}
|
||||
};
|
||||
|
||||
// Initial snapshot so the client renders immediately, before any event.
|
||||
send({ kind: "hello", occupancy: getOccupancy(db) });
|
||||
// Initial snapshot so the client renders immediately, before any event:
|
||||
// occupancy AND the current device-status set (for the footer).
|
||||
send({ kind: "hello", occupancy: getOccupancy(db), devices: deviceMonitor.snapshot() });
|
||||
|
||||
// Subscribe to the live buses. Each handler recomputes occupancy from the
|
||||
// ledger (cheap fold) so the pushed count is always authoritative.
|
||||
@@ -94,10 +97,16 @@ export async function wsRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
const offPrinter = deviceEvents.onPrinterStatus((event) => {
|
||||
send({ kind: "printer-status", event });
|
||||
});
|
||||
// Unified device status (all categories) for the booth footer — pushed on
|
||||
// change; the initial set rode the hello above.
|
||||
const offDevice = deviceEvents.onDeviceStatus((event) => {
|
||||
send({ kind: "device-status", event });
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
offLedger();
|
||||
offPrinter();
|
||||
offDevice();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user