import type { FastifyInstance } from "fastify"; import { requireRole } from "../auth.js"; import { deviceEvents } from "../device-events.js"; import type { PrinterMonitor } from "../printer-monitor.js"; // Live printer-status API. The PrinterMonitor polls printers in the background; // these endpoints expose its cache (snapshot) and a live push stream (SSE) so the // booth UI shows paper-out / cover-open / offline in real time. Any authenticated // operator may read status (it's operational, not a setup action). export async function printerRoutes( app: FastifyInstance, monitor: PrinterMonitor, ): Promise { const guard = requireRole("admin", "operator", "cashier", "readonly"); // Current status of every monitored printer (cached — no device round-trip). app.get("/api/printers/status", { preHandler: guard }, async () => ({ printers: monitor.snapshot(), })); // Live stream: emits the full snapshot on connect, then one event per change. // Server-Sent Events — one-way, survives proxies, trivially consumed by the SPA. app.get("/api/printers/status/stream", { preHandler: guard }, (req, reply) => { reply.raw.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive", }); const send = (event: string, data: unknown) => { reply.raw.write(`event: ${event}\n`); reply.raw.write(`data: ${JSON.stringify(data)}\n\n`); }; // Initial state so a fresh client doesn't wait for the next change. send("snapshot", { printers: monitor.snapshot() }); const unsubscribe = deviceEvents.onPrinterStatus((e) => send("status", e)); // Heartbeat keeps intermediaries from closing an idle connection. const heartbeat = setInterval(() => reply.raw.write(": ping\n\n"), 25000); heartbeat.unref?.(); req.raw.on("close", () => { clearInterval(heartbeat); unsubscribe(); }); }); }