feat(admin): per-unit live-monitoring (keepalive) toggle on /admin/slmm

Adds a "Live Monitoring (keepalive)" card listing each SLMM device with its
monitor_enabled state and an Enable/Disable toggle. Reads from /api/slmm/roster
(now includes monitor_enabled) and POSTs to /api/slmm/{unit}/monitor/{start,stop},
which persist the flag in SLMM (survives restarts; auto-started on boot). Shows a
reachability dot + 24/7 ON/OFF badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 19:28:55 +00:00
parent 3b818dcd97
commit bdc91177e2
+66 -1
View File
@@ -42,6 +42,18 @@
</div> </div>
</div> </div>
<!-- Live Monitoring (keepalive) -->
<div class="rounded-xl bg-white dark:bg-slate-800 shadow-lg p-4 mb-6">
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-1">Live Monitoring (keepalive)</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
Keepalive runs the 1&nbsp;Hz DOD feed 24/7 (even with no viewer), which powers the live-chart
trail and continuous threshold alerts. Toggling persists and survives restarts.
</p>
<div id="monitor-list" class="text-sm">
<p class="text-gray-500 dark:text-gray-400">Loading…</p>
</div>
</div>
<!-- Raw API tester --> <!-- Raw API tester -->
<div class="rounded-xl bg-white dark:bg-slate-800 shadow-lg p-4 mb-6"> <div class="rounded-xl bg-white dark:bg-slate-800 shadow-lg p-4 mb-6">
<h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-3">Raw API Tester</h2> <h2 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-3">Raw API Tester</h2>
@@ -132,7 +144,60 @@ async function sendRaw() {
} }
} }
async function loadMonitors() {
const el = document.getElementById('monitor-list');
try {
const r = await fetch('/api/slmm/roster');
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
const devices = d.devices || [];
if (!devices.length) {
el.innerHTML = '<p class="text-gray-500 dark:text-gray-400">No devices configured.</p>';
return;
}
el.innerHTML = devices.map(dev => {
const on = !!dev.monitor_enabled;
const reach = dev.status ? dev.status.is_reachable : null;
const reachDot = reach === false
? '<span class="w-2 h-2 rounded-full bg-red-500 inline-block" title="unreachable"></span>'
: '<span class="w-2 h-2 rounded-full bg-green-500 inline-block" title="reachable"></span>';
return `
<div class="flex items-center justify-between border-b border-gray-100 dark:border-gray-700 py-2">
<div class="flex items-center gap-2">
${reachDot}
<span class="font-mono text-gray-900 dark:text-white">${_esc(dev.unit_id)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">${_esc(dev.host)}:${_esc(dev.tcp_port)}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-xs font-medium px-2 py-0.5 rounded ${on
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: 'bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-400'}">${on ? '24/7 ON' : 'OFF'}</span>
<button onclick="toggleMonitor('${_esc(dev.unit_id)}', ${!on})"
class="px-3 py-1 text-xs rounded text-white ${on
? 'bg-red-600 hover:bg-red-700' : 'bg-seismo-orange hover:bg-orange-600'}">
${on ? 'Disable' : 'Enable'}
</button>
</div>
</div>`;
}).join('');
} catch (e) {
el.innerHTML = `<p class="text-red-600 dark:text-red-400">Failed to load devices: ${_esc(e.message)}</p>`;
}
}
async function toggleMonitor(unitId, enable) {
const action = enable ? 'start' : 'stop';
try {
const r = await fetch(`/api/slmm/${encodeURIComponent(unitId)}/monitor/${action}`, { method: 'POST' });
if (!r.ok) throw new Error('HTTP ' + r.status);
await loadMonitors();
} catch (e) {
alert('Toggle failed: ' + e.message);
}
}
loadSlmmOverview(); loadSlmmOverview();
setInterval(loadSlmmOverview, 30000); loadMonitors();
setInterval(() => { loadSlmmOverview(); loadMonitors(); }, 30000);
</script> </script>
{% endblock %} {% endblock %}