mirror of
https://github.com/smittix/intercept.git
synced 2026-04-29 09:09:59 -07:00
Improve cross-app UX: accessibility, mode consistency, and render performance
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user