/** * System Health – IIFE module * * Always-on monitoring that auto-connects when the mode is entered. * Streams real-time system metrics via SSE and provides SDR device enumeration. */ const SystemHealth = (function () { 'use strict'; let eventSource = null; let connected = false; let lastMetrics = null; // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- function formatBytes(bytes) { if (bytes == null) return '--'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; let val = bytes; while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; } return val.toFixed(1) + ' ' + units[i]; } function barClass(pct) { if (pct >= 85) return 'crit'; if (pct >= 60) return 'warn'; return 'ok'; } function barHtml(pct, label) { if (pct == null) return 'N/A'; const cls = barClass(pct); const rounded = Math.round(pct); return '
' + (label ? '' + label + '' : '') + '
' + '' + rounded + '%' + '
'; } // ----------------------------------------------------------------------- // Rendering // ----------------------------------------------------------------------- function renderCpuCard(m) { const el = document.getElementById('sysCardCpu'); if (!el) return; const cpu = m.cpu; if (!cpu) { el.innerHTML = '
psutil not available
'; return; } el.innerHTML = '
CPU
' + '
' + barHtml(cpu.percent, '') + '
Load: ' + cpu.load_1 + ' / ' + cpu.load_5 + ' / ' + cpu.load_15 + '
' + '
Cores: ' + cpu.count + '
' + '
'; } function renderMemoryCard(m) { const el = document.getElementById('sysCardMemory'); if (!el) return; const mem = m.memory; if (!mem) { el.innerHTML = '
N/A
'; return; } const swap = m.swap || {}; el.innerHTML = '
Memory
' + '
' + barHtml(mem.percent, '') + '
' + formatBytes(mem.used) + ' / ' + formatBytes(mem.total) + '
' + '
Swap: ' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '
' + '
'; } function renderDiskCard(m) { const el = document.getElementById('sysCardDisk'); if (!el) return; const disk = m.disk; if (!disk) { el.innerHTML = '
N/A
'; return; } el.innerHTML = '
Disk
' + '
' + barHtml(disk.percent, '') + '
' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '
' + '
Path: ' + (disk.path || '/') + '
' + '
'; } function _extractPrimaryTemp(temps) { if (!temps) return null; // Prefer common chip names const preferred = ['cpu_thermal', 'coretemp', 'k10temp', 'acpitz', 'soc_thermal']; for (const name of preferred) { if (temps[name] && temps[name].length) return temps[name][0]; } // Fall back to first available for (const key of Object.keys(temps)) { if (temps[key] && temps[key].length) return temps[key][0]; } return null; } function renderSdrCard(devices) { const el = document.getElementById('sysCardSdr'); if (!el) return; let html = '
SDR Devices
'; html += '
'; if (!devices || !devices.length) { html += 'No devices found'; } else { devices.forEach(function (d) { html += '
' + ' ' + '' + d.type + ' #' + d.index + '' + '
' + (d.name || 'Unknown') + '
' + (d.serial ? '
S/N: ' + d.serial + '
' : '') + '
'; }); } html += '
'; el.innerHTML = html; } function renderProcessCard(m) { const el = document.getElementById('sysCardProcesses'); if (!el) return; const procs = m.processes || {}; const keys = Object.keys(procs).sort(); let html = '
Processes
'; if (!keys.length) { html += 'No data'; } else { keys.forEach(function (k) { const running = procs[k]; const dotCls = running ? 'running' : 'stopped'; const label = k.charAt(0).toUpperCase() + k.slice(1); html += '
' + ' ' + '' + label + '' + '
'; }); } html += '
'; el.innerHTML = html; } function renderSystemInfoCard(m) { const el = document.getElementById('sysCardInfo'); if (!el) return; const sys = m.system || {}; const temp = _extractPrimaryTemp(m.temperatures); let html = '
System Info
'; html += '
Host: ' + (sys.hostname || '--') + '
'; html += '
OS: ' + (sys.platform || '--') + '
'; html += '
Python: ' + (sys.python || '--') + '
'; html += '
App: v' + (sys.version || '--') + '
'; html += '
Uptime: ' + (sys.uptime_human || '--') + '
'; if (temp) { html += '
Temp: ' + Math.round(temp.current) + '°C'; if (temp.high) html += ' / ' + Math.round(temp.high) + '°C max'; html += '
'; } html += '
'; el.innerHTML = html; } function updateSidebarQuickStats(m) { const cpuEl = document.getElementById('sysQuickCpu'); const tempEl = document.getElementById('sysQuickTemp'); const ramEl = document.getElementById('sysQuickRam'); const diskEl = document.getElementById('sysQuickDisk'); if (cpuEl) cpuEl.textContent = m.cpu ? Math.round(m.cpu.percent) + '%' : '--'; if (ramEl) ramEl.textContent = m.memory ? Math.round(m.memory.percent) + '%' : '--'; if (diskEl) diskEl.textContent = m.disk ? Math.round(m.disk.percent) + '%' : '--'; const temp = _extractPrimaryTemp(m.temperatures); if (tempEl) tempEl.innerHTML = temp ? Math.round(temp.current) + '°C' : '--'; // Color-code values [cpuEl, ramEl, diskEl].forEach(function (el) { if (!el) return; const val = parseInt(el.textContent); el.classList.remove('sys-val-ok', 'sys-val-warn', 'sys-val-crit'); if (!isNaN(val)) el.classList.add('sys-val-' + barClass(val)); }); } function updateSidebarProcesses(m) { const el = document.getElementById('sysProcessList'); if (!el) return; const procs = m.processes || {}; const keys = Object.keys(procs).sort(); if (!keys.length) { el.textContent = 'No data'; return; } const running = keys.filter(function (k) { return procs[k]; }); const stopped = keys.filter(function (k) { return !procs[k]; }); el.innerHTML = (running.length ? '' + running.length + ' running' : '') + (running.length && stopped.length ? ' · ' : '') + (stopped.length ? '' + stopped.length + ' stopped' : ''); } function renderAll(m) { renderCpuCard(m); renderMemoryCard(m); renderDiskCard(m); renderProcessCard(m); renderSystemInfoCard(m); updateSidebarQuickStats(m); updateSidebarProcesses(m); } // ----------------------------------------------------------------------- // SSE Connection // ----------------------------------------------------------------------- function connect() { if (eventSource) return; eventSource = new EventSource('/system/stream'); eventSource.onmessage = function (e) { try { var data = JSON.parse(e.data); if (data.type === 'keepalive') return; lastMetrics = data; renderAll(data); } catch (_) { /* ignore parse errors */ } }; eventSource.onopen = function () { connected = true; }; eventSource.onerror = function () { connected = false; }; } function disconnect() { if (eventSource) { eventSource.close(); eventSource = null; } connected = false; } // ----------------------------------------------------------------------- // SDR Devices // ----------------------------------------------------------------------- function refreshSdr() { var sidebarEl = document.getElementById('sysSdrList'); if (sidebarEl) sidebarEl.innerHTML = 'Scanning…'; var cardEl = document.getElementById('sysCardSdr'); if (cardEl) cardEl.innerHTML = '
SDR Devices
Scanning…
'; fetch('/system/sdr_devices') .then(function (r) { return r.json(); }) .then(function (data) { var devices = data.devices || []; renderSdrCard(devices); // Update sidebar if (sidebarEl) { if (!devices.length) { sidebarEl.innerHTML = 'No SDR devices found'; } else { var html = ''; devices.forEach(function (d) { html += '
' + d.type + ' #' + d.index + ' — ' + (d.name || 'Unknown') + '
'; }); sidebarEl.innerHTML = html; } } }) .catch(function () { if (sidebarEl) sidebarEl.innerHTML = 'Detection failed'; renderSdrCard([]); }); } // ----------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------- function init() { connect(); refreshSdr(); } function destroy() { disconnect(); } return { init: init, destroy: destroy, refreshSdr: refreshSdr, }; })();