mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 20:16:35 +00:00
feat(cameras): live ONVIF event feed on camera detail page
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.
This commit is contained in:
parent
74a871cd9b
commit
9f382775a7
2 changed files with 52 additions and 0 deletions
|
|
@ -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, ">").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(
|
||||
`<div style="color:#999; font-size:0.85rem; padding:1rem 0">No events yet. ONVIF events appear here as the kiosk receives them.</div>`,
|
||||
);
|
||||
}
|
||||
const rows = events.map((e) => {
|
||||
let payload = "";
|
||||
try { payload = JSON.stringify(e.payload, null, 1); } catch { payload = String(e.payload); }
|
||||
return `<tr>
|
||||
<td style="font-size:0.8rem; white-space:nowrap">${formatTimeShort(e.received_at)}</td>
|
||||
<td><code style="font-size:0.75rem">${escapeHtml(e.topic)}</code></td>
|
||||
<td style="font-size:0.75rem">${escapeHtml(e.source_type)}</td>
|
||||
<td><pre style="margin:0; font-size:0.7rem; max-height:80px; overflow:auto; background:#fafafa; padding:2px 4px">${escapeHtml(payload)}</pre></td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
return htmlFragment(
|
||||
`<table><thead><tr><th>Time</th><th>Topic</th><th>Source</th><th>Payload</th></tr></thead><tbody>${rows}</tbody></table>`,
|
||||
);
|
||||
});
|
||||
|
||||
// ---- Kiosk edit/delete/labels ---------------------------------------------
|
||||
|
||||
app.get("/admin/kiosks/:id", (event) => {
|
||||
|
|
|
|||
|
|
@ -1329,6 +1329,26 @@ export function CameraEditPage(props: CameraEditProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<h2 style="margin:0 0 1rem; font-size:1.1rem">Live Events</h2>
|
||||
<p style="color:#666; font-size:0.85rem; margin-bottom:0.75rem">
|
||||
ONVIF events from kiosks subscribed to this camera. Auto-refreshes
|
||||
every 5s. All topics shown — motion, ANPR, line crossing, I/O, analytics, unknown.
|
||||
</p>
|
||||
<div
|
||||
id={`camera-events-${String(cam.id)}`}
|
||||
class="table-wrap"
|
||||
{...{
|
||||
"hx-get": `/admin/cameras/${cam.id}/events`,
|
||||
"hx-trigger": "load, every 5s",
|
||||
"hx-swap": "innerHTML",
|
||||
}}
|
||||
style="max-height:300px; overflow-y:auto"
|
||||
>
|
||||
<div style="color:#999; font-size:0.85rem; padding:1rem 0">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<h2 style="margin:0 0 1rem; font-size:1.1rem">Kiosk Subscriptions</h2>
|
||||
<p style="color:#666; font-size:0.85rem; margin-bottom:0.75rem">
|
||||
|
|
|
|||
Loading…
Reference in a new issue