/**
* System Health – Enhanced Dashboard IIFE module
*
* Streams real-time system metrics via SSE with rich visualizations:
* SVG arc gauge, per-core bars, temperature sparkline, network bandwidth,
* disk I/O, 3D globe, weather, and process grid.
*/
const SystemHealth = (function () {
'use strict';
let eventSource = null;
let connected = false;
let lastMetrics = null;
// Temperature sparkline ring buffer (last 20 readings)
const SPARKLINE_SIZE = 20;
let tempHistory = [];
// Network I/O delta tracking
let prevNetIo = null;
let prevNetTimestamp = null;
// Disk I/O delta tracking
let prevDiskIo = null;
let prevDiskTimestamp = null;
// Location & weather state
let locationData = null;
let weatherData = null;
let weatherTimer = null;
let globeInstance = null;
let globeDestroyed = false;
const GLOBE_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/globe.gl@2.33.1/dist/globe.gl.min.js';
const GLOBE_TEXTURE_URL = '/static/images/globe/earth-dark.jpg';
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function formatBytes(bytes) {
if (bytes == null) return '--';
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = 0;
var val = bytes;
while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
return val.toFixed(1) + ' ' + units[i];
}
function formatRate(bytesPerSec) {
if (bytesPerSec == null) return '--';
return formatBytes(bytesPerSec) + '/s';
}
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 ';
var cls = barClass(pct);
var rounded = Math.round(pct);
return '
' +
(label ? '
' + label + ' ' : '') +
'
' +
'
' + rounded + '% ' +
'
';
}
function escHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// -----------------------------------------------------------------------
// SVG Arc Gauge
// -----------------------------------------------------------------------
function arcGaugeSvg(pct) {
var radius = 36;
var cx = 45, cy = 45;
var startAngle = -225;
var endAngle = 45;
var totalAngle = endAngle - startAngle; // 270 degrees
var fillAngle = startAngle + (totalAngle * Math.min(pct, 100) / 100);
function polarToCart(angle) {
var r = angle * Math.PI / 180;
return { x: cx + radius * Math.cos(r), y: cy + radius * Math.sin(r) };
}
var bgStart = polarToCart(startAngle);
var bgEnd = polarToCart(endAngle);
var fillEnd = polarToCart(fillAngle);
var largeArcBg = totalAngle > 180 ? 1 : 0;
var fillArc = (fillAngle - startAngle) > 180 ? 1 : 0;
var cls = barClass(pct);
return '' +
' ' +
' ' +
' ';
}
// -----------------------------------------------------------------------
// Temperature Sparkline
// -----------------------------------------------------------------------
function sparklineSvg(values) {
if (!values || values.length < 2) return '';
var w = 200, h = 40;
var min = Math.min.apply(null, values);
var max = Math.max.apply(null, values);
var range = max - min || 1;
var step = w / (values.length - 1);
var points = values.map(function (v, i) {
var x = Math.round(i * step);
var y = Math.round(h - ((v - min) / range) * (h - 4) - 2);
return x + ',' + y;
});
var areaPoints = points.join(' ') + ' ' + w + ',' + h + ' 0,' + h;
return '' +
'' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ';
}
// -----------------------------------------------------------------------
// Rendering — CPU Card
// -----------------------------------------------------------------------
function renderCpuCard(m) {
var el = document.getElementById('sysCardCpu');
if (!el) return;
var cpu = m.cpu;
if (!cpu) { el.innerHTML = 'psutil not available
'; return; }
var pct = Math.round(cpu.percent);
var coreHtml = '';
if (cpu.per_core && cpu.per_core.length) {
coreHtml = '';
cpu.per_core.forEach(function (c) {
var cls = barClass(c);
var h = Math.max(3, Math.round(c / 100 * 48));
coreHtml += '
';
});
coreHtml += '
';
}
var freqHtml = '';
if (cpu.freq) {
var freqGhz = (cpu.freq.current / 1000).toFixed(2);
freqHtml = 'Freq: ' + freqGhz + ' GHz
';
}
el.innerHTML =
'' +
'' +
'
' +
'
' + arcGaugeSvg(pct) +
'
' + pct + '%
' +
'
' +
'
Load: ' + cpu.load_1 + ' / ' + cpu.load_5 + ' / ' + cpu.load_15 + '
' +
'
Cores: ' + cpu.count + '
' +
freqHtml +
'
' +
coreHtml +
'
';
}
// -----------------------------------------------------------------------
// Memory Card
// -----------------------------------------------------------------------
function renderMemoryCard(m) {
var el = document.getElementById('sysCardMemory');
if (!el) return;
var mem = m.memory;
if (!mem) { el.innerHTML = 'N/A
'; return; }
var swap = m.swap || {};
el.innerHTML =
'' +
'' +
barHtml(mem.percent, 'RAM') +
'
' + formatBytes(mem.used) + ' / ' + formatBytes(mem.total) + '
' +
(swap.total > 0 ? barHtml(swap.percent, 'Swap') +
'
' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '
' : '') +
'
';
}
// -----------------------------------------------------------------------
// Temperature & Power Card
// -----------------------------------------------------------------------
function _extractPrimaryTemp(temps) {
if (!temps) return null;
var preferred = ['cpu_thermal', 'coretemp', 'k10temp', 'acpitz', 'soc_thermal'];
for (var i = 0; i < preferred.length; i++) {
if (temps[preferred[i]] && temps[preferred[i]].length) return temps[preferred[i]][0];
}
for (var key in temps) {
if (temps[key] && temps[key].length) return temps[key][0];
}
return null;
}
function renderTempCard(m) {
var el = document.getElementById('sysCardTemp');
if (!el) return;
var temp = _extractPrimaryTemp(m.temperatures);
var html = '';
if (temp) {
// Update sparkline history
tempHistory.push(temp.current);
if (tempHistory.length > SPARKLINE_SIZE) tempHistory.shift();
html += '
' + Math.round(temp.current) + '°C
';
html += '
' + sparklineSvg(tempHistory) + '
';
// Additional sensors
if (m.temperatures) {
for (var chip in m.temperatures) {
m.temperatures[chip].forEach(function (s) {
html += '
' + escHtml(s.label) + ': ' + Math.round(s.current) + '°C
';
});
}
}
} else {
html += '
No temperature sensors ';
}
// Fans
if (m.fans) {
for (var fChip in m.fans) {
m.fans[fChip].forEach(function (f) {
html += '
Fan ' + escHtml(f.label) + ': ' + f.current + ' RPM
';
});
}
}
// Battery
if (m.battery) {
html += '
' +
'Battery: ' + Math.round(m.battery.percent) + '%' +
(m.battery.plugged ? ' (plugged)' : '') + '
';
}
// Throttle flags (Pi)
if (m.power && m.power.throttled) {
html += '
Throttle: 0x' + m.power.throttled + '
';
}
// Power draw
if (m.power && m.power.draw_watts != null) {
html += '
Power: ' + m.power.draw_watts + ' W
';
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// Disk Card
// -----------------------------------------------------------------------
function renderDiskCard(m) {
var el = document.getElementById('sysCardDisk');
if (!el) return;
var disk = m.disk;
if (!disk) { el.innerHTML = 'N/A
'; return; }
var html = '';
html += barHtml(disk.percent, '');
html += '
' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '
';
// Disk I/O rates
if (m.disk_io && prevDiskIo && prevDiskTimestamp) {
var dt = (m.timestamp - prevDiskTimestamp);
if (dt > 0) {
var readRate = (m.disk_io.read_bytes - prevDiskIo.read_bytes) / dt;
var writeRate = (m.disk_io.write_bytes - prevDiskIo.write_bytes) / dt;
var readIops = Math.round((m.disk_io.read_count - prevDiskIo.read_count) / dt);
var writeIops = Math.round((m.disk_io.write_count - prevDiskIo.write_count) / dt);
html += '
' +
'R: ' + formatRate(Math.max(0, readRate)) + ' ' +
'W: ' + formatRate(Math.max(0, writeRate)) + ' ' +
'
';
html += '
IOPS: ' + Math.max(0, readIops) + 'r / ' + Math.max(0, writeIops) + 'w
';
}
}
if (m.disk_io) {
prevDiskIo = m.disk_io;
prevDiskTimestamp = m.timestamp;
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// Network Card
// -----------------------------------------------------------------------
function renderNetworkCard(m) {
var el = document.getElementById('sysCardNetwork');
if (!el) return;
var net = m.network;
if (!net) { el.innerHTML = 'N/A
'; return; }
var html = '';
// Interfaces
var ifaces = net.interfaces || [];
if (ifaces.length === 0) {
html += '
No interfaces ';
} else {
ifaces.forEach(function (iface) {
html += '
';
html += '
' + escHtml(iface.name) +
(iface.is_up ? '' : ' (down) ') + '
';
if (iface.ipv4) html += '
' + escHtml(iface.ipv4) + '
';
var details = [];
if (iface.mac) details.push('MAC: ' + iface.mac);
if (iface.speed) details.push(iface.speed + ' Mbps');
if (details.length) html += '
' + escHtml(details.join(' | ')) + '
';
// Bandwidth for this interface
if (net.io && net.io[iface.name] && prevNetIo && prevNetIo[iface.name] && prevNetTimestamp) {
var dt = (m.timestamp - prevNetTimestamp);
if (dt > 0) {
var prev = prevNetIo[iface.name];
var cur = net.io[iface.name];
var upRate = (cur.bytes_sent - prev.bytes_sent) / dt;
var downRate = (cur.bytes_recv - prev.bytes_recv) / dt;
html += '
' +
'↑ ' + formatRate(Math.max(0, upRate)) + ' ' +
'↓ ' + formatRate(Math.max(0, downRate)) + ' ' +
'
';
}
}
html += '
';
});
}
// Connection count
if (net.connections != null) {
html += '
Connections: ' + net.connections + '
';
}
// Save for next delta
if (net.io) {
prevNetIo = net.io;
prevNetTimestamp = m.timestamp;
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// Location & Weather Card
// -----------------------------------------------------------------------
function renderLocationCard() {
var el = document.getElementById('sysCardLocation');
if (!el) return;
// Preserve the globe DOM node if it already has a canvas
var existingGlobe = document.getElementById('sysGlobeContainer');
var savedGlobe = null;
if (existingGlobe && existingGlobe.querySelector('canvas')) {
savedGlobe = existingGlobe;
existingGlobe.parentNode.removeChild(existingGlobe);
}
var html = '';
html += '
';
// Globe placeholder (will be replaced with saved node or initialized fresh)
if (!savedGlobe) {
html += '
';
} else {
html += '
';
}
// Details below globe
html += '
';
if (locationData && locationData.lat != null) {
html += '
' +
locationData.lat.toFixed(4) + '°' + (locationData.lat >= 0 ? 'N' : 'S') + ', ' +
locationData.lon.toFixed(4) + '°' + (locationData.lon >= 0 ? 'E' : 'W') + '
';
// GPS status indicator
if (locationData.source === 'gps' && locationData.gps) {
var gps = locationData.gps;
var fixLabel = gps.fix_quality === 3 ? '3D Fix' : '2D Fix';
var dotCls = gps.fix_quality === 3 ? 'fix-3d' : 'fix-2d';
html += '
' +
' ' + fixLabel;
if (gps.satellites != null) html += ' · ' + gps.satellites + ' sats';
if (gps.accuracy != null) html += ' · ±' + gps.accuracy + 'm';
html += '
';
} else {
html += '
Source: ' + escHtml(locationData.source || 'unknown') + '
';
}
} else {
html += '
No location
';
}
// Weather
if (weatherData && !weatherData.error) {
html += '
';
html += '
' + (weatherData.temp_c || '--') + '°C
';
html += '
' + escHtml(weatherData.condition || '') + '
';
var details = [];
if (weatherData.humidity) details.push('Humidity: ' + weatherData.humidity + '%');
if (weatherData.wind_mph) details.push('Wind: ' + weatherData.wind_mph + ' mph ' + (weatherData.wind_dir || ''));
if (weatherData.feels_like_c) details.push('Feels like: ' + weatherData.feels_like_c + '°C');
details.forEach(function (d) {
html += '
' + escHtml(d) + '
';
});
html += '
';
} else if (weatherData && weatherData.error) {
html += '
';
}
html += '
'; // .sys-location-details
html += '
'; // .sys-location-inner
html += '
';
el.innerHTML = html;
// Re-insert saved globe or initialize fresh
if (savedGlobe) {
var placeholder = document.getElementById('sysGlobePlaceholder');
if (placeholder) placeholder.parentNode.replaceChild(savedGlobe, placeholder);
} else {
requestAnimationFrame(function () { initGlobe(); });
}
}
// -----------------------------------------------------------------------
// Globe (reuses globe.gl from GPS mode)
// -----------------------------------------------------------------------
function ensureGlobeLibrary() {
return new Promise(function (resolve, reject) {
if (typeof window.Globe === 'function') { resolve(true); return; }
// Check if script already exists
var existing = document.querySelector(
'script[data-intercept-globe-src="' + GLOBE_SCRIPT_URL + '"], ' +
'script[src="' + GLOBE_SCRIPT_URL + '"]'
);
if (existing) {
if (existing.dataset.loaded === 'true') { resolve(true); return; }
if (existing.dataset.failed === 'true') { resolve(false); return; }
existing.addEventListener('load', function () { resolve(true); }, { once: true });
existing.addEventListener('error', function () { resolve(false); }, { once: true });
return;
}
var script = document.createElement('script');
script.src = GLOBE_SCRIPT_URL;
script.async = true;
script.crossOrigin = 'anonymous';
script.dataset.interceptGlobeSrc = GLOBE_SCRIPT_URL;
script.onload = function () { script.dataset.loaded = 'true'; resolve(true); };
script.onerror = function () { script.dataset.failed = 'true'; resolve(false); };
document.head.appendChild(script);
});
}
function initGlobe() {
var container = document.getElementById('sysGlobeContainer');
if (!container || globeDestroyed) return;
// Don't reinitialize if globe canvas is still alive in this container
if (globeInstance && container.querySelector('canvas')) return;
// Clear stale reference if canvas was destroyed by innerHTML replacement
if (globeInstance && !container.querySelector('canvas')) {
globeInstance = null;
}
ensureGlobeLibrary().then(function (ready) {
if (!ready || typeof window.Globe !== 'function' || globeDestroyed) return;
// Wait for layout — container may have 0 dimensions right after
// display:none is removed by switchMode(). Use RAF retry like GPS mode.
var attempts = 0;
function tryInit() {
if (globeDestroyed) return;
container = document.getElementById('sysGlobeContainer');
if (!container) return;
if ((!container.clientWidth || !container.clientHeight) && attempts < 8) {
attempts++;
requestAnimationFrame(tryInit);
return;
}
if (!container.clientWidth || !container.clientHeight) return;
container.innerHTML = '';
container.style.background = 'radial-gradient(circle, rgba(10,20,40,0.9), rgba(2,4,8,0.98) 70%)';
try {
globeInstance = window.Globe()(container)
.backgroundColor('rgba(0,0,0,0)')
.globeImageUrl(GLOBE_TEXTURE_URL)
.showAtmosphere(true)
.atmosphereColor('#3bb9ff')
.atmosphereAltitude(0.12)
.pointsData([])
.pointRadius(0.8)
.pointAltitude(0.01)
.pointColor(function () { return '#00d4ff'; });
var controls = globeInstance.controls();
if (controls) {
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
controls.enablePan = false;
controls.minDistance = 120;
controls.maxDistance = 300;
}
// Size the globe
globeInstance.width(container.clientWidth);
globeInstance.height(container.clientHeight);
updateGlobePosition();
} catch (e) {
// Globe.gl / WebGL init failed — show static fallback
container.innerHTML = 'Globe unavailable
';
}
}
requestAnimationFrame(tryInit);
});
}
function updateGlobePosition() {
if (!globeInstance || !locationData || locationData.lat == null) return;
// Observer point
globeInstance.pointsData([{
lat: locationData.lat,
lng: locationData.lon,
size: 0.8,
color: '#00d4ff',
}]);
// Snap view
globeInstance.pointOfView({ lat: locationData.lat, lng: locationData.lon, altitude: 2.0 }, 1000);
// Stop auto-rotate when we have a fix
var controls = globeInstance.controls();
if (controls) controls.autoRotate = false;
}
function destroyGlobe() {
globeDestroyed = true;
if (globeInstance) {
var container = document.getElementById('sysGlobeContainer');
if (container) container.innerHTML = '';
globeInstance = null;
}
}
// -----------------------------------------------------------------------
// SDR Card
// -----------------------------------------------------------------------
function renderSdrCard(devices) {
var el = document.getElementById('sysCardSdr');
if (!el) return;
var html = '';
html += '';
if (!devices || !devices.length) {
html += '
No devices found ';
} else {
devices.forEach(function (d) {
html += '
' +
'
' +
'
' + escHtml(d.type) + ' #' + d.index + ' ' +
'
' + escHtml(d.name || 'Unknown') + '
' +
(d.serial ? '
S/N: ' + escHtml(d.serial) + '
' : '') +
'
';
});
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// Process Card
// -----------------------------------------------------------------------
function renderProcessCard(m) {
var el = document.getElementById('sysCardProcesses');
if (!el) return;
var procs = m.processes || {};
var keys = Object.keys(procs).sort();
var html = '';
if (!keys.length) {
html += '
No data ';
} else {
var running = 0, stopped = 0;
html += '
';
keys.forEach(function (k) {
var isRunning = procs[k];
if (isRunning) running++; else stopped++;
var dotCls = isRunning ? 'running' : 'stopped';
var label = k.charAt(0).toUpperCase() + k.slice(1);
html += '
' +
' ' +
'' + escHtml(label) + ' ' +
'
';
});
html += '
';
html += '
' + running + ' running / ' + stopped + ' idle
';
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// System Info Card
// -----------------------------------------------------------------------
function renderSystemInfoCard(m) {
var el = document.getElementById('sysCardInfo');
if (!el) return;
var sys = m.system || {};
var html = '';
html += '
Host ' + escHtml(sys.hostname || '--') + '
';
html += '
OS ' + escHtml((sys.platform || '--').replace(/-with-glibc[\d.]+/, '')) + '
';
html += '
Python ' + escHtml(sys.python || '--') + '
';
html += '
App v' + escHtml(sys.version || '--') + '
';
html += '
Uptime ' + escHtml(sys.uptime_human || '--') + '
';
if (m.boot_time) {
var bootDate = new Date(m.boot_time * 1000);
html += '
Boot ' + escHtml(bootDate.toLocaleString()) + '
';
}
if (m.network && m.network.connections != null) {
html += '
Connections ' + m.network.connections + '
';
}
html += '
';
el.innerHTML = html;
}
// -----------------------------------------------------------------------
// Sidebar Updates
// -----------------------------------------------------------------------
function updateSidebarQuickStats(m) {
var cpuEl = document.getElementById('sysQuickCpu');
var tempEl = document.getElementById('sysQuickTemp');
var ramEl = document.getElementById('sysQuickRam');
var 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) + '%' : '--';
var 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;
var 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) {
var el = document.getElementById('sysProcessList');
if (!el) return;
var procs = m.processes || {};
var keys = Object.keys(procs).sort();
if (!keys.length) { el.textContent = 'No data'; return; }
var running = keys.filter(function (k) { return procs[k]; });
var 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 updateSidebarNetwork(m) {
var el = document.getElementById('sysQuickNet');
if (!el || !m.network) return;
var ifaces = m.network.interfaces || [];
var ips = [];
ifaces.forEach(function (iface) {
if (iface.ipv4 && iface.is_up) {
ips.push(iface.name + ': ' + iface.ipv4);
}
});
el.textContent = ips.length ? ips.join(', ') : '--';
}
function updateSidebarBattery(m) {
var section = document.getElementById('sysQuickBatterySection');
var el = document.getElementById('sysQuickBattery');
if (!section || !el) return;
if (m.battery) {
section.style.display = '';
el.textContent = Math.round(m.battery.percent) + '%' + (m.battery.plugged ? ' (plugged)' : '');
} else {
section.style.display = 'none';
}
}
function updateSidebarLocation() {
var el = document.getElementById('sysQuickLocation');
if (!el) return;
if (locationData && locationData.lat != null) {
el.textContent = locationData.lat.toFixed(4) + ', ' + locationData.lon.toFixed(4) + ' (' + locationData.source + ')';
} else {
el.textContent = 'No location';
}
}
// -----------------------------------------------------------------------
// Render all
// -----------------------------------------------------------------------
function renderAll(m) {
renderCpuCard(m);
renderMemoryCard(m);
renderTempCard(m);
renderDiskCard(m);
renderNetworkCard(m);
renderProcessCard(m);
renderSystemInfoCard(m);
updateSidebarQuickStats(m);
updateSidebarProcesses(m);
updateSidebarNetwork(m);
updateSidebarBattery(m);
}
// -----------------------------------------------------------------------
// Location & Weather Fetching
// -----------------------------------------------------------------------
function fetchLocation() {
fetch('/system/location')
.then(function (r) { return r.json(); })
.then(function (data) {
// If server only has default/none, check client-side saved location
if ((data.source === 'default' || data.source === 'none') &&
window.ObserverLocation && ObserverLocation.getShared) {
var shared = ObserverLocation.getShared();
if (shared && shared.lat && shared.lon) {
data.lat = shared.lat;
data.lon = shared.lon;
data.source = 'manual';
}
}
locationData = data;
updateSidebarLocation();
renderLocationCard();
if (data.lat != null) fetchWeather();
})
.catch(function () {
renderLocationCard();
});
}
function fetchWeather() {
if (!locationData || locationData.lat == null) return;
fetch('/system/weather?lat=' + locationData.lat + '&lon=' + locationData.lon)
.then(function (r) { return r.json(); })
.then(function (data) {
weatherData = data;
renderLocationCard();
})
.catch(function () {});
}
// -----------------------------------------------------------------------
// 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 = '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 += ' ' +
escHtml(d.type) + ' #' + d.index + ' — ' + escHtml(d.name || 'Unknown') + '
';
});
sidebarEl.innerHTML = html;
}
}
})
.catch(function () {
if (sidebarEl) sidebarEl.innerHTML = 'Detection failed ';
renderSdrCard([]);
});
}
// -----------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------
function init() {
globeDestroyed = false;
connect();
refreshSdr();
fetchLocation();
// Refresh weather every 10 minutes
weatherTimer = setInterval(function () {
fetchWeather();
}, 600000);
}
function destroy() {
disconnect();
destroyGlobe();
if (weatherTimer) {
clearInterval(weatherTimer);
weatherTimer = null;
}
}
return {
init: init,
destroy: destroy,
refreshSdr: refreshSdr,
};
})();