Improve cross-app UX: accessibility, mode consistency, and render performance

This commit is contained in:
Smittix
2026-02-19 22:32:08 +00:00
parent 40cc7a5058
commit dc01b20b98
11 changed files with 1051 additions and 507 deletions
+188 -2
View File
@@ -3586,6 +3586,7 @@ header h1 .tagline {
.wifi-networks-table-wrapper {
flex: 1;
overflow-y: auto;
overscroll-behavior: contain;
}
.wifi-networks-table {
@@ -3694,6 +3695,22 @@ header h1 .tagline {
color: var(--text-dim);
}
.app-collection-state-row td {
text-align: center;
padding: 0;
}
.app-collection-state {
color: var(--text-dim);
padding: 16px 12px;
font-size: 11px;
text-align: center;
}
.app-collection-state.is-loading {
color: var(--accent-cyan);
}
/* WiFi Radar Panel (CENTER) */
.wifi-radar-panel {
display: flex;
@@ -4082,14 +4099,14 @@ header h1 .tagline {
display: flex;
gap: 12px;
flex: 1;
min-height: 380px;
min-height: 420px;
}
.bt-side-panels {
display: flex;
flex-direction: column;
gap: 12px;
width: 240px;
width: 300px;
flex-shrink: 0;
}
@@ -4097,6 +4114,21 @@ header h1 .tagline {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.bt-tracker-panel h5 {
margin-bottom: 8px;
}
.bt-tracker-list {
font-size: 11px;
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 2px;
overscroll-behavior: contain;
}
.bt-radar-panel {
@@ -4603,6 +4635,7 @@ header h1 .tagline {
min-height: 0;
padding: 8px 10px 12px;
background: var(--bg-primary);
overscroll-behavior: contain;
}
.bt-device-list .wifi-device-list-header {
@@ -4661,6 +4694,44 @@ header h1 .tagline {
text-overflow: ellipsis;
}
.bt-list-signal-strip {
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
background: var(--bg-primary);
}
.bt-list-signal-title {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.45px;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 6px;
}
.bt-signal-dist-compact {
gap: 6px;
padding: 0;
}
.bt-signal-dist-compact .signal-range {
gap: 8px;
}
.bt-signal-dist-compact .signal-range span:first-child {
width: 50px;
font-size: 9px;
}
.bt-signal-dist-compact .signal-range span:last-child {
width: 22px;
font-size: 10px;
}
.bt-signal-dist-compact .signal-bar-bg {
height: 10px;
}
.bt-device-toolbar {
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
@@ -4719,13 +4790,111 @@ header h1 .tagline {
}
.bt-tracker-item {
padding: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background 0.15s ease;
cursor: pointer;
}
.bt-tracker-item:hover {
background: rgba(239, 68, 68, 0.08);
}
.bt-tracker-item:focus-visible {
outline: 1px solid var(--accent-cyan);
outline-offset: -1px;
}
.bt-tracker-row-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.bt-tracker-left,
.bt-tracker-right {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.bt-tracker-confidence {
font-size: 9px;
padding: 2px 5px;
border-radius: 3px;
font-weight: 700;
letter-spacing: 0.2px;
}
.bt-tracker-confidence-high .bt-tracker-confidence {
color: #ef4444;
background: rgba(239, 68, 68, 0.2);
}
.bt-tracker-confidence-medium .bt-tracker-confidence {
color: #f97316;
background: rgba(249, 115, 22, 0.2);
}
.bt-tracker-confidence-low .bt-tracker-confidence {
color: #eab308;
background: rgba(234, 179, 8, 0.2);
}
.bt-tracker-type {
font-size: 11px;
color: var(--text-primary);
font-weight: 500;
}
.bt-tracker-risk {
font-size: 9px;
font-weight: 700;
}
.bt-risk-high {
color: #ef4444;
}
.bt-risk-medium {
color: #f97316;
}
.bt-risk-low {
color: var(--text-dim);
}
.bt-tracker-rssi,
.bt-tracker-seen {
font-size: 10px;
color: var(--text-dim);
}
.bt-tracker-row-bottom {
display: flex;
justify-content: space-between;
margin-top: 3px;
gap: 10px;
}
.bt-tracker-address {
font-size: 9px;
color: var(--text-dim);
font-family: var(--font-mono);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bt-tracker-evidence {
margin-top: 3px;
font-size: 9px;
color: var(--text-dim);
font-style: italic;
}
/* Bluetooth Signal Distribution */
.bt-signal-dist {
display: flex;
@@ -4804,6 +4973,11 @@ header h1 .tagline {
border-color: var(--accent-cyan);
}
.bt-device-row:focus-visible {
outline: 1px solid var(--accent-cyan);
outline-offset: 1px;
}
.bt-row-main {
display: flex;
justify-content: space-between;
@@ -4941,6 +5115,10 @@ header h1 .tagline {
padding: 4px 4px 0 42px;
}
.bt-device-filter-state {
margin-top: 8px;
}
/* Bluetooth Device Modal */
.bt-modal-overlay {
position: fixed;
@@ -5159,6 +5337,14 @@ header h1 .tagline {
min-height: 0;
}
.bt-side-panels {
width: 100%;
}
.bt-tracker-list {
max-height: 280px;
}
.bt-device-list {
width: 100%;
min-width: auto;
+34 -2
View File
@@ -8,7 +8,7 @@ const CommandPalette = (function() {
let activeIndex = 0;
let filteredItems = [];
const modeCommands = [
const fallbackModeCommands = [
{ mode: 'pager', label: 'Pager' },
{ mode: 'sensor', label: '433MHz Sensors' },
{ mode: 'rtlamr', label: 'Meters' },
@@ -30,6 +30,38 @@ const CommandPalette = (function() {
{ mode: 'spaceweather', label: 'Space Weather' },
];
function getModeCommands() {
const commands = [];
const seenModes = new Set();
const catalog = window.interceptModeCatalog;
if (catalog && typeof catalog === 'object') {
for (const [mode, meta] of Object.entries(catalog)) {
if (!mode || seenModes.has(mode)) continue;
const label = String((meta && meta.label) || mode).trim();
commands.push({ mode, label });
seenModes.add(mode);
}
if (commands.length > 0) return commands;
}
const navNodes = document.querySelectorAll('.mode-nav-btn[data-mode], .mobile-nav-btn[data-mode]');
navNodes.forEach((node) => {
if (node.tagName === 'A') {
const href = String(node.getAttribute('href') || '');
if (href.includes('/dashboard')) return;
}
const mode = String(node.dataset.mode || '').trim();
if (!mode || seenModes.has(mode)) return;
const label = String(node.dataset.modeLabel || node.textContent || mode).trim();
commands.push({ mode, label });
seenModes.add(mode);
});
if (commands.length > 0) return commands;
return fallbackModeCommands.slice();
}
function init() {
buildDOM();
registerHotkeys();
@@ -189,7 +221,7 @@ const CommandPalette = (function() {
},
];
for (const modeEntry of modeCommands) {
for (const modeEntry of getModeCommands()) {
commands.push({
title: `Switch Mode: ${modeEntry.label}`,
description: 'Navigate directly to mode',
+38 -4
View File
@@ -3,6 +3,12 @@ const RunState = (function() {
const REFRESH_MS = 5000;
const CHIP_MODES = ['pager', 'sensor', 'wifi', 'bluetooth', 'adsb', 'ais', 'acars', 'vdl2', 'aprs', 'dsc', 'dmr', 'subghz'];
const MODE_ALIASES = {
bt: 'bluetooth',
bt_locate: 'bluetooth',
btlocate: 'bluetooth',
aircraft: 'adsb',
};
const modeLabels = {
pager: 'Pager',
@@ -69,7 +75,7 @@ const RunState = (function() {
const original = window.switchMode;
const wrapped = function(mode) {
if (mode) {
activeMode = String(mode);
activeMode = normalizeMode(String(mode));
}
const result = original.apply(this, arguments);
markActiveChip();
@@ -110,7 +116,7 @@ const RunState = (function() {
return;
}
const processes = data.processes || {};
const processes = normalizeProcesses(data.processes || {});
for (const mode of CHIP_MODES) {
const isRunning = Boolean(processes[mode]);
chipsContainer.appendChild(buildChip(modeLabels[mode] || mode.toUpperCase(), isRunning, mode));
@@ -146,7 +152,7 @@ const RunState = (function() {
document.querySelectorAll('#runStateChips .run-state-chip').forEach((chip) => {
chip.classList.remove('active');
if (chip.dataset.mode && chip.dataset.mode === activeMode) {
if (chip.dataset.mode && chip.dataset.mode === normalizeMode(activeMode)) {
chip.classList.add('active');
}
});
@@ -154,7 +160,11 @@ const RunState = (function() {
function inferCurrentMode() {
const modeParam = new URLSearchParams(window.location.search).get('mode');
if (modeParam) return modeParam;
if (modeParam) return normalizeMode(modeParam);
if (typeof window.currentMode === 'string' && window.currentMode) {
return normalizeMode(window.currentMode);
}
const indicator = document.getElementById('activeModeIndicator');
if (!indicator) return 'pager';
@@ -163,6 +173,7 @@ const RunState = (function() {
const normalized = text.toLowerCase();
if (normalized.includes('wifi')) return 'wifi';
if (normalized.includes('bluetooth')) return 'bluetooth';
if (normalized.includes('bt locate')) return 'bluetooth';
if (normalized.includes('ads-b')) return 'adsb';
if (normalized.includes('ais')) return 'ais';
if (normalized.includes('acars')) return 'acars';
@@ -175,6 +186,29 @@ const RunState = (function() {
return 'pager';
}
function normalizeMode(mode) {
const value = String(mode || '').trim().toLowerCase();
if (!value) return 'pager';
return MODE_ALIASES[value] || value;
}
function normalizeProcesses(raw) {
const processes = Object.assign({}, raw || {});
processes.bluetooth = Boolean(
processes.bluetooth ||
processes.bt ||
processes.bt_scan ||
processes.btlocate ||
processes.bt_locate
);
processes.wifi = Boolean(
processes.wifi ||
processes.wifi_scan ||
processes.wlan
);
return processes;
}
function extractMessage(err) {
if (!err) return 'Unknown error';
if (typeof err === 'string') return err;
+36 -4
View File
@@ -435,10 +435,16 @@ const Settings = {
};
// Settings modal functions
let lastSettingsFocusEl = null;
function showSettings() {
const modal = document.getElementById('settingsModal');
if (modal) {
lastSettingsFocusEl = document.activeElement;
modal.classList.add('active');
modal.setAttribute('aria-hidden', 'false');
const content = modal.querySelector('.settings-content');
if (content) content.focus();
Settings.init().then(() => {
Settings.checkAssets();
});
@@ -449,18 +455,27 @@ function hideSettings() {
const modal = document.getElementById('settingsModal');
if (modal) {
modal.classList.remove('active');
modal.setAttribute('aria-hidden', 'true');
if (lastSettingsFocusEl && typeof lastSettingsFocusEl.focus === 'function') {
lastSettingsFocusEl.focus();
}
}
}
function switchSettingsTab(tabName) {
// Update tab buttons
document.querySelectorAll('.settings-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
const isActive = tab.dataset.tab === tabName;
tab.classList.toggle('active', isActive);
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
// Update sections
document.querySelectorAll('.settings-section').forEach(section => {
section.classList.toggle('active', section.id === `settings-${tabName}`);
const isActive = section.id === `settings-${tabName}`;
section.classList.toggle('active', isActive);
section.hidden = !isActive;
section.setAttribute('role', 'tabpanel');
});
// Load tools/dependencies when that tab is selected
@@ -560,6 +575,7 @@ function loadSettingsTools() {
// Initialize settings on page load
document.addEventListener('DOMContentLoaded', () => {
Settings.init();
switchSettingsTab('offline');
});
// =============================================================================
@@ -919,12 +935,17 @@ const _originalSwitchSettingsTab = typeof switchSettingsTab !== 'undefined' ? sw
function switchSettingsTab(tabName) {
// Update tab buttons
document.querySelectorAll('.settings-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
const isActive = tab.dataset.tab === tabName;
tab.classList.toggle('active', isActive);
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
// Update sections
document.querySelectorAll('.settings-section').forEach(section => {
section.classList.toggle('active', section.id === `settings-${tabName}`);
const isActive = section.id === `settings-${tabName}`;
section.classList.toggle('active', isActive);
section.hidden = !isActive;
section.setAttribute('role', 'tabpanel');
});
// Load content based on tab
@@ -1026,3 +1047,14 @@ function setAnimationsEnabled(enabled) {
}
localStorage.setItem('intercept-animations', enabled ? 'on' : 'off');
}
if (!window._settingsEscapeHandlerBound) {
window._settingsEscapeHandlerBound = true;
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
const modal = document.getElementById('settingsModal');
if (modal && modal.classList.contains('active')) {
hideSettings();
}
});
}
+36
View File
@@ -177,6 +177,37 @@ const AppFeedback = (function() {
return text.includes('script error') || text.includes('resizeobserver loop limit exceeded');
}
function renderCollectionState(container, options) {
if (!container) return null;
const opts = options || {};
const type = String(opts.type || 'empty').toLowerCase();
const message = String(opts.message || (type === 'loading' ? 'Loading...' : 'No data available'));
const className = opts.className || `app-collection-state is-${type}`;
container.innerHTML = '';
if (container.tagName === 'TBODY') {
const row = document.createElement('tr');
row.className = 'app-collection-state-row';
const cell = document.createElement('td');
const columns = Number.isFinite(opts.columns) ? opts.columns : 1;
cell.colSpan = Math.max(1, columns);
const state = document.createElement('div');
state.className = className;
state.textContent = message;
cell.appendChild(state);
row.appendChild(cell);
container.appendChild(row);
return row;
}
const state = document.createElement('div');
state.className = className;
state.textContent = message;
container.appendChild(state);
return state;
}
function isNetworkError(message) {
const text = String(message || '').toLowerCase();
return text.includes('networkerror') || text.includes('failed to fetch') || text.includes('timeout');
@@ -192,6 +223,7 @@ const AppFeedback = (function() {
toast,
reportError,
removeToast,
renderCollectionState,
};
})();
@@ -207,6 +239,10 @@ window.reportActionableError = function(context, error, options) {
return AppFeedback.reportError(context, error, options);
};
window.renderCollectionState = function(container, options) {
return AppFeedback.renderCollectionState(container, options);
};
document.addEventListener('DOMContentLoaded', () => {
AppFeedback.init();
});
+232 -135
View File
@@ -38,6 +38,11 @@ const BluetoothMode = (function() {
let currentDeviceFilter = 'all';
let currentSearchTerm = '';
let visibleDeviceCount = 0;
let pendingDeviceFlush = false;
let selectedDeviceNeedsRefresh = false;
let filterListenersBound = false;
let listListenersBound = false;
const pendingDeviceIds = new Set();
// Agent support
let showAllAgentsMode = false;
@@ -111,8 +116,9 @@ const BluetoothMode = (function() {
// Initialize legacy heatmap (zone counts)
initHeatmap();
// Initialize device list filters
initDeviceFilters();
// Initialize device list filters
initDeviceFilters();
initListInteractions();
// Set initial panel states
updateVisualizationPanels();
@@ -122,6 +128,7 @@ const BluetoothMode = (function() {
* Initialize device list filter buttons
*/
function initDeviceFilters() {
if (filterListenersBound) return;
const filterContainer = document.getElementById('btDeviceFilters');
if (filterContainer) {
filterContainer.addEventListener('click', (e) => {
@@ -148,6 +155,35 @@ const BluetoothMode = (function() {
applyDeviceFilter();
});
}
filterListenersBound = true;
}
function initListInteractions() {
if (listListenersBound) return;
if (deviceContainer) {
deviceContainer.addEventListener('click', (event) => {
const locateBtn = event.target.closest('.bt-locate-btn[data-locate-id]');
if (locateBtn) {
event.preventDefault();
locateById(locateBtn.dataset.locateId);
return;
}
const row = event.target.closest('.bt-device-row[data-bt-device-id]');
if (!row) return;
selectDevice(row.dataset.btDeviceId);
});
}
const trackerList = document.getElementById('btTrackerList');
if (trackerList) {
trackerList.addEventListener('click', (event) => {
const row = event.target.closest('.bt-tracker-item[data-device-id]');
if (!row) return;
selectDevice(row.dataset.deviceId);
});
}
listListenersBound = true;
}
/**
@@ -192,6 +228,18 @@ const BluetoothMode = (function() {
visibleDeviceCount = visibleCount;
let stateEl = deviceContainer.querySelector('.bt-device-filter-state');
if (visibleCount === 0 && devices.size > 0) {
if (!stateEl) {
stateEl = document.createElement('div');
stateEl.className = 'bt-device-filter-state app-collection-state is-empty';
deviceContainer.appendChild(stateEl);
}
stateEl.textContent = 'No devices match current filters';
} else if (stateEl) {
stateEl.remove();
}
// Update visible count
updateFilteredCount();
}
@@ -915,14 +963,25 @@ const BluetoothMode = (function() {
function setScanning(scanning) {
isScanning = scanning;
if (startBtn) startBtn.style.display = scanning ? 'none' : 'block';
if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none';
if (scanning && deviceContainer) {
deviceContainer.innerHTML = '';
devices.clear();
resetStats();
}
if (startBtn) startBtn.style.display = scanning ? 'none' : 'block';
if (stopBtn) stopBtn.style.display = scanning ? 'block' : 'none';
if (scanning && deviceContainer) {
pendingDeviceIds.clear();
selectedDeviceNeedsRefresh = false;
pendingDeviceFlush = false;
if (typeof renderCollectionState === 'function') {
renderCollectionState(deviceContainer, { type: 'loading', message: 'Scanning for Bluetooth devices...' });
} else {
deviceContainer.innerHTML = '';
}
devices.clear();
resetStats();
} else if (!scanning && deviceContainer && devices.size === 0) {
if (typeof renderCollectionState === 'function') {
renderCollectionState(deviceContainer, { type: 'empty', message: 'Start scanning to discover Bluetooth devices' });
}
}
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
@@ -1087,17 +1146,43 @@ const BluetoothMode = (function() {
}, pollInterval);
}
function handleDeviceUpdate(device) {
devices.set(device.device_id, device);
renderDevice(device);
updateDeviceCount();
updateStatsFromDevices();
updateVisualizationPanels();
updateProximityZones();
// Update new proximity radar
updateRadar();
}
function handleDeviceUpdate(device) {
devices.set(device.device_id, device);
pendingDeviceIds.add(device.device_id);
if (selectedDeviceId === device.device_id) {
selectedDeviceNeedsRefresh = true;
}
scheduleDeviceFlush();
}
function scheduleDeviceFlush() {
if (pendingDeviceFlush) return;
pendingDeviceFlush = true;
requestAnimationFrame(() => {
pendingDeviceFlush = false;
pendingDeviceIds.forEach((deviceId) => {
const device = devices.get(deviceId);
if (device) {
renderDevice(device, false);
}
});
pendingDeviceIds.clear();
applyDeviceFilter();
updateDeviceCount();
updateStatsFromDevices();
updateVisualizationPanels();
updateProximityZones();
updateRadar();
if (selectedDeviceNeedsRefresh && selectedDeviceId && devices.has(selectedDeviceId)) {
showDeviceDetail(selectedDeviceId);
}
selectedDeviceNeedsRefresh = false;
});
}
/**
* Update stats from all devices
@@ -1171,83 +1256,83 @@ const BluetoothMode = (function() {
// Tracker Detection - Enhanced display with confidence and evidence
const trackerList = document.getElementById('btTrackerList');
if (trackerList) {
if (devices.size === 0) {
trackerList.innerHTML = '<div style="color:#666;padding:10px;text-align:center;font-size:11px;">Start scanning to detect trackers</div>';
} else if (deviceStats.trackers.length === 0) {
trackerList.innerHTML = '<div style="color:#22c55e;padding:10px;text-align:center;font-size:11px;">No trackers detected</div>';
} else {
// Sort by risk score (highest first), then confidence
const sortedTrackers = [...deviceStats.trackers].sort((a, b) => {
const riskA = a.risk_score || 0;
const riskB = b.risk_score || 0;
if (riskB !== riskA) return riskB - riskA;
const confA = a.tracker_confidence_score || 0;
const confB = b.tracker_confidence_score || 0;
return confB - confA;
});
trackerList.innerHTML = sortedTrackers.map(t => {
// Get tracker type badge color based on confidence
const confidence = t.tracker_confidence || 'low';
const confColor = confidence === 'high' ? '#ef4444' :
confidence === 'medium' ? '#f97316' : '#eab308';
const confBg = confidence === 'high' ? 'rgba(239,68,68,0.2)' :
confidence === 'medium' ? 'rgba(249,115,22,0.2)' : 'rgba(234,179,8,0.2)';
// Risk score indicator
const riskScore = t.risk_score || 0;
const riskColor = riskScore >= 0.5 ? '#ef4444' : riskScore >= 0.3 ? '#f97316' : '#666';
// Tracker type label
const trackerType = t.tracker_name || t.tracker_type || 'Unknown Tracker';
// Build evidence tooltip (first 2 items)
const evidence = (t.tracker_evidence || []).slice(0, 2);
const evidenceHtml = evidence.length > 0
? '<div style="font-size:9px;color:#888;margin-top:3px;font-style:italic;">' +
evidence.map(e => '• ' + escapeHtml(e)).join('<br>') +
'</div>'
: '';
const deviceIdEscaped = escapeHtml(t.device_id).replace(/'/g, "\\'");
return '<div class="bt-tracker-item" style="padding:8px;border-bottom:1px solid rgba(255,255,255,0.05);cursor:pointer;" onclick="BluetoothMode.selectDevice(\'' + deviceIdEscaped + '\')">' +
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
'<div style="display:flex;align-items:center;gap:6px;">' +
'<span style="background:' + confBg + ';color:' + confColor + ';font-size:9px;padding:2px 5px;border-radius:3px;font-weight:600;">' + confidence.toUpperCase() + '</span>' +
'<span style="color:#fff;font-size:11px;">' + escapeHtml(trackerType) + '</span>' +
'</div>' +
'<div style="display:flex;align-items:center;gap:8px;">' +
(riskScore >= 0.3 ? '<span style="color:' + riskColor + ';font-size:9px;font-weight:600;">RISK ' + Math.round(riskScore * 100) + '%</span>' : '') +
'<span style="color:#666;font-size:10px;">' + (t.rssi_current || '--') + ' dBm</span>' +
'</div>' +
'</div>' +
'<div style="display:flex;justify-content:space-between;margin-top:3px;">' +
'<span style="font-size:9px;color:#888;font-family:monospace;">' + (t.address_type === 'uuid' ? formatAddress(t) : t.address) + '</span>' +
'<span style="font-size:9px;color:#666;">Seen ' + (t.seen_count || 0) + 'x</span>' +
'</div>' +
evidenceHtml +
'</div>';
}).join('');
}
}
}
if (trackerList) {
if (devices.size === 0) {
if (typeof renderCollectionState === 'function') {
renderCollectionState(trackerList, { type: 'empty', message: 'Start scanning to detect trackers' });
} else {
trackerList.innerHTML = '<div class="app-collection-state is-empty">Start scanning to detect trackers</div>';
}
} else if (deviceStats.trackers.length === 0) {
if (typeof renderCollectionState === 'function') {
renderCollectionState(trackerList, { type: 'empty', message: 'No trackers detected' });
} else {
trackerList.innerHTML = '<div class="app-collection-state is-empty">No trackers detected</div>';
}
} else {
// Sort by risk score (highest first), then confidence
const sortedTrackers = [...deviceStats.trackers].sort((a, b) => {
const riskA = a.risk_score || 0;
const riskB = b.risk_score || 0;
if (riskB !== riskA) return riskB - riskA;
const confA = a.tracker_confidence_score || 0;
const confB = b.tracker_confidence_score || 0;
return confB - confA;
});
trackerList.innerHTML = sortedTrackers.map((t) => {
const confidence = t.tracker_confidence || 'low';
const riskScore = t.risk_score || 0;
const trackerType = t.tracker_name || t.tracker_type || 'Unknown Tracker';
const evidence = (t.tracker_evidence || []).slice(0, 2);
const evidenceHtml = evidence.length > 0
? `<div class="bt-tracker-evidence">${evidence.map((e) => `${escapeHtml(e)}`).join('<br>')}</div>`
: '';
const riskClass = riskScore >= 0.5 ? 'high' : riskScore >= 0.3 ? 'medium' : 'low';
const riskHtml = riskScore >= 0.3
? `<span class="bt-tracker-risk bt-risk-${riskClass}">RISK ${Math.round(riskScore * 100)}%</span>`
: '';
return `
<div class="bt-tracker-item bt-tracker-confidence-${escapeHtml(confidence)}" data-device-id="${escapeAttr(t.device_id)}" role="button" tabindex="0" data-keyboard-activate="true">
<div class="bt-tracker-row-top">
<div class="bt-tracker-left">
<span class="bt-tracker-confidence">${escapeHtml(confidence.toUpperCase())}</span>
<span class="bt-tracker-type">${escapeHtml(trackerType)}</span>
</div>
<div class="bt-tracker-right">
${riskHtml}
<span class="bt-tracker-rssi">${t.rssi_current != null ? t.rssi_current : '--'} dBm</span>
</div>
</div>
<div class="bt-tracker-row-bottom">
<span class="bt-tracker-address">${escapeHtml(t.address_type === 'uuid' ? formatAddress(t) : (t.address || '--'))}</span>
<span class="bt-tracker-seen">Seen ${t.seen_count || 0}x</span>
</div>
${evidenceHtml}
</div>
`;
}).join('');
}
}
}
function updateDeviceCount() {
updateFilteredCount();
}
function renderDevice(device) {
function renderDevice(device, reapplyFilter = true) {
if (!deviceContainer) {
deviceContainer = document.getElementById('btDeviceListContent');
if (!deviceContainer) return;
}
const escapedId = CSS.escape(device.device_id);
const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]');
const cardHtml = createSimpleDeviceCard(device);
deviceContainer.querySelectorAll('.app-collection-state, .bt-device-filter-state').forEach((el) => el.remove());
const escapedId = CSS.escape(device.device_id);
const existingCard = deviceContainer.querySelector('[data-bt-device-id="' + escapedId + '"]');
const cardHtml = createSimpleDeviceCard(device);
if (existingCard) {
existingCard.outerHTML = cardHtml;
@@ -1255,8 +1340,9 @@ const BluetoothMode = (function() {
deviceContainer.insertAdjacentHTML('afterbegin', cardHtml);
}
// Re-apply filter after rendering
applyDeviceFilter();
if (reapplyFilter) {
applyDeviceFilter();
}
}
function createSimpleDeviceCard(device) {
@@ -1277,12 +1363,11 @@ const BluetoothMode = (function() {
// RSSI typically ranges from -100 (weak) to -30 (very strong)
const rssiPercent = rssi != null ? Math.max(0, Math.min(100, ((rssi + 100) / 70) * 100)) : 0;
const displayName = device.name || formatDeviceId(device.address);
const name = escapeHtml(displayName);
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
const displayName = device.name || formatDeviceId(device.address);
const name = escapeHtml(displayName);
const addr = escapeHtml(isUuidAddress(device) ? formatAddress(device) : (device.address || 'Unknown'));
const mfr = device.manufacturer_name ? escapeHtml(device.manufacturer_name) : '';
const seenCount = device.seen_count || 0;
const deviceIdEscaped = escapeHtml(device.device_id).replace(/'/g, "\\'");
const searchIndex = [
displayName,
device.address,
@@ -1373,14 +1458,14 @@ const BluetoothMode = (function() {
}
const secondaryInfo = secondaryParts.join(' · ');
// Row border color - highlight trackers in red/orange
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' :
isTracker ? '#f97316' : rssiColor;
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '" data-bt-device-id="' + escapeHtml(device.device_id) + '" data-is-new="' + isNew + '" data-has-name="' + hasName + '" data-rssi="' + (rssi || -100) + '" data-is-tracker="' + isTracker + '" data-search="' + escapeAttr(searchIndex) + '" onclick="BluetoothMode.selectDevice(\'' + deviceIdEscaped + '\')" style="border-left-color:' + borderColor + ';">' +
'<div class="bt-row-main">' +
'<div class="bt-row-left">' +
protoBadge +
// Row border color - highlight trackers in red/orange
const borderColor = isTracker && trackerConfidence === 'high' ? '#ef4444' :
isTracker ? '#f97316' : rssiColor;
return '<div class="bt-device-row' + (isTracker ? ' is-tracker' : '') + '" data-bt-device-id="' + escapeAttr(device.device_id) + '" data-is-new="' + isNew + '" data-has-name="' + hasName + '" data-rssi="' + (rssi || -100) + '" data-is-tracker="' + isTracker + '" data-search="' + escapeAttr(searchIndex) + '" role="button" tabindex="0" data-keyboard-activate="true" style="border-left-color:' + borderColor + ';">' +
'<div class="bt-row-main">' +
'<div class="bt-row-left">' +
protoBadge +
'<span class="bt-device-name">' + name + '</span>' +
trackerBadge +
irkBadge +
@@ -1395,13 +1480,13 @@ const BluetoothMode = (function() {
'</div>' +
statusDot +
'</div>' +
'</div>' +
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
'<div class="bt-row-actions">' +
'<button class="bt-locate-btn" data-locate-id="' + escapeHtml(device.device_id) + '" onclick="event.stopPropagation(); BluetoothMode.locateById(this.dataset.locateId)">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
'Locate</button>' +
'</div>' +
'</div>' +
'<div class="bt-row-secondary">' + secondaryInfo + '</div>' +
'<div class="bt-row-actions">' +
'<button type="button" class="bt-locate-btn" data-locate-id="' + escapeAttr(device.device_id) + '">' +
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>' +
'Locate</button>' +
'</div>' +
'</div>';
}
@@ -1532,18 +1617,22 @@ const BluetoothMode = (function() {
/**
* Clear all collected data.
*/
function clearData() {
devices.clear();
resetStats();
if (deviceContainer) {
deviceContainer.innerHTML = '';
}
updateDeviceCount();
updateProximityZones();
updateRadar();
}
function clearData() {
devices.clear();
pendingDeviceIds.clear();
pendingDeviceFlush = false;
selectedDeviceNeedsRefresh = false;
resetStats();
clearSelection();
if (deviceContainer) {
if (typeof renderCollectionState === 'function') {
renderCollectionState(deviceContainer, { type: 'empty', message: 'Start scanning to discover Bluetooth devices' });
} else {
deviceContainer.innerHTML = '';
}
}
}
/**
* Toggle "Show All Agents" mode.
@@ -1578,19 +1667,27 @@ const BluetoothMode = (function() {
}
});
toRemove.forEach(deviceId => devices.delete(deviceId));
// Re-render device list
if (deviceContainer) {
deviceContainer.innerHTML = '';
devices.forEach(device => renderDevice(device));
}
updateDeviceCount();
updateStatsFromDevices();
updateVisualizationPanels();
updateProximityZones();
updateRadar();
toRemove.forEach(deviceId => devices.delete(deviceId));
// Re-render device list
if (deviceContainer) {
deviceContainer.innerHTML = '';
devices.forEach(device => renderDevice(device, false));
applyDeviceFilter();
if (devices.size === 0 && typeof renderCollectionState === 'function') {
renderCollectionState(deviceContainer, { type: 'empty', message: 'No devices for current agent' });
}
}
if (selectedDeviceId && !devices.has(selectedDeviceId)) {
clearSelection();
}
updateDeviceCount();
updateStatsFromDevices();
updateVisualizationPanels();
updateProximityZones();
updateRadar();
}
/**
+268 -162
View File
@@ -120,10 +120,23 @@ const WiFiMode = (function() {
let channelStats = [];
let recommendations = [];
// UI state
let selectedNetwork = null;
let currentFilter = 'all';
let currentSort = { field: 'rssi', order: 'desc' };
// 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
@@ -152,10 +165,11 @@ const WiFiMode = (function() {
// Initialize components
initScanModeTabs();
initNetworkFilters();
initSortControls();
initProximityRadar();
initChannelChart();
initNetworkFilters();
initSortControls();
initProximityRadar();
initChannelChart();
scheduleRender({ table: true, stats: true, radar: true, chart: true });
// Check if already scanning
checkScanStatus();
@@ -364,14 +378,16 @@ const WiFiMode = (function() {
// Scan Mode Tabs
// ==========================================================================
function initScanModeTabs() {
if (elements.scanModeQuick) {
elements.scanModeQuick.addEventListener('click', () => setScanMode('quick'));
}
if (elements.scanModeDeep) {
elements.scanModeDeep.addEventListener('click', () => setScanMode('deep'));
}
}
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;
@@ -682,10 +698,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)
@@ -693,15 +709,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
updateNetworkTable();
updateStats();
updateProximityRadar();
updateChannelChart();
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 => {
@@ -910,22 +923,25 @@ const WiFiMode = (function() {
}
}
function handleNetworkUpdate(network) {
networks.set(network.bssid, network);
updateNetworkRow(network);
updateStats();
updateProximityRadar();
updateChannelChart();
if (onNetworkUpdate) onNetworkUpdate(network);
}
function handleClientUpdate(client) {
clients.set(client.mac, client);
updateStats();
// 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);
}
@@ -939,32 +955,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)`;
updateNetworkRow(network);
// 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 (!elements.networkFilters) return;
elements.networkFilters.addEventListener('click', (e) => {
if (e.target.matches('.wifi-filter-btn')) {
const filter = e.target.dataset.filter;
setNetworkFilter(filter);
}
});
}
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;
@@ -979,10 +1000,11 @@ const WiFiMode = (function() {
updateNetworkTable();
}
function initSortControls() {
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;
@@ -992,16 +1014,54 @@ const WiFiMode = (function() {
currentSort.field = field;
currentSort.order = 'desc';
}
updateNetworkTable();
}
});
}
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':
@@ -1051,22 +1111,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('');
}
// Render table
elements.networkTableBody.innerHTML = filtered.map(n => createNetworkRow(n)).join('');
}
function createNetworkRow(network) {
const rssi = network.rssi_current;
const signalClass = rssi >= -50 ? 'signal-strong' :
rssi >= -70 ? 'signal-medium' :
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
const securityClass = network.security === 'Open' ? 'security-open' :
network.security === 'WEP' ? 'security-wep' :
network.security.includes('WPA3') ? 'security-wpa3' : 'security-wpa';
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>' : '';
@@ -1075,22 +1157,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)}"
onclick="WiFiMode.selectNetwork('${escapeHtml(network.bssid)}')">
<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(network.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>
@@ -1099,15 +1184,12 @@ const WiFiMode = (function() {
`;
}
function updateNetworkRow(network) {
const row = elements.networkTableBody?.querySelector(`tr[data-bssid="${network.bssid}"]`);
if (row) {
row.outerHTML = createNetworkRow(network);
} else {
// Add new row
updateNetworkTable();
}
}
function updateNetworkRow(network) {
scheduleRender({
table: true,
detail: selectedNetwork === network.bssid,
});
}
function selectNetwork(bssid) {
selectedNetwork = bssid;
@@ -1130,8 +1212,9 @@ const WiFiMode = (function() {
// Detail Panel
// ==========================================================================
function updateDetailPanel(bssid) {
if (!elements.detailDrawer) return;
function updateDetailPanel(bssid, options = {}) {
const { refreshClients = true } = options;
if (!elements.detailDrawer) return;
const network = networks.get(bssid);
if (!network) {
@@ -1176,9 +1259,11 @@ const WiFiMode = (function() {
// Show the drawer
elements.detailDrawer.classList.add('open');
// Fetch and display clients for this network
fetchClientsForNetwork(network.bssid);
}
// Fetch and display clients for this network
if (refreshClients) {
fetchClientsForNetwork(network.bssid);
}
}
function closeDetail() {
selectedNetwork = null;
@@ -1194,12 +1279,18 @@ const WiFiMode = (function() {
// Client Display
// ==========================================================================
async function fetchClientsForNetwork(bssid) {
if (!elements.detailClientList) return;
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
@@ -1208,28 +1299,44 @@ const WiFiMode = (function() {
response = await fetch(`${CONFIG.apiBase}/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
}
if (!response.ok) {
// Hide client list on error
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 {
elements.detailClientList.style.display = 'none';
}
} catch (error) {
console.debug('[WiFiMode] Error fetching clients:', error);
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');
@@ -1586,17 +1693,16 @@ const WiFiMode = (function() {
/**
* Clear all collected data.
*/
function clearData() {
networks.clear();
clients.clear();
probeRequests = [];
channelStats = [];
recommendations = [];
updateNetworkTable();
updateStats();
updateProximityRadar();
updateChannelChart();
function clearData() {
networks.clear();
clients.clear();
probeRequests = [];
channelStats = [];
recommendations = [];
if (selectedNetwork) {
closeDetail();
}
scheduleRender({ table: true, stats: true, radar: true, chart: true });
}
/**
@@ -1642,12 +1748,12 @@ const WiFiMode = (function() {
clientsToRemove.push(mac);
}
});
clientsToRemove.forEach(mac => clients.delete(mac));
updateNetworkTable();
updateStats();
updateProximityRadar();
}
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.