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 cfe03317c9
commit 963bcdf9fa
11 changed files with 1051 additions and 507 deletions

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.