feat(portal): show the client their active alert limits
New scoped GET /portal/api/location/{id}/thresholds returns the enabled alert
rules (scrubbed: name/metric/comparison/threshold/duration/schedule — no cooldown
or hysteresis internals). Location page renders an "Alert limits" panel above the
history, e.g. "Night noise · Leq above 65 dB for 60s · 22:00–07:00", hidden when
no limits are set. Gives the breach history context.
Verified: portal.py compiles; location script balances; template parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,8 +69,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert limits (what this location is alerted on) -->
|
||||
<div id="p-limits-section" class="reveal mt-7 hidden" style="animation-delay:180ms">
|
||||
<div class="text-[10px] uppercase tracking-[0.22em] text-[var(--text-dim)] font-mono mb-3">Alert limits</div>
|
||||
<div id="p-thresholds" class="space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Alert history -->
|
||||
<div class="reveal mt-7" style="animation-delay:180ms">
|
||||
<div class="reveal mt-7" style="animation-delay:220ms">
|
||||
<div class="text-[10px] uppercase tracking-[0.22em] text-[var(--text-dim)] font-mono mb-3">Alert history</div>
|
||||
<div id="p-events" class="space-y-2"></div>
|
||||
</div>
|
||||
@@ -260,6 +266,35 @@ window.addEventListener('beforeunload', closeStream);
|
||||
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() : ''; }
|
||||
|
||||
// ---- alert limits (the active thresholds, read-only) ---------------------
|
||||
function fmtThreshold(r) {
|
||||
const m = EV_METRIC[r.metric] || r.metric;
|
||||
const cmp = r.comparison === 'below' ? 'below' : 'above';
|
||||
let s = `${m} ${cmp} ${r.threshold_db} dB`;
|
||||
if (r.duration_s) s += ` for ${r.duration_s}s`;
|
||||
if (r.schedule_start && r.schedule_end) s += ` · ${r.schedule_start}–${r.schedule_end}`;
|
||||
return s;
|
||||
}
|
||||
async function loadThresholds() {
|
||||
const sec = document.getElementById('p-limits-section');
|
||||
try {
|
||||
const j = await (await fetch(`/portal/api/location/${encodeURIComponent(LOC_ID)}/thresholds`)).json();
|
||||
const rules = j.rules || [];
|
||||
if (!rules.length) { sec.classList.add('hidden'); return; }
|
||||
const list = document.getElementById('p-thresholds');
|
||||
list.innerHTML = '';
|
||||
for (const r of rules) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'panel px-3.5 py-2.5 text-sm flex items-center gap-2.5';
|
||||
row.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-seismo-orange shrink-0"></span>
|
||||
<span class="text-[var(--text)]">${r.name || 'Alert'}</span>
|
||||
<span class="text-[var(--text-dim)] font-mono text-xs">${fmtThreshold(r)}</span>`;
|
||||
list.appendChild(row);
|
||||
}
|
||||
sec.classList.remove('hidden');
|
||||
} catch (e) { sec.classList.add('hidden'); }
|
||||
}
|
||||
|
||||
async function loadEvents() {
|
||||
try {
|
||||
const j = await (await fetch(`/portal/api/location/${encodeURIComponent(LOC_ID)}/events?limit=20`)).json();
|
||||
@@ -293,6 +328,7 @@ prefill(); // instant first paint from cache
|
||||
backfill(); // seed the chart trail
|
||||
openStream(); // then upgrade to the live feed
|
||||
loadEvents();
|
||||
loadThresholds();
|
||||
setInterval(loadEvents, 20000);
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user