From 9f382775a7d97cb40d3b922e773a565aa1e8b675 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Thu, 21 May 2026 12:09:09 +0200 Subject: [PATCH] feat(cameras): live ONVIF event feed on camera detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camera edit page gains a "Live Events" panel that auto-refreshes every 5s via htmx. Shows last 20 events for this camera from event_log: topic, source type, timestamp, and raw payload JSON. Surfaces ALL ONVIF topics including unknown ones — if a camera produces an event type we haven't seen before, it shows up here immediately. queryEvents gains camera_id + source_type filters. Route GET /admin/cameras/:id/events returns an HTML fragment with the event table rows. --- .../service-admin-http/routes-admin.ts | 32 +++++++++++++++++++ server/src/web-templates/admin-pages.tsx | 20 ++++++++++++ 2 files changed, 52 insertions(+) diff --git a/server/src/plugins/service-admin-http/routes-admin.ts b/server/src/plugins/service-admin-http/routes-admin.ts index 6ae7295..f2f7cfa 100644 --- a/server/src/plugins/service-admin-http/routes-admin.ts +++ b/server/src/plugins/service-admin-http/routes-admin.ts @@ -1434,6 +1434,38 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void { return new Response(null, { status: 302, headers: { location: "/admin/cameras" } }); }); + // ---- Camera live event feed (htmx fragment, polled every 5s) --------------- + const formatTimeShort = (iso: string) => { + try { return new Date(iso).toLocaleString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", day: "2-digit", month: "short" }); } + catch { return iso; } + }; + const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + app.get("/admin/cameras/:id/events", (event) => { + const id = Number(getRouterParam(event, "id")); + const { events } = deps.repo.queryEvents({ + camera_id: id, + limit: 20, + }); + if (events.length === 0) { + return htmlFragment( + `
No events yet. ONVIF events appear here as the kiosk receives them.
`, + ); + } + const rows = events.map((e) => { + let payload = ""; + try { payload = JSON.stringify(e.payload, null, 1); } catch { payload = String(e.payload); } + return ` + ${formatTimeShort(e.received_at)} + ${escapeHtml(e.topic)} + ${escapeHtml(e.source_type)} +
${escapeHtml(payload)}
+ `; + }).join(""); + return htmlFragment( + `${rows}
TimeTopicSourcePayload
`, + ); + }); + // ---- Kiosk edit/delete/labels --------------------------------------------- app.get("/admin/kiosks/:id", (event) => { diff --git a/server/src/web-templates/admin-pages.tsx b/server/src/web-templates/admin-pages.tsx index 654dbf8..188832c 100644 --- a/server/src/web-templates/admin-pages.tsx +++ b/server/src/web-templates/admin-pages.tsx @@ -1329,6 +1329,26 @@ export function CameraEditPage(props: CameraEditProps) { )} +
+

Live Events

+

+ ONVIF events from kiosks subscribed to this camera. Auto-refreshes + every 5s. All topics shown — motion, ANPR, line crossing, I/O, analytics, unknown. +

+
+
Loading...
+
+
+

Kiosk Subscriptions