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" } });
|
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 ---------------------------------------------
|
// ---- Kiosk edit/delete/labels ---------------------------------------------
|
||||||
|
|
||||||
app.get("/admin/kiosks/:id", (event) => {
|
app.get("/admin/kiosks/:id", (event) => {
|
||||||
|
|
|
||||||
|
|
@ -1329,6 +1329,26 @@ export function CameraEditPage(props: CameraEditProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
<h2 style="margin:0 0 1rem; font-size:1.1rem">Kiosk Subscriptions</h2>
|
<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">
|
<p style="color:#666; font-size:0.85rem; margin-bottom:0.75rem">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue