import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "@tanstack/react-query"; import { fetchDeviceStatus, type DeviceStatus } from "../api.js"; import { qk } from "../lib/query.js"; import { useLiveStore } from "../lib/live-store.js"; // Fixed device-status footer for the booth chrome. One compact chip per configured // device — relays, readers, cameras, printers — labelled by ROLE, never vendor // (e.g. "Lexuesi hyrje", "Printer kabina", "Kamera dalje"), with a traffic-light // dot. Fault detail does NOT pollute the footer: clicking opens a small panel that // lists the degraded/offline devices and their issues. Status is fed by the // DeviceMonitor over the WS (snapshot on connect + per-device pushes, held in the // live store); a REST snapshot seeds it / fills in if the WS is briefly down. // See wiki/concepts/device-status-monitoring.md, booth-console.md. const DOT: Record = { ready: "bg-term-green", degraded: "bg-term-amber", offline: "bg-term-red", }; const TEXT: Record = { ready: "text-term-text", degraded: "text-term-amber", offline: "text-term-red", }; /** i18n key for a device category. */ const CATEGORY_KEY: Record = { access: "devices.catAccess", reader: "devices.catReader", camera: "devices.catCamera", printer: "devices.catPrinter", vision: "devices.catVision", }; /** i18n key for the role/direction token (null = no suffix). */ function roleKey(roleKind: DeviceStatus["roleKind"]): string | null { return roleKind ? `devices.role.${roleKind}` : null; } /** Stable display order: access (barrier) first, then readers, cameras, printers. */ const ORDER: Record = { access: 0, reader: 1, camera: 2, printer: 3, vision: 4, }; /** "Lexuesi hyrje" — category word + localised role/direction (when known). */ function useLabel() { const { t } = useTranslation(); return (d: DeviceStatus) => { const cat = t(CATEGORY_KEY[d.category]); const rk = roleKey(d.roleKind); return rk ? `${cat} ${t(rk)}` : cat; }; } function sortDevices(list: DeviceStatus[]): DeviceStatus[] { return [...list].sort( (a, b) => ORDER[a.category] - ORDER[b.category] || (a.roleKind ?? "").localeCompare(b.roleKind ?? ""), ); } export function DeviceFooter() { const { t } = useTranslation(); const label = useLabel(); // Seed/fallback from REST; the WS keeps the live store authoritative thereafter. const seed = useQuery({ queryKey: qk.deviceStatus, queryFn: fetchDeviceStatus }); const live = useLiveStore((s) => s.devices); const [open, setOpen] = useState(false); const rootRef = useRef(null); // Close the issues panel on an outside click or Escape. useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false); document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [open]); // Prefer the live store (WS); fall back to the REST snapshot before the first push. const fromLive = Object.values(live); const devices = sortDevices(fromLive.length > 0 ? fromLive : seed.data?.devices ?? []); const problems = devices.filter((d) => d.state !== "ready"); return ( ); }