diff --git a/backend/routers/portal.py b/backend/routers/portal.py index 6af574e..6f86108 100644 --- a/backend/routers/portal.py +++ b/backend/routers/portal.py @@ -214,6 +214,34 @@ async def portal_location_history(location_id: str, hours: float = 2.0, return {"status": "ok", "readings": (r.json() or {}).get("readings", [])} +# Whitelist of alert-event fields exposed to a client (no internal ids/ack-by). +_PORTAL_EVENT_FIELDS = ("rule_name", "metric", "threshold_db", "onset_at", + "onset_value", "peak_value", "clear_at", "status") + + +@router.get("/api/location/{location_id}/events") +async def portal_location_events(location_id: str, limit: int = 20, + client: Client = Depends(get_current_client), + db: Session = Depends(get_db)): + """Scrubbed breach history for a location the client owns (read-only).""" + resolve_client_location(client, location_id, db) + unit_id = active_unit_for_location(location_id, db) + if not unit_id: + return {"status": "ok", "events": []} + limit = max(1, min(limit, 100)) + try: + async with httpx.AsyncClient(timeout=5.0) as hc: + r = await hc.get(f"{SLMM_BASE_URL}/api/nl43/{unit_id}/alerts/events", + params={"limit": limit}) + except Exception: + return {"status": "ok", "events": []} + if r.status_code != 200: + return {"status": "ok", "events": []} + raw = (r.json() or {}).get("events", []) + events = [{k: e.get(k) for k in _PORTAL_EVENT_FIELDS} for e in raw] + return {"status": "ok", "events": events, "active": sum(1 for e in events if e.get("status") == "active")} + + # -- live stream (fan-out feed, scoped + scrubbed) --------------------------- def _scrub_frame(raw: str) -> str: diff --git a/templates/portal/location.html b/templates/portal/location.html index 1a3148c..7ebe04e 100644 --- a/templates/portal/location.html +++ b/templates/portal/location.html @@ -16,6 +16,13 @@ No device is currently assigned to this location. {% else %} +
Lp (Instant)
@@ -48,6 +55,12 @@
+ + +
+

Alert history

+
+
{% endif %} {% endblock %} @@ -203,10 +216,45 @@ document.addEventListener('visibilitychange', () => { }); window.addEventListener('beforeunload', closeStream); +// ---- alert history + current-alarm banner (read-only) -------------------- +const EV_METRIC = { leq: 'Leq', lp: 'Lp', lmax: 'Lmax', lpeak: 'Lpeak', ln1: 'L1', ln2: 'L10' }; +function fmtAlertTime(iso) { return iso ? new Date(iso.endsWith('Z') ? iso : iso + 'Z').toLocaleString() : ''; } + +async function loadEvents() { + try { + const j = await (await fetch(`/portal/api/location/${encodeURIComponent(LOC_ID)}/events?limit=20`)).json(); + const events = j.events || []; + const banner = document.getElementById('p-alarm-banner'); + if (j.active) { + banner.classList.remove('hidden'); + document.getElementById('p-alarm-text').textContent = + j.active > 1 ? `${j.active} alerts currently active` : 'Currently above threshold.'; + } else banner.classList.add('hidden'); + const list = document.getElementById('p-events'); + if (!events.length) { list.innerHTML = '
No alerts have fired.
'; return; } + list.innerHTML = ''; + for (const e of events) { + const m = EV_METRIC[e.metric] || e.metric; + const active = e.status === 'active'; + const when = active ? `since ${fmtAlertTime(e.onset_at)}` + : `${fmtAlertTime(e.onset_at)} → ${fmtAlertTime(e.clear_at)}`; + const peak = (e.peak_value != null) ? ` · peak ${e.peak_value} dB` : ''; + const row = document.createElement('div'); + row.className = 'px-3 py-2 rounded-lg border text-sm ' + + (active ? 'border-red-700/60 bg-red-900/20 text-red-200' : 'border-slate-700 bg-slate-800/40 text-gray-300'); + row.innerHTML = `
${e.rule_name || 'Alert'} · ${m} ${e.threshold_db} dB
+
${when}${peak}
`; + list.appendChild(row); + } + } catch (e) { /* leave history as-is */ } +} + initChart(); prefill(); // instant first paint from cache backfill(); // seed the chart trail openStream(); // then upgrade to the live feed +loadEvents(); +setInterval(loadEvents, 20000); {% endif %} {% endblock %} diff --git a/templates/slm_detail.html b/templates/slm_detail.html index def5ed4..abeccbe 100644 --- a/templates/slm_detail.html +++ b/templates/slm_detail.html @@ -117,7 +117,9 @@
-

Alerts

+

Alerts + +

Threshold rules evaluated on this device's live feed.

+ + +
+
+

History

+ +
+
+
{% endblock %}