mirror of
https://github.com/smittix/intercept.git
synced 2026-04-30 17:49:58 -07:00
Merge upstream/main and resolve acars, vdl2, dashboard conflicts
Resolved conflicts: - routes/acars.py: keep /messages and /clear endpoints for history reload - routes/vdl2.py: keep /messages and /clear endpoints for history reload - templates/adsb_dashboard.html: keep removal of hardcoded device-1 defaults for ACARS/VDL2 selectors (users pick their own device) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1909,7 +1909,42 @@ const BtLocate = (function() {
|
||||
handleDetection,
|
||||
invalidateMap,
|
||||
fetchPairedIrks,
|
||||
destroy,
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear all timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (durationTimer) {
|
||||
clearInterval(durationTimer);
|
||||
durationTimer = null;
|
||||
}
|
||||
if (mapStabilizeTimer) {
|
||||
clearInterval(mapStabilizeTimer);
|
||||
mapStabilizeTimer = null;
|
||||
}
|
||||
if (queuedDetectionTimer) {
|
||||
clearTimeout(queuedDetectionTimer);
|
||||
queuedDetectionTimer = null;
|
||||
}
|
||||
if (crosshairResetTimer) {
|
||||
clearTimeout(crosshairResetTimer);
|
||||
crosshairResetTimer = null;
|
||||
}
|
||||
if (beepTimer) {
|
||||
clearInterval(beepTimer);
|
||||
beepTimer = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
window.BtLocate = BtLocate;
|
||||
|
||||
@@ -117,13 +117,13 @@ const Meshtastic = (function() {
|
||||
Settings.createTileLayer().addTo(meshMap);
|
||||
Settings.registerMap(meshMap);
|
||||
} else {
|
||||
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd',
|
||||
className: 'tile-layer-cyan'
|
||||
}).addTo(meshMap);
|
||||
}
|
||||
L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
||||
maxZoom: 19,
|
||||
subdomains: 'abcd',
|
||||
className: 'tile-layer-cyan'
|
||||
}).addTo(meshMap);
|
||||
}
|
||||
|
||||
// Handle resize
|
||||
setTimeout(() => {
|
||||
@@ -401,10 +401,10 @@ const Meshtastic = (function() {
|
||||
|
||||
// Position is nested in the response
|
||||
const pos = info.position;
|
||||
if (pos && pos.latitude !== undefined && pos.latitude !== null && pos.longitude !== undefined && pos.longitude !== null) {
|
||||
if (posRow) posRow.style.display = 'flex';
|
||||
if (posEl) posEl.textContent = `${pos.latitude.toFixed(5)}, ${pos.longitude.toFixed(5)}`;
|
||||
} else {
|
||||
if (pos && pos.latitude !== undefined && pos.latitude !== null && pos.longitude !== undefined && pos.longitude !== null) {
|
||||
if (posRow) posRow.style.display = 'flex';
|
||||
if (posEl) posEl.textContent = `${pos.latitude.toFixed(5)}, ${pos.longitude.toFixed(5)}`;
|
||||
} else {
|
||||
if (posRow) posRow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -2295,7 +2295,8 @@ const Meshtastic = (function() {
|
||||
// Store & Forward
|
||||
showStoreForwardModal,
|
||||
requestStoreForward,
|
||||
closeStoreForwardModal
|
||||
closeStoreForwardModal,
|
||||
destroy
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2306,6 +2307,13 @@ const Meshtastic = (function() {
|
||||
setTimeout(() => meshMap.invalidateSize(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — tear down SSE, timers, and event listeners for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
stopStream();
|
||||
}
|
||||
})();
|
||||
|
||||
// Initialize when DOM is ready (will be called by selectMode)
|
||||
|
||||
@@ -515,6 +515,13 @@ const SpyStations = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — no-op placeholder for consistent lifecycle interface.
|
||||
*/
|
||||
function destroy() {
|
||||
// SpyStations has no background timers or streams to clean up.
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
@@ -524,7 +531,8 @@ const SpyStations = (function() {
|
||||
showDetails,
|
||||
closeDetails,
|
||||
showHelp,
|
||||
closeHelp
|
||||
closeHelp,
|
||||
destroy
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -858,6 +858,13 @@ const SSTVGeneral = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and stop scope animation for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
stopStream();
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
@@ -869,6 +876,7 @@ const SSTVGeneral = (function() {
|
||||
deleteImage,
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
selectPreset
|
||||
selectPreset,
|
||||
destroy
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -12,12 +12,12 @@ const SSTV = (function() {
|
||||
let progress = 0;
|
||||
let issMap = null;
|
||||
let issMarker = null;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
let issTrackLine = null;
|
||||
let issPosition = null;
|
||||
let issUpdateInterval = null;
|
||||
let countdownInterval = null;
|
||||
let nextPassData = null;
|
||||
let pendingMapInvalidate = false;
|
||||
|
||||
// ISS frequency
|
||||
const ISS_FREQ = 145.800;
|
||||
@@ -38,31 +38,31 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize the SSTV mode
|
||||
*/
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||
setTimeout(() => invalidateMap(), 80);
|
||||
setTimeout(() => invalidateMap(), 260);
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
if (!issMap || typeof issMap.getContainer !== 'function') return false;
|
||||
const container = issMap.getContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function init() {
|
||||
checkStatus();
|
||||
loadImages();
|
||||
loadLocationInputs();
|
||||
loadIssSchedule();
|
||||
initMap();
|
||||
startIssTracking();
|
||||
startCountdown();
|
||||
// Ensure Leaflet recomputes dimensions after the SSTV pane becomes visible.
|
||||
setTimeout(() => invalidateMap(), 80);
|
||||
setTimeout(() => invalidateMap(), 260);
|
||||
}
|
||||
|
||||
function isMapContainerVisible() {
|
||||
if (!issMap || typeof issMap.getContainer !== 'function') return false;
|
||||
const container = issMap.getContainer();
|
||||
if (!container) return false;
|
||||
if (container.offsetWidth <= 0 || container.offsetHeight <= 0) return false;
|
||||
if (container.style && container.style.display === 'none') return false;
|
||||
if (typeof window.getComputedStyle === 'function') {
|
||||
const style = window.getComputedStyle(container);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load location into input fields
|
||||
@@ -189,9 +189,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Initialize Leaflet map for ISS tracking
|
||||
*/
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
async function initMap() {
|
||||
const mapContainer = document.getElementById('sstvIssMap');
|
||||
if (!mapContainer || issMap) return;
|
||||
|
||||
// Create map
|
||||
issMap = L.map('sstvIssMap', {
|
||||
@@ -231,21 +231,21 @@ const SSTV = (function() {
|
||||
issMarker = L.marker([0, 0], { icon: issIcon }).addTo(issMap);
|
||||
|
||||
// Create ground track line
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
});
|
||||
|
||||
// Initial layout passes for first-time mode load.
|
||||
setTimeout(() => invalidateMap(), 40);
|
||||
setTimeout(() => invalidateMap(), 180);
|
||||
}
|
||||
issTrackLine = L.polyline([], {
|
||||
color: '#00d4ff',
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
dashArray: '5, 5'
|
||||
}).addTo(issMap);
|
||||
|
||||
issMap.on('resize moveend zoomend', () => {
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
});
|
||||
|
||||
// Initial layout passes for first-time mode load.
|
||||
setTimeout(() => invalidateMap(), 40);
|
||||
setTimeout(() => invalidateMap(), 180);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start ISS position tracking
|
||||
@@ -454,9 +454,9 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Update map with ISS position
|
||||
*/
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
function updateMap() {
|
||||
if (!issMap || !issPosition) return;
|
||||
if (pendingMapInvalidate) invalidateMap();
|
||||
|
||||
const lat = issPosition.lat;
|
||||
const lon = issPosition.lon;
|
||||
@@ -516,13 +516,13 @@ const SSTV = (function() {
|
||||
issTrackLine.setLatLngs(segments.length > 0 ? segments : []);
|
||||
}
|
||||
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
} else {
|
||||
pendingMapInvalidate = true;
|
||||
}
|
||||
}
|
||||
// Pan map to follow ISS only when the map pane is currently renderable.
|
||||
if (isMapContainerVisible()) {
|
||||
issMap.panTo([lat, lon], { animate: true, duration: 0.5 });
|
||||
} else {
|
||||
pendingMapInvalidate = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check current decoder status
|
||||
@@ -1335,27 +1335,27 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Show status message
|
||||
*/
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ISS map size after pane/layout changes.
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (!issMap) return false;
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingMapInvalidate = true;
|
||||
return false;
|
||||
}
|
||||
issMap.invalidateSize({ pan: false, animate: false });
|
||||
pendingMapInvalidate = false;
|
||||
return true;
|
||||
}
|
||||
function showStatusMessage(message, type) {
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('SSTV', message);
|
||||
} else {
|
||||
console.log(`[SSTV ${type}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate ISS map size after pane/layout changes.
|
||||
*/
|
||||
function invalidateMap() {
|
||||
if (!issMap) return false;
|
||||
if (!isMapContainerVisible()) {
|
||||
pendingMapInvalidate = true;
|
||||
return false;
|
||||
}
|
||||
issMap.invalidateSize({ pan: false, animate: false });
|
||||
pendingMapInvalidate = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
@@ -1370,12 +1370,25 @@ const SSTV = (function() {
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
useGPS,
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown,
|
||||
invalidateMap
|
||||
};
|
||||
})();
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
stopCountdown,
|
||||
invalidateMap,
|
||||
destroy
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear ISS tracking/countdown timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
stopIssTracking();
|
||||
stopCountdown();
|
||||
}
|
||||
})();
|
||||
|
||||
// Initialize when DOM is ready (will be called by selectMode)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* System Health – IIFE module
|
||||
* System Health – Enhanced Dashboard 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.
|
||||
* 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';
|
||||
@@ -11,19 +12,46 @@ const SystemHealth = (function () {
|
||||
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 '--';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0;
|
||||
let val = bytes;
|
||||
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';
|
||||
@@ -32,8 +60,8 @@ const SystemHealth = (function () {
|
||||
|
||||
function barHtml(pct, label) {
|
||||
if (pct == null) return '<span class="sys-metric-na">N/A</span>';
|
||||
const cls = barClass(pct);
|
||||
const rounded = Math.round(pct);
|
||||
var cls = barClass(pct);
|
||||
var rounded = Math.round(pct);
|
||||
return '<div class="sys-metric-bar-wrap">' +
|
||||
(label ? '<span class="sys-metric-bar-label">' + label + '</span>' : '') +
|
||||
'<div class="sys-metric-bar"><div class="sys-metric-bar-fill ' + cls + '" style="width:' + rounded + '%"></div></div>' +
|
||||
@@ -41,71 +69,531 @@ const SystemHealth = (function () {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rendering
|
||||
// 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 '<svg viewBox="0 0 90 90">' +
|
||||
'<path class="arc-bg" d="M ' + bgStart.x + ' ' + bgStart.y +
|
||||
' A ' + radius + ' ' + radius + ' 0 ' + largeArcBg + ' 1 ' + bgEnd.x + ' ' + bgEnd.y + '"/>' +
|
||||
'<path class="arc-fill ' + cls + '" d="M ' + bgStart.x + ' ' + bgStart.y +
|
||||
' A ' + radius + ' ' + radius + ' 0 ' + fillArc + ' 1 ' + fillEnd.x + ' ' + fillEnd.y + '"/>' +
|
||||
'</svg>';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 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 '<svg viewBox="0 0 ' + w + ' ' + h + '" preserveAspectRatio="none">' +
|
||||
'<defs><linearGradient id="sparkGradient" x1="0" y1="0" x2="0" y2="1">' +
|
||||
'<stop offset="0%" stop-color="var(--accent-cyan, #00d4ff)" stop-opacity="0.3"/>' +
|
||||
'<stop offset="100%" stop-color="var(--accent-cyan, #00d4ff)" stop-opacity="0.0"/>' +
|
||||
'</linearGradient></defs>' +
|
||||
'<polygon class="sys-sparkline-area" points="' + areaPoints + '"/>' +
|
||||
'<polyline class="sys-sparkline-line" points="' + points.join(' ') + '"/>' +
|
||||
'</svg>';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rendering — CPU Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderCpuCard(m) {
|
||||
const el = document.getElementById('sysCardCpu');
|
||||
var el = document.getElementById('sysCardCpu');
|
||||
if (!el) return;
|
||||
const cpu = m.cpu;
|
||||
var cpu = m.cpu;
|
||||
if (!cpu) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">psutil not available</span></div>'; return; }
|
||||
|
||||
var pct = Math.round(cpu.percent);
|
||||
var coreHtml = '';
|
||||
if (cpu.per_core && cpu.per_core.length) {
|
||||
coreHtml = '<div class="sys-core-bars">';
|
||||
cpu.per_core.forEach(function (c) {
|
||||
var cls = barClass(c);
|
||||
var h = Math.max(3, Math.round(c / 100 * 48));
|
||||
coreHtml += '<div class="sys-core-bar"><div class="sys-core-bar-fill ' + cls +
|
||||
'" style="height:' + h + 'px;background:var(--accent-' +
|
||||
(cls === 'ok' ? 'green' : cls === 'warn' ? 'yellow' : 'red') +
|
||||
', #00ff88)"></div></div>';
|
||||
});
|
||||
coreHtml += '</div>';
|
||||
}
|
||||
|
||||
var freqHtml = '';
|
||||
if (cpu.freq) {
|
||||
var freqGhz = (cpu.freq.current / 1000).toFixed(2);
|
||||
freqHtml = '<div class="sys-card-detail">Freq: ' + freqGhz + ' GHz</div>';
|
||||
}
|
||||
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">CPU</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(cpu.percent, '') +
|
||||
'<div class="sys-gauge-wrap">' +
|
||||
'<div class="sys-gauge-arc">' + arcGaugeSvg(pct) +
|
||||
'<div class="sys-gauge-label">' + pct + '%</div></div>' +
|
||||
'<div class="sys-gauge-details">' +
|
||||
'<div class="sys-card-detail">Load: ' + cpu.load_1 + ' / ' + cpu.load_5 + ' / ' + cpu.load_15 + '</div>' +
|
||||
'<div class="sys-card-detail">Cores: ' + cpu.count + '</div>' +
|
||||
freqHtml +
|
||||
'</div></div>' +
|
||||
coreHtml +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Memory Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderMemoryCard(m) {
|
||||
const el = document.getElementById('sysCardMemory');
|
||||
var el = document.getElementById('sysCardMemory');
|
||||
if (!el) return;
|
||||
const mem = m.memory;
|
||||
var mem = m.memory;
|
||||
if (!mem) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
const swap = m.swap || {};
|
||||
var swap = m.swap || {};
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">Memory</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(mem.percent, '') +
|
||||
barHtml(mem.percent, 'RAM') +
|
||||
'<div class="sys-card-detail">' + formatBytes(mem.used) + ' / ' + formatBytes(mem.total) + '</div>' +
|
||||
'<div class="sys-card-detail">Swap: ' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '</div>' +
|
||||
(swap.total > 0 ? barHtml(swap.percent, 'Swap') +
|
||||
'<div class="sys-card-detail">' + formatBytes(swap.used) + ' / ' + formatBytes(swap.total) + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderDiskCard(m) {
|
||||
const el = document.getElementById('sysCardDisk');
|
||||
if (!el) return;
|
||||
const disk = m.disk;
|
||||
if (!disk) { el.innerHTML = '<div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
el.innerHTML =
|
||||
'<div class="sys-card-header">Disk</div>' +
|
||||
'<div class="sys-card-body">' +
|
||||
barHtml(disk.percent, '') +
|
||||
'<div class="sys-card-detail">' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '</div>' +
|
||||
'<div class="sys-card-detail">Path: ' + (disk.path || '/') + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
// -----------------------------------------------------------------------
|
||||
// Temperature & Power Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
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];
|
||||
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];
|
||||
}
|
||||
// Fall back to first available
|
||||
for (const key of Object.keys(temps)) {
|
||||
for (var key in temps) {
|
||||
if (temps[key] && temps[key].length) return temps[key][0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderSdrCard(devices) {
|
||||
const el = document.getElementById('sysCardSdr');
|
||||
function renderTempCard(m) {
|
||||
var el = document.getElementById('sysCardTemp');
|
||||
if (!el) return;
|
||||
let html = '<div class="sys-card-header">SDR Devices <button class="sys-rescan-btn" onclick="SystemHealth.refreshSdr()">Rescan</button></div>';
|
||||
|
||||
var temp = _extractPrimaryTemp(m.temperatures);
|
||||
var html = '<div class="sys-card-header">Temperature & Power</div><div class="sys-card-body">';
|
||||
|
||||
if (temp) {
|
||||
// Update sparkline history
|
||||
tempHistory.push(temp.current);
|
||||
if (tempHistory.length > SPARKLINE_SIZE) tempHistory.shift();
|
||||
|
||||
html += '<div class="sys-temp-big">' + Math.round(temp.current) + '°C</div>';
|
||||
html += '<div class="sys-sparkline-wrap">' + sparklineSvg(tempHistory) + '</div>';
|
||||
|
||||
// Additional sensors
|
||||
if (m.temperatures) {
|
||||
for (var chip in m.temperatures) {
|
||||
m.temperatures[chip].forEach(function (s) {
|
||||
html += '<div class="sys-card-detail">' + escHtml(s.label) + ': ' + Math.round(s.current) + '°C</div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html += '<span class="sys-metric-na">No temperature sensors</span>';
|
||||
}
|
||||
|
||||
// Fans
|
||||
if (m.fans) {
|
||||
for (var fChip in m.fans) {
|
||||
m.fans[fChip].forEach(function (f) {
|
||||
html += '<div class="sys-card-detail">Fan ' + escHtml(f.label) + ': ' + f.current + ' RPM</div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Battery
|
||||
if (m.battery) {
|
||||
html += '<div class="sys-card-detail" style="margin-top:8px">' +
|
||||
'Battery: ' + Math.round(m.battery.percent) + '%' +
|
||||
(m.battery.plugged ? ' (plugged)' : '') + '</div>';
|
||||
}
|
||||
|
||||
// Throttle flags (Pi)
|
||||
if (m.power && m.power.throttled) {
|
||||
html += '<div class="sys-card-detail" style="color:var(--accent-yellow,#ffcc00)">Throttle: 0x' + m.power.throttled + '</div>';
|
||||
}
|
||||
|
||||
// Power draw
|
||||
if (m.power && m.power.draw_watts != null) {
|
||||
html += '<div class="sys-card-detail">Power: ' + m.power.draw_watts + ' W</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Disk Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderDiskCard(m) {
|
||||
var el = document.getElementById('sysCardDisk');
|
||||
if (!el) return;
|
||||
var disk = m.disk;
|
||||
if (!disk) { el.innerHTML = '<div class="sys-card-header">Disk & Storage</div><div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
|
||||
var html = '<div class="sys-card-header">Disk & Storage</div><div class="sys-card-body">';
|
||||
html += barHtml(disk.percent, '');
|
||||
html += '<div class="sys-card-detail">' + formatBytes(disk.used) + ' / ' + formatBytes(disk.total) + '</div>';
|
||||
|
||||
// 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 += '<div class="sys-disk-io">' +
|
||||
'<span class="sys-disk-io-read">R: ' + formatRate(Math.max(0, readRate)) + '</span>' +
|
||||
'<span class="sys-disk-io-write">W: ' + formatRate(Math.max(0, writeRate)) + '</span>' +
|
||||
'</div>';
|
||||
html += '<div class="sys-card-detail">IOPS: ' + Math.max(0, readIops) + 'r / ' + Math.max(0, writeIops) + 'w</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if (m.disk_io) {
|
||||
prevDiskIo = m.disk_io;
|
||||
prevDiskTimestamp = m.timestamp;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Network Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderNetworkCard(m) {
|
||||
var el = document.getElementById('sysCardNetwork');
|
||||
if (!el) return;
|
||||
var net = m.network;
|
||||
if (!net) { el.innerHTML = '<div class="sys-card-header">Network</div><div class="sys-card-body"><span class="sys-metric-na">N/A</span></div>'; return; }
|
||||
|
||||
var html = '<div class="sys-card-header">Network</div><div class="sys-card-body">';
|
||||
|
||||
// Interfaces
|
||||
var ifaces = net.interfaces || [];
|
||||
if (ifaces.length === 0) {
|
||||
html += '<span class="sys-metric-na">No interfaces</span>';
|
||||
} else {
|
||||
ifaces.forEach(function (iface) {
|
||||
html += '<div class="sys-net-iface">';
|
||||
html += '<div class="sys-net-iface-name">' + escHtml(iface.name) +
|
||||
(iface.is_up ? '' : ' <span style="color:var(--text-dim)">(down)</span>') + '</div>';
|
||||
if (iface.ipv4) html += '<div class="sys-net-iface-ip">' + escHtml(iface.ipv4) + '</div>';
|
||||
var details = [];
|
||||
if (iface.mac) details.push('MAC: ' + iface.mac);
|
||||
if (iface.speed) details.push(iface.speed + ' Mbps');
|
||||
if (details.length) html += '<div class="sys-net-iface-detail">' + escHtml(details.join(' | ')) + '</div>';
|
||||
|
||||
// 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 += '<div class="sys-bandwidth">' +
|
||||
'<span class="sys-bw-up">↑ ' + formatRate(Math.max(0, upRate)) + '</span>' +
|
||||
'<span class="sys-bw-down">↓ ' + formatRate(Math.max(0, downRate)) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Connection count
|
||||
if (net.connections != null) {
|
||||
html += '<div class="sys-card-detail" style="margin-top:8px">Connections: ' + net.connections + '</div>';
|
||||
}
|
||||
|
||||
// Save for next delta
|
||||
if (net.io) {
|
||||
prevNetIo = net.io;
|
||||
prevNetTimestamp = m.timestamp;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
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 = '<div class="sys-card-header">Location & Weather</div><div class="sys-card-body">';
|
||||
html += '<div class="sys-location-inner">';
|
||||
|
||||
// Globe placeholder (will be replaced with saved node or initialized fresh)
|
||||
if (!savedGlobe) {
|
||||
html += '<div class="sys-globe-wrap" id="sysGlobeContainer"></div>';
|
||||
} else {
|
||||
html += '<div id="sysGlobePlaceholder"></div>';
|
||||
}
|
||||
|
||||
// Details below globe
|
||||
html += '<div class="sys-location-details">';
|
||||
|
||||
if (locationData && locationData.lat != null) {
|
||||
html += '<div class="sys-location-coords">' +
|
||||
locationData.lat.toFixed(4) + '°' + (locationData.lat >= 0 ? 'N' : 'S') + ', ' +
|
||||
locationData.lon.toFixed(4) + '°' + (locationData.lon >= 0 ? 'E' : 'W') + '</div>';
|
||||
|
||||
// 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 += '<div class="sys-gps-status">' +
|
||||
'<span class="sys-gps-dot ' + dotCls + '"></span> ' + fixLabel;
|
||||
if (gps.satellites != null) html += ' · ' + gps.satellites + ' sats';
|
||||
if (gps.accuracy != null) html += ' · ±' + gps.accuracy + 'm';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div class="sys-location-source">Source: ' + escHtml(locationData.source || 'unknown') + '</div>';
|
||||
}
|
||||
} else {
|
||||
html += '<div class="sys-location-coords" style="color:var(--text-dim)">No location</div>';
|
||||
}
|
||||
|
||||
// Weather
|
||||
if (weatherData && !weatherData.error) {
|
||||
html += '<div class="sys-weather">';
|
||||
html += '<div class="sys-weather-temp">' + (weatherData.temp_c || '--') + '°C</div>';
|
||||
html += '<div class="sys-weather-condition">' + escHtml(weatherData.condition || '') + '</div>';
|
||||
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 += '<div class="sys-weather-detail">' + escHtml(d) + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
} else if (weatherData && weatherData.error) {
|
||||
html += '<div class="sys-weather"><div class="sys-weather-condition" style="color:var(--text-dim)">Weather unavailable</div></div>';
|
||||
}
|
||||
|
||||
html += '</div>'; // .sys-location-details
|
||||
html += '</div>'; // .sys-location-inner
|
||||
html += '</div>';
|
||||
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 = '<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--text-dim);font-size:11px;">Globe unavailable</div>';
|
||||
}
|
||||
}
|
||||
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 = '<div class="sys-card-header">SDR Devices <button class="sys-rescan-btn" onclick="SystemHealth.refreshSdr()">Rescan</button></div>';
|
||||
html += '<div class="sys-card-body">';
|
||||
if (!devices || !devices.length) {
|
||||
html += '<span class="sys-metric-na">No devices found</span>';
|
||||
@@ -113,9 +601,9 @@ const SystemHealth = (function () {
|
||||
devices.forEach(function (d) {
|
||||
html += '<div class="sys-sdr-device">' +
|
||||
'<span class="sys-process-dot running"></span> ' +
|
||||
'<strong>' + d.type + ' #' + d.index + '</strong>' +
|
||||
'<div class="sys-card-detail">' + (d.name || 'Unknown') + '</div>' +
|
||||
(d.serial ? '<div class="sys-card-detail">S/N: ' + d.serial + '</div>' : '') +
|
||||
'<strong>' + escHtml(d.type) + ' #' + d.index + '</strong>' +
|
||||
'<div class="sys-card-detail">' + escHtml(d.name || 'Unknown') + '</div>' +
|
||||
(d.serial ? '<div class="sys-card-detail">S/N: ' + escHtml(d.serial) + '</div>' : '') +
|
||||
'</div>';
|
||||
});
|
||||
}
|
||||
@@ -123,93 +611,197 @@ const SystemHealth = (function () {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Process Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderProcessCard(m) {
|
||||
const el = document.getElementById('sysCardProcesses');
|
||||
var el = document.getElementById('sysCardProcesses');
|
||||
if (!el) return;
|
||||
const procs = m.processes || {};
|
||||
const keys = Object.keys(procs).sort();
|
||||
let html = '<div class="sys-card-header">Processes</div><div class="sys-card-body">';
|
||||
var procs = m.processes || {};
|
||||
var keys = Object.keys(procs).sort();
|
||||
var html = '<div class="sys-card-header">Active Processes</div><div class="sys-card-body">';
|
||||
if (!keys.length) {
|
||||
html += '<span class="sys-metric-na">No data</span>';
|
||||
} else {
|
||||
var running = 0, stopped = 0;
|
||||
html += '<div class="sys-process-grid">';
|
||||
keys.forEach(function (k) {
|
||||
const running = procs[k];
|
||||
const dotCls = running ? 'running' : 'stopped';
|
||||
const label = k.charAt(0).toUpperCase() + k.slice(1);
|
||||
var isRunning = procs[k];
|
||||
if (isRunning) running++; else stopped++;
|
||||
var dotCls = isRunning ? 'running' : 'stopped';
|
||||
var label = k.charAt(0).toUpperCase() + k.slice(1);
|
||||
html += '<div class="sys-process-item">' +
|
||||
'<span class="sys-process-dot ' + dotCls + '"></span> ' +
|
||||
'<span class="sys-process-name">' + label + '</span>' +
|
||||
'<span class="sys-process-name">' + escHtml(label) + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '<div class="sys-process-summary">' + running + ' running / ' + stopped + ' idle</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// System Info Card
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderSystemInfoCard(m) {
|
||||
const el = document.getElementById('sysCardInfo');
|
||||
var el = document.getElementById('sysCardInfo');
|
||||
if (!el) return;
|
||||
const sys = m.system || {};
|
||||
const temp = _extractPrimaryTemp(m.temperatures);
|
||||
let html = '<div class="sys-card-header">System Info</div><div class="sys-card-body">';
|
||||
html += '<div class="sys-card-detail">Host: ' + (sys.hostname || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">OS: ' + (sys.platform || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">Python: ' + (sys.python || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">App: v' + (sys.version || '--') + '</div>';
|
||||
html += '<div class="sys-card-detail">Uptime: ' + (sys.uptime_human || '--') + '</div>';
|
||||
if (temp) {
|
||||
html += '<div class="sys-card-detail">Temp: ' + Math.round(temp.current) + '°C';
|
||||
if (temp.high) html += ' / ' + Math.round(temp.high) + '°C max';
|
||||
html += '</div>';
|
||||
var sys = m.system || {};
|
||||
var html = '<div class="sys-card-header">System Info</div><div class="sys-card-body"><div class="sys-info-grid">';
|
||||
|
||||
html += '<div class="sys-info-item"><strong>Host</strong><span>' + escHtml(sys.hostname || '--') + '</span></div>';
|
||||
html += '<div class="sys-info-item"><strong>OS</strong><span>' + escHtml((sys.platform || '--').replace(/-with-glibc[\d.]+/, '')) + '</span></div>';
|
||||
html += '<div class="sys-info-item"><strong>Python</strong><span>' + escHtml(sys.python || '--') + '</span></div>';
|
||||
html += '<div class="sys-info-item"><strong>App</strong><span>v' + escHtml(sys.version || '--') + '</span></div>';
|
||||
html += '<div class="sys-info-item"><strong>Uptime</strong><span>' + escHtml(sys.uptime_human || '--') + '</span></div>';
|
||||
|
||||
if (m.boot_time) {
|
||||
var bootDate = new Date(m.boot_time * 1000);
|
||||
html += '<div class="sys-info-item"><strong>Boot</strong><span>' + escHtml(bootDate.toLocaleString()) + '</span></div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (m.network && m.network.connections != null) {
|
||||
html += '<div class="sys-info-item"><strong>Connections</strong><span>' + m.network.connections + '</span></div>';
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Sidebar Updates
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function updateSidebarQuickStats(m) {
|
||||
const cpuEl = document.getElementById('sysQuickCpu');
|
||||
const tempEl = document.getElementById('sysQuickTemp');
|
||||
const ramEl = document.getElementById('sysQuickRam');
|
||||
const diskEl = document.getElementById('sysQuickDisk');
|
||||
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) + '%' : '--';
|
||||
|
||||
const temp = _extractPrimaryTemp(m.temperatures);
|
||||
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;
|
||||
const val = parseInt(el.textContent);
|
||||
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) {
|
||||
const el = document.getElementById('sysProcessList');
|
||||
var el = document.getElementById('sysProcessList');
|
||||
if (!el) return;
|
||||
const procs = m.processes || {};
|
||||
const keys = Object.keys(procs).sort();
|
||||
var procs = m.processes || {};
|
||||
var 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]; });
|
||||
var running = keys.filter(function (k) { return procs[k]; });
|
||||
var stopped = keys.filter(function (k) { return !procs[k]; });
|
||||
el.innerHTML =
|
||||
(running.length ? '<span style="color: var(--accent-green, #00ff88);">' + running.length + ' running</span>' : '') +
|
||||
(running.length && stopped.length ? ' · ' : '') +
|
||||
(stopped.length ? '<span style="color: var(--text-dim);">' + stopped.length + ' stopped</span>' : '');
|
||||
}
|
||||
|
||||
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 () {});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -267,7 +859,7 @@ const SystemHealth = (function () {
|
||||
var html = '';
|
||||
devices.forEach(function (d) {
|
||||
html += '<div style="margin-bottom: 4px;"><span class="sys-process-dot running"></span> ' +
|
||||
d.type + ' #' + d.index + ' — ' + (d.name || 'Unknown') + '</div>';
|
||||
escHtml(d.type) + ' #' + d.index + ' — ' + escHtml(d.name || 'Unknown') + '</div>';
|
||||
});
|
||||
sidebarEl.innerHTML = html;
|
||||
}
|
||||
@@ -284,12 +876,24 @@ const SystemHealth = (function () {
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1005,6 +1005,15 @@ function escapeHtmlWebsdr(str) {
|
||||
|
||||
// ============== EXPORTS ==============
|
||||
|
||||
/**
|
||||
* Destroy — disconnect audio and clear S-meter timer for clean mode switching.
|
||||
*/
|
||||
function destroyWebSDR() {
|
||||
disconnectFromReceiver();
|
||||
}
|
||||
|
||||
const WebSDR = { destroy: destroyWebSDR };
|
||||
|
||||
window.initWebSDR = initWebSDR;
|
||||
window.searchReceivers = searchReceivers;
|
||||
window.selectReceiver = selectReceiver;
|
||||
@@ -1015,3 +1024,4 @@ window.disconnectFromReceiver = disconnectFromReceiver;
|
||||
window.tuneKiwi = tuneKiwi;
|
||||
window.tuneFromBar = tuneFromBar;
|
||||
window.setKiwiVolume = setKiwiVolume;
|
||||
window.WebSDR = WebSDR;
|
||||
|
||||
@@ -28,9 +28,9 @@ const WiFiMode = (function() {
|
||||
maxProbes: 1000,
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
// ==========================================================================
|
||||
// Agent Support
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get the API base URL, routing through agent proxy if agent is selected.
|
||||
@@ -59,49 +59,49 @@ const WiFiMode = (function() {
|
||||
/**
|
||||
* Check for agent mode conflicts before starting WiFi scan.
|
||||
*/
|
||||
function checkAgentConflicts() {
|
||||
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
|
||||
return true;
|
||||
}
|
||||
if (typeof checkAgentModeConflict === 'function') {
|
||||
return checkAgentModeConflict('wifi');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getChannelPresetList(preset) {
|
||||
switch (preset) {
|
||||
case '2.4-common':
|
||||
return '1,6,11';
|
||||
case '2.4-all':
|
||||
return '1,2,3,4,5,6,7,8,9,10,11,12,13';
|
||||
case '5-low':
|
||||
return '36,40,44,48';
|
||||
case '5-mid':
|
||||
return '52,56,60,64';
|
||||
case '5-high':
|
||||
return '149,153,157,161,165';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildChannelConfig() {
|
||||
const preset = document.getElementById('wifiChannelPreset')?.value || '';
|
||||
const listInput = document.getElementById('wifiChannelList')?.value || '';
|
||||
const singleInput = document.getElementById('wifiChannel')?.value || '';
|
||||
|
||||
const listValue = listInput.trim();
|
||||
const presetValue = getChannelPresetList(preset);
|
||||
|
||||
const channels = listValue || presetValue || '';
|
||||
const channel = channels ? null : (singleInput.trim() ? parseInt(singleInput.trim()) : null);
|
||||
|
||||
return {
|
||||
channels: channels || null,
|
||||
channel: Number.isFinite(channel) ? channel : null,
|
||||
};
|
||||
}
|
||||
function checkAgentConflicts() {
|
||||
if (typeof currentAgent === 'undefined' || currentAgent === 'local') {
|
||||
return true;
|
||||
}
|
||||
if (typeof checkAgentModeConflict === 'function') {
|
||||
return checkAgentModeConflict('wifi');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getChannelPresetList(preset) {
|
||||
switch (preset) {
|
||||
case '2.4-common':
|
||||
return '1,6,11';
|
||||
case '2.4-all':
|
||||
return '1,2,3,4,5,6,7,8,9,10,11,12,13';
|
||||
case '5-low':
|
||||
return '36,40,44,48';
|
||||
case '5-mid':
|
||||
return '52,56,60,64';
|
||||
case '5-high':
|
||||
return '149,153,157,161,165';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildChannelConfig() {
|
||||
const preset = document.getElementById('wifiChannelPreset')?.value || '';
|
||||
const listInput = document.getElementById('wifiChannelList')?.value || '';
|
||||
const singleInput = document.getElementById('wifiChannel')?.value || '';
|
||||
|
||||
const listValue = listInput.trim();
|
||||
const presetValue = getChannelPresetList(preset);
|
||||
|
||||
const channels = listValue || presetValue || '';
|
||||
const channel = channels ? null : (singleInput.trim() ? parseInt(singleInput.trim()) : null);
|
||||
|
||||
return {
|
||||
channels: channels || null,
|
||||
channel: Number.isFinite(channel) ? channel : null,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// State
|
||||
@@ -120,23 +120,23 @@ const WiFiMode = (function() {
|
||||
let channelStats = [];
|
||||
let recommendations = [];
|
||||
|
||||
// UI state
|
||||
let selectedNetwork = null;
|
||||
let currentFilter = 'all';
|
||||
let currentSort = { field: 'rssi', order: 'desc' };
|
||||
let renderFramePending = false;
|
||||
const pendingRender = {
|
||||
table: false,
|
||||
stats: false,
|
||||
radar: false,
|
||||
chart: false,
|
||||
detail: false,
|
||||
};
|
||||
const listenersBound = {
|
||||
scanTabs: false,
|
||||
filters: false,
|
||||
sort: false,
|
||||
};
|
||||
// UI state
|
||||
let selectedNetwork = null;
|
||||
let currentFilter = 'all';
|
||||
let currentSort = { field: 'rssi', order: 'desc' };
|
||||
let renderFramePending = false;
|
||||
const pendingRender = {
|
||||
table: false,
|
||||
stats: false,
|
||||
radar: false,
|
||||
chart: false,
|
||||
detail: false,
|
||||
};
|
||||
const listenersBound = {
|
||||
scanTabs: false,
|
||||
filters: false,
|
||||
sort: false,
|
||||
};
|
||||
|
||||
// Agent state
|
||||
let showAllAgentsMode = false; // Show combined results from all agents
|
||||
@@ -165,11 +165,11 @@ const WiFiMode = (function() {
|
||||
|
||||
// Initialize components
|
||||
initScanModeTabs();
|
||||
initNetworkFilters();
|
||||
initSortControls();
|
||||
initProximityRadar();
|
||||
initChannelChart();
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
initNetworkFilters();
|
||||
initSortControls();
|
||||
initProximityRadar();
|
||||
initChannelChart();
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
|
||||
// Check if already scanning
|
||||
checkScanStatus();
|
||||
@@ -378,16 +378,16 @@ const WiFiMode = (function() {
|
||||
// Scan Mode Tabs
|
||||
// ==========================================================================
|
||||
|
||||
function initScanModeTabs() {
|
||||
if (listenersBound.scanTabs) return;
|
||||
if (elements.scanModeQuick) {
|
||||
elements.scanModeQuick.addEventListener('click', () => setScanMode('quick'));
|
||||
}
|
||||
if (elements.scanModeDeep) {
|
||||
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
||||
}
|
||||
listenersBound.scanTabs = true;
|
||||
}
|
||||
function initScanModeTabs() {
|
||||
if (listenersBound.scanTabs) return;
|
||||
if (elements.scanModeQuick) {
|
||||
elements.scanModeQuick.addEventListener('click', () => setScanMode('quick'));
|
||||
}
|
||||
if (elements.scanModeDeep) {
|
||||
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
|
||||
}
|
||||
listenersBound.scanTabs = true;
|
||||
}
|
||||
|
||||
function setScanMode(mode) {
|
||||
scanMode = mode;
|
||||
@@ -511,10 +511,10 @@ const WiFiMode = (function() {
|
||||
setScanning(true, 'deep');
|
||||
|
||||
try {
|
||||
const iface = elements.interfaceSelect?.value || null;
|
||||
const band = document.getElementById('wifiBand')?.value || 'all';
|
||||
const channelConfig = buildChannelConfig();
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const iface = elements.interfaceSelect?.value || null;
|
||||
const band = document.getElementById('wifiBand')?.value || 'all';
|
||||
const channelConfig = buildChannelConfig();
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
|
||||
let response;
|
||||
if (isAgentMode) {
|
||||
@@ -523,25 +523,25 @@ const WiFiMode = (function() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
scan_type: 'deep',
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${CONFIG.apiBase}/scan/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
}
|
||||
interface: iface,
|
||||
scan_type: 'deep',
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${CONFIG.apiBase}/scan/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
interface: iface,
|
||||
band: band === 'abg' ? 'all' : band === 'bg' ? '2.4' : '5',
|
||||
channel: channelConfig.channel,
|
||||
channels: channelConfig.channels,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
@@ -572,8 +572,8 @@ const WiFiMode = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopScan() {
|
||||
console.log('[WiFiMode] Stopping scan...');
|
||||
async function stopScan() {
|
||||
console.log('[WiFiMode] Stopping scan...');
|
||||
|
||||
// Stop polling
|
||||
if (pollTimer) {
|
||||
@@ -585,41 +585,41 @@ const WiFiMode = (function() {
|
||||
stopAgentDeepScanPolling();
|
||||
|
||||
// Close event stream
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// Update UI immediately so mode transitions are responsive even if the
|
||||
// backend needs extra time to terminate subprocesses.
|
||||
setScanning(false);
|
||||
|
||||
// Stop scan on server (local or agent)
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const timeoutMs = isAgentMode ? 8000 : 2200;
|
||||
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
||||
|
||||
try {
|
||||
if (isAgentMode) {
|
||||
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
} else if (scanMode === 'deep') {
|
||||
await fetch(`${CONFIG.apiBase}/scan/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[WiFiMode] Error stopping scan:', error);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
|
||||
// Update UI immediately so mode transitions are responsive even if the
|
||||
// backend needs extra time to terminate subprocesses.
|
||||
setScanning(false);
|
||||
|
||||
// Stop scan on server (local or agent)
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
const timeoutMs = isAgentMode ? 8000 : 2200;
|
||||
const controller = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||||
const timeoutId = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
||||
|
||||
try {
|
||||
if (isAgentMode) {
|
||||
await fetch(`/controller/agents/${currentAgent}/wifi/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
} else if (scanMode === 'deep') {
|
||||
await fetch(`${CONFIG.apiBase}/scan/stop`, {
|
||||
method: 'POST',
|
||||
...(controller ? { signal: controller.signal } : {}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[WiFiMode] Error stopping scan:', error);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setScanning(scanning, mode = null) {
|
||||
isScanning = scanning;
|
||||
@@ -713,10 +713,10 @@ const WiFiMode = (function() {
|
||||
}, CONFIG.pollInterval);
|
||||
}
|
||||
|
||||
function processQuickScanResult(result) {
|
||||
// Update networks
|
||||
result.access_points.forEach(ap => {
|
||||
networks.set(ap.bssid, ap);
|
||||
function processQuickScanResult(result) {
|
||||
// Update networks
|
||||
result.access_points.forEach(ap => {
|
||||
networks.set(ap.bssid, ap);
|
||||
});
|
||||
|
||||
// Update channel stats (calculate from networks if not provided by API)
|
||||
@@ -724,12 +724,12 @@ const WiFiMode = (function() {
|
||||
recommendations = result.recommendations || [];
|
||||
|
||||
// If no channel stats from API, calculate from networks
|
||||
if (channelStats.length === 0 && networks.size > 0) {
|
||||
channelStats = calculateChannelStats();
|
||||
}
|
||||
|
||||
// Update UI
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
if (channelStats.length === 0 && networks.size > 0) {
|
||||
channelStats = calculateChannelStats();
|
||||
}
|
||||
|
||||
// Update UI
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
|
||||
// Callbacks
|
||||
result.access_points.forEach(ap => {
|
||||
@@ -938,25 +938,25 @@ const WiFiMode = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleNetworkUpdate(network) {
|
||||
networks.set(network.bssid, network);
|
||||
scheduleRender({
|
||||
table: true,
|
||||
stats: true,
|
||||
radar: true,
|
||||
chart: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
|
||||
if (onNetworkUpdate) onNetworkUpdate(network);
|
||||
}
|
||||
|
||||
function handleClientUpdate(client) {
|
||||
clients.set(client.mac, client);
|
||||
scheduleRender({ stats: true });
|
||||
|
||||
// Update client display if this client belongs to the selected network
|
||||
updateClientInList(client);
|
||||
function handleNetworkUpdate(network) {
|
||||
networks.set(network.bssid, network);
|
||||
scheduleRender({
|
||||
table: true,
|
||||
stats: true,
|
||||
radar: true,
|
||||
chart: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
|
||||
if (onNetworkUpdate) onNetworkUpdate(network);
|
||||
}
|
||||
|
||||
function handleClientUpdate(client) {
|
||||
clients.set(client.mac, client);
|
||||
scheduleRender({ stats: true });
|
||||
|
||||
// Update client display if this client belongs to the selected network
|
||||
updateClientInList(client);
|
||||
|
||||
if (onClientUpdate) onClientUpdate(client);
|
||||
}
|
||||
@@ -970,37 +970,37 @@ const WiFiMode = (function() {
|
||||
if (onProbeRequest) onProbeRequest(probe);
|
||||
}
|
||||
|
||||
function handleHiddenRevealed(bssid, revealedSsid) {
|
||||
const network = networks.get(bssid);
|
||||
if (network) {
|
||||
network.revealed_essid = revealedSsid;
|
||||
network.display_name = `${revealedSsid} (revealed)`;
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === bssid,
|
||||
});
|
||||
|
||||
// Show notification
|
||||
showInfo(`Hidden SSID revealed: ${revealedSsid}`);
|
||||
}
|
||||
}
|
||||
function handleHiddenRevealed(bssid, revealedSsid) {
|
||||
const network = networks.get(bssid);
|
||||
if (network) {
|
||||
network.revealed_essid = revealedSsid;
|
||||
network.display_name = `${revealedSsid} (revealed)`;
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === bssid,
|
||||
});
|
||||
|
||||
// Show notification
|
||||
showInfo(`Hidden SSID revealed: ${revealedSsid}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Network Table
|
||||
// ==========================================================================
|
||||
|
||||
function initNetworkFilters() {
|
||||
if (listenersBound.filters) return;
|
||||
if (!elements.networkFilters) return;
|
||||
|
||||
elements.networkFilters.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.wifi-filter-btn')) {
|
||||
const filter = e.target.dataset.filter;
|
||||
setNetworkFilter(filter);
|
||||
}
|
||||
});
|
||||
listenersBound.filters = true;
|
||||
}
|
||||
function initNetworkFilters() {
|
||||
if (listenersBound.filters) return;
|
||||
if (!elements.networkFilters) return;
|
||||
|
||||
elements.networkFilters.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.wifi-filter-btn')) {
|
||||
const filter = e.target.dataset.filter;
|
||||
setNetworkFilter(filter);
|
||||
}
|
||||
});
|
||||
listenersBound.filters = true;
|
||||
}
|
||||
|
||||
function setNetworkFilter(filter) {
|
||||
currentFilter = filter;
|
||||
@@ -1015,11 +1015,11 @@ const WiFiMode = (function() {
|
||||
updateNetworkTable();
|
||||
}
|
||||
|
||||
function initSortControls() {
|
||||
if (listenersBound.sort) return;
|
||||
if (!elements.networkTable) return;
|
||||
|
||||
elements.networkTable.addEventListener('click', (e) => {
|
||||
function initSortControls() {
|
||||
if (listenersBound.sort) return;
|
||||
if (!elements.networkTable) return;
|
||||
|
||||
elements.networkTable.addEventListener('click', (e) => {
|
||||
const th = e.target.closest('th[data-sort]');
|
||||
if (th) {
|
||||
const field = th.dataset.sort;
|
||||
@@ -1029,54 +1029,54 @@ const WiFiMode = (function() {
|
||||
currentSort.field = field;
|
||||
currentSort.order = 'desc';
|
||||
}
|
||||
updateNetworkTable();
|
||||
}
|
||||
});
|
||||
|
||||
if (elements.networkTableBody) {
|
||||
elements.networkTableBody.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-bssid]');
|
||||
if (!row) return;
|
||||
selectNetwork(row.dataset.bssid);
|
||||
});
|
||||
}
|
||||
listenersBound.sort = true;
|
||||
}
|
||||
|
||||
function scheduleRender(flags = {}) {
|
||||
pendingRender.table = pendingRender.table || Boolean(flags.table);
|
||||
pendingRender.stats = pendingRender.stats || Boolean(flags.stats);
|
||||
pendingRender.radar = pendingRender.radar || Boolean(flags.radar);
|
||||
pendingRender.chart = pendingRender.chart || Boolean(flags.chart);
|
||||
pendingRender.detail = pendingRender.detail || Boolean(flags.detail);
|
||||
|
||||
if (renderFramePending) return;
|
||||
renderFramePending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
renderFramePending = false;
|
||||
|
||||
if (pendingRender.table) updateNetworkTable();
|
||||
if (pendingRender.stats) updateStats();
|
||||
if (pendingRender.radar) updateProximityRadar();
|
||||
if (pendingRender.chart) updateChannelChart();
|
||||
if (pendingRender.detail && selectedNetwork) {
|
||||
updateDetailPanel(selectedNetwork, { refreshClients: false });
|
||||
}
|
||||
|
||||
pendingRender.table = false;
|
||||
pendingRender.stats = false;
|
||||
pendingRender.radar = false;
|
||||
pendingRender.chart = false;
|
||||
pendingRender.detail = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateNetworkTable() {
|
||||
if (!elements.networkTableBody) return;
|
||||
|
||||
// Filter networks
|
||||
let filtered = Array.from(networks.values());
|
||||
updateNetworkTable();
|
||||
}
|
||||
});
|
||||
|
||||
if (elements.networkTableBody) {
|
||||
elements.networkTableBody.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('tr[data-bssid]');
|
||||
if (!row) return;
|
||||
selectNetwork(row.dataset.bssid);
|
||||
});
|
||||
}
|
||||
listenersBound.sort = true;
|
||||
}
|
||||
|
||||
function scheduleRender(flags = {}) {
|
||||
pendingRender.table = pendingRender.table || Boolean(flags.table);
|
||||
pendingRender.stats = pendingRender.stats || Boolean(flags.stats);
|
||||
pendingRender.radar = pendingRender.radar || Boolean(flags.radar);
|
||||
pendingRender.chart = pendingRender.chart || Boolean(flags.chart);
|
||||
pendingRender.detail = pendingRender.detail || Boolean(flags.detail);
|
||||
|
||||
if (renderFramePending) return;
|
||||
renderFramePending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
renderFramePending = false;
|
||||
|
||||
if (pendingRender.table) updateNetworkTable();
|
||||
if (pendingRender.stats) updateStats();
|
||||
if (pendingRender.radar) updateProximityRadar();
|
||||
if (pendingRender.chart) updateChannelChart();
|
||||
if (pendingRender.detail && selectedNetwork) {
|
||||
updateDetailPanel(selectedNetwork, { refreshClients: false });
|
||||
}
|
||||
|
||||
pendingRender.table = false;
|
||||
pendingRender.stats = false;
|
||||
pendingRender.radar = false;
|
||||
pendingRender.chart = false;
|
||||
pendingRender.detail = false;
|
||||
});
|
||||
}
|
||||
|
||||
function updateNetworkTable() {
|
||||
if (!elements.networkTableBody) return;
|
||||
|
||||
// Filter networks
|
||||
let filtered = Array.from(networks.values());
|
||||
|
||||
switch (currentFilter) {
|
||||
case 'hidden':
|
||||
@@ -1126,44 +1126,44 @@ const WiFiMode = (function() {
|
||||
return bVal > aVal ? 1 : bVal < aVal ? -1 : 0;
|
||||
} else {
|
||||
return aVal > bVal ? 1 : aVal < bVal ? -1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
let message = 'Start scanning to discover networks';
|
||||
let type = 'empty';
|
||||
if (isScanning) {
|
||||
message = 'Scanning for networks...';
|
||||
type = 'loading';
|
||||
} else if (networks.size > 0) {
|
||||
message = 'No networks match current filters';
|
||||
}
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(elements.networkTableBody, {
|
||||
type,
|
||||
message,
|
||||
columns: 7,
|
||||
});
|
||||
} else {
|
||||
elements.networkTableBody.innerHTML = `<tr class="wifi-network-placeholder"><td colspan="7"><div class="placeholder-text">${escapeHtml(message)}</div></td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Render table
|
||||
elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function createNetworkRow(network) {
|
||||
const rssi = network.rssi_current;
|
||||
const security = network.security || 'Unknown';
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
|
||||
const securityClass = security === 'Open' ? 'security-open' :
|
||||
security === 'WEP' ? 'security-wep' :
|
||||
security.includes('WPA3') ? 'security-wpa3' : 'security-wpa';
|
||||
if (filtered.length === 0) {
|
||||
let message = 'Start scanning to discover networks';
|
||||
let type = 'empty';
|
||||
if (isScanning) {
|
||||
message = 'Scanning for networks...';
|
||||
type = 'loading';
|
||||
} else if (networks.size > 0) {
|
||||
message = 'No networks match current filters';
|
||||
}
|
||||
if (typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(elements.networkTableBody, {
|
||||
type,
|
||||
message,
|
||||
columns: 7,
|
||||
});
|
||||
} else {
|
||||
elements.networkTableBody.innerHTML = `<tr class="wifi-network-placeholder"><td colspan="7"><div class="placeholder-text">${escapeHtml(message)}</div></td></tr>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Render table
|
||||
elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join('');
|
||||
}
|
||||
|
||||
function createNetworkRow(network) {
|
||||
const rssi = network.rssi_current;
|
||||
const security = network.security || 'Unknown';
|
||||
const signalClass = rssi >= -50 ? 'signal-strong' :
|
||||
rssi >= -70 ? 'signal-medium' :
|
||||
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
|
||||
|
||||
const securityClass = security === 'Open' ? 'security-open' :
|
||||
security === 'WEP' ? 'security-wep' :
|
||||
security.includes('WPA3') ? 'security-wpa3' : 'security-wpa';
|
||||
|
||||
const hiddenBadge = network.is_hidden ? '<span class="badge badge-hidden">Hidden</span>' : '';
|
||||
const newBadge = network.is_new ? '<span class="badge badge-new">New</span>' : '';
|
||||
@@ -1172,25 +1172,25 @@ const WiFiMode = (function() {
|
||||
const agentName = network._agent || 'Local';
|
||||
const agentClass = agentName === 'Local' ? 'agent-local' : 'agent-remote';
|
||||
|
||||
return `
|
||||
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
|
||||
data-bssid="${escapeHtml(network.bssid)}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-keyboard-activate="true"
|
||||
aria-label="Select network ${escapeHtml(network.display_name || network.essid || '[Hidden]')}">
|
||||
<td class="col-essid">
|
||||
<span class="essid">${escapeHtml(network.display_name || network.essid || '[Hidden]')}</span>
|
||||
${hiddenBadge}${newBadge}
|
||||
</td>
|
||||
return `
|
||||
<tr class="wifi-network-row ${network.bssid === selectedNetwork ? 'selected' : ''}"
|
||||
data-bssid="${escapeHtml(network.bssid)}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-keyboard-activate="true"
|
||||
aria-label="Select network ${escapeHtml(network.display_name || network.essid || '[Hidden]')}">
|
||||
<td class="col-essid">
|
||||
<span class="essid">${escapeHtml(network.display_name || network.essid || '[Hidden]')}</span>
|
||||
${hiddenBadge}${newBadge}
|
||||
</td>
|
||||
<td class="col-bssid"><code>${escapeHtml(network.bssid)}</code></td>
|
||||
<td class="col-channel">${network.channel || '-'}</td>
|
||||
<td class="col-rssi">
|
||||
<span class="rssi-value ${signalClass}">${rssi != null ? rssi : '-'}</span>
|
||||
</td>
|
||||
<td class="col-security">
|
||||
<span class="security-badge ${securityClass}">${escapeHtml(security)}</span>
|
||||
</td>
|
||||
<td class="col-rssi">
|
||||
<span class="rssi-value ${signalClass}">${rssi != null ? rssi : '-'}</span>
|
||||
</td>
|
||||
<td class="col-security">
|
||||
<span class="security-badge ${securityClass}">${escapeHtml(security)}</span>
|
||||
</td>
|
||||
<td class="col-clients">${network.client_count || 0}</td>
|
||||
<td class="col-agent">
|
||||
<span class="agent-badge ${agentClass}">${escapeHtml(agentName)}</span>
|
||||
@@ -1199,12 +1199,12 @@ const WiFiMode = (function() {
|
||||
`;
|
||||
}
|
||||
|
||||
function updateNetworkRow(network) {
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
}
|
||||
function updateNetworkRow(network) {
|
||||
scheduleRender({
|
||||
table: true,
|
||||
detail: selectedNetwork === network.bssid,
|
||||
});
|
||||
}
|
||||
|
||||
function selectNetwork(bssid) {
|
||||
selectedNetwork = bssid;
|
||||
@@ -1227,9 +1227,9 @@ const WiFiMode = (function() {
|
||||
// Detail Panel
|
||||
// ==========================================================================
|
||||
|
||||
function updateDetailPanel(bssid, options = {}) {
|
||||
const { refreshClients = true } = options;
|
||||
if (!elements.detailDrawer) return;
|
||||
function updateDetailPanel(bssid, options = {}) {
|
||||
const { refreshClients = true } = options;
|
||||
if (!elements.detailDrawer) return;
|
||||
|
||||
const network = networks.get(bssid);
|
||||
if (!network) {
|
||||
@@ -1274,11 +1274,11 @@ const WiFiMode = (function() {
|
||||
// Show the drawer
|
||||
elements.detailDrawer.classList.add('open');
|
||||
|
||||
// Fetch and display clients for this network
|
||||
if (refreshClients) {
|
||||
fetchClientsForNetwork(network.bssid);
|
||||
}
|
||||
}
|
||||
// Fetch and display clients for this network
|
||||
if (refreshClients) {
|
||||
fetchClientsForNetwork(network.bssid);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedNetwork = null;
|
||||
@@ -1294,18 +1294,18 @@ const WiFiMode = (function() {
|
||||
// Client Display
|
||||
// ==========================================================================
|
||||
|
||||
async function fetchClientsForNetwork(bssid) {
|
||||
if (!elements.detailClientList) return;
|
||||
const listContainer = elements.detailClientList.querySelector('.wifi-client-list');
|
||||
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'loading', message: 'Loading clients...' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
}
|
||||
|
||||
try {
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
let response;
|
||||
async function fetchClientsForNetwork(bssid) {
|
||||
if (!elements.detailClientList) return;
|
||||
const listContainer = elements.detailClientList.querySelector('.wifi-client-list');
|
||||
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'loading', message: 'Loading clients...' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
}
|
||||
|
||||
try {
|
||||
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
|
||||
let response;
|
||||
|
||||
if (isAgentMode) {
|
||||
// Route through agent proxy
|
||||
@@ -1314,44 +1314,44 @@ const WiFiMode = (function() {
|
||||
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Handle agent response format (may be nested in 'result')
|
||||
const result = isAgentMode && data.result ? data.result : data;
|
||||
const clientList = result.clients || [];
|
||||
|
||||
if (clientList.length > 0) {
|
||||
renderClientList(clientList, bssid);
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||
if (countBadge) countBadge.textContent = '0';
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'No associated clients' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientList.length > 0) {
|
||||
renderClientList(clientList, bssid);
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
const countBadge = document.getElementById('wifiClientCountBadge');
|
||||
if (countBadge) countBadge.textContent = '0';
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'No associated clients' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('[WiFiMode] Error fetching clients:', error);
|
||||
if (listContainer && typeof renderCollectionState === 'function') {
|
||||
renderCollectionState(listContainer, { type: 'empty', message: 'Client list unavailable' });
|
||||
elements.detailClientList.style.display = 'block';
|
||||
} else {
|
||||
elements.detailClientList.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderClientList(clientList, bssid) {
|
||||
const container = elements.detailClientList?.querySelector('.wifi-client-list');
|
||||
@@ -1708,16 +1708,16 @@ const WiFiMode = (function() {
|
||||
/**
|
||||
* Clear all collected data.
|
||||
*/
|
||||
function clearData() {
|
||||
networks.clear();
|
||||
clients.clear();
|
||||
probeRequests = [];
|
||||
channelStats = [];
|
||||
recommendations = [];
|
||||
if (selectedNetwork) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
function clearData() {
|
||||
networks.clear();
|
||||
clients.clear();
|
||||
probeRequests = [];
|
||||
channelStats = [];
|
||||
recommendations = [];
|
||||
if (selectedNetwork) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1763,12 +1763,12 @@ const WiFiMode = (function() {
|
||||
clientsToRemove.push(mac);
|
||||
}
|
||||
});
|
||||
clientsToRemove.forEach(mac => clients.delete(mac));
|
||||
if (selectedNetwork && !networks.has(selectedNetwork)) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
clientsToRemove.forEach(mac => clients.delete(mac));
|
||||
if (selectedNetwork && !networks.has(selectedNetwork)) {
|
||||
closeDetail();
|
||||
}
|
||||
scheduleRender({ table: true, stats: true, radar: true, chart: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh WiFi interfaces from current agent.
|
||||
@@ -1811,7 +1811,28 @@ const WiFiMode = (function() {
|
||||
onNetworkUpdate: (cb) => { onNetworkUpdate = cb; },
|
||||
onClientUpdate: (cb) => { onClientUpdate = cb; },
|
||||
onProbeRequest: (cb) => { onProbeRequest = cb; },
|
||||
|
||||
// Lifecycle
|
||||
destroy,
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy — close SSE stream and clear polling timers for clean mode switching.
|
||||
*/
|
||||
function destroy() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (agentPollTimer) {
|
||||
clearInterval(agentPollTimer);
|
||||
agentPollTimer = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
|
||||
Reference in New Issue
Block a user