f87e4c0d6b
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
59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
import { create } from "zustand";
|
|
import type { DeviceStatus, LedgerEvent, Occupancy } from "../api.js";
|
|
|
|
// CLIENT state for the live booth feed — deliberately small. Server data (the
|
|
// authoritative event list, occupancy totals) is owned by TanStack Query; this
|
|
// store holds only what Query shouldn't: the WS connection status, the latest
|
|
// pushed occupancy snapshot, and a rolling in-memory tail of recent events for the
|
|
// live ticker. Anything durable is re-fetched via Query. See lib/query.ts.
|
|
|
|
/** Connection state of the booth WebSocket, for a status indicator in the UI. */
|
|
export type WsStatus = "connecting" | "open" | "closed";
|
|
|
|
/** Cap the in-memory live feed so a long-running booth session can't grow it
|
|
* unbounded — the full history is always available via the /api/events query. */
|
|
const MAX_FEED = 200;
|
|
|
|
interface LiveState {
|
|
status: WsStatus;
|
|
/** Most recent occupancy pushed by the server (rides on every ledger event). */
|
|
occupancy: Occupancy | null;
|
|
/** Newest-first tail of recently pushed ledger events (for the live ticker). */
|
|
feed: LedgerEvent[];
|
|
/** Live device status keyed by device id (for the footer): set from the WS
|
|
* hello snapshot, then upserted per device on each device-status push. */
|
|
devices: Record<string, DeviceStatus>;
|
|
setStatus: (s: WsStatus) => void;
|
|
setOccupancy: (o: Occupancy) => void;
|
|
pushEvent: (e: LedgerEvent) => void;
|
|
/** Replace the whole device-status set (WS hello / reconnect snapshot). */
|
|
setDevices: (list: DeviceStatus[]) => void;
|
|
/** Upsert one device's status (a device-status push). */
|
|
upsertDevice: (d: DeviceStatus) => void;
|
|
reset: () => void;
|
|
}
|
|
|
|
/** Index a device-status list by device id. */
|
|
function byId(list: DeviceStatus[]): Record<string, DeviceStatus> {
|
|
const m: Record<string, DeviceStatus> = {};
|
|
for (const d of list) m[d.deviceId] = d;
|
|
return m;
|
|
}
|
|
|
|
export const useLiveStore = create<LiveState>((set) => ({
|
|
status: "connecting",
|
|
occupancy: null,
|
|
feed: [],
|
|
devices: {},
|
|
setStatus: (status) => set({ status }),
|
|
setOccupancy: (occupancy) => set({ occupancy }),
|
|
pushEvent: (e) =>
|
|
set((s) => ({
|
|
// Newest first; de-dupe by id (a reconnect can replay) and cap the length.
|
|
feed: s.feed.some((x) => x.id === e.id) ? s.feed : [e, ...s.feed].slice(0, MAX_FEED),
|
|
})),
|
|
setDevices: (list) => set({ devices: byId(list) }),
|
|
upsertDevice: (d) => set((s) => ({ devices: { ...s.devices, [d.deviceId]: d } })),
|
|
reset: () => set({ status: "connecting", occupancy: null, feed: [], devices: {} }),
|
|
}));
|