Release v2.13.0 - WiFi client display in AP detail drawer

Features:
- Display connected clients for access points in detail drawer
- Real-time client updates via SSE streaming
- Client cards show MAC, vendor, RSSI, probed SSIDs, and last seen
- Count badge in Connected Clients header

Other changes:
- Updated aircraft database
- CSS and template refinements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-04 17:44:23 +00:00
parent fc5a5d3862
commit e48659c159
27 changed files with 365 additions and 132 deletions
+5 -5
View File
@@ -246,10 +246,10 @@ function toggleSection(el) {
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const currentTheme = html.getAttribute('data-theme') || 'dark';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
localStorage.setItem('intercept-theme', newTheme);
// Update button text
const btn = document.getElementById('themeToggle');
@@ -259,7 +259,7 @@ function toggleTheme() {
}
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'dark';
const savedTheme = localStorage.getItem('intercept-theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
const btn = document.getElementById('themeToggle');
if (btn) {
@@ -373,7 +373,7 @@ function showInfo(text) {
const infoEl = document.createElement('div');
infoEl.className = 'info-msg';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #0a0a0a; border: 1px solid #1a1a1a; border-left: 2px solid #00d4ff; font-family: "Space Mono", monospace; font-size: 11px; color: #888; word-break: break-all;';
infoEl.textContent = text;
output.insertBefore(infoEl, output.firstChild);
}
@@ -387,7 +387,7 @@ function showError(text) {
const errorEl = document.createElement('div');
errorEl.className = 'error-msg';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "JetBrains Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.style.cssText = 'padding: 12px 15px; margin-bottom: 8px; background: #1a0a0a; border: 1px solid #2a1a1a; border-left: 2px solid #ff3366; font-family: "Space Mono", monospace; font-size: 11px; color: #ff6688; word-break: break-all;';
errorEl.textContent = '⚠ ' + text;
output.insertBefore(errorEl, output.firstChild);
}
+2 -2
View File
@@ -813,11 +813,11 @@ function renderUpdateStatus(data) {
<div style="display: grid; gap: 8px; font-size: 12px;">
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Current Version</span>
<span style="font-family: 'JetBrains Mono', monospace; color: var(--text-primary);">v${data.current_version}</span>
<span style="font-family: 'Space Mono', monospace; color: var(--text-primary);">v${data.current_version}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="color: var(--text-dim);">Latest Version</span>
<span style="font-family: 'JetBrains Mono', monospace; color: ${data.update_available ? 'var(--accent-green)' : 'var(--text-primary)'};">v${data.latest_version}</span>
<span style="font-family: 'Space Mono', monospace; color: ${data.update_available ? 'var(--accent-green)' : 'var(--text-primary)'};">v${data.latest_version}</span>
</div>
${data.last_check ? `
<div style="display: flex; justify-content: space-between;">
+1 -1
View File
@@ -1448,7 +1448,7 @@ function drawAudioVisualizer() {
}
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.font = '8px JetBrains Mono';
ctx.font = '8px Space Mono';
ctx.fillText('0', 2, canvas.height - 2);
ctx.fillText('4kHz', canvas.width / 4, canvas.height - 2);
ctx.fillText('8kHz', canvas.width / 2, canvas.height - 2);
+1 -1
View File
@@ -84,7 +84,7 @@ const SpyStations = (function() {
modeContainer.innerHTML = modes.map(m => `
<label class="inline-checkbox">
<input type="checkbox" data-mode="${m}" checked onchange="SpyStations.applyFilters()">
<span style="font-family: 'JetBrains Mono', monospace; font-size: 10px;">${m}</span>
<span style="font-family: 'Space Mono', monospace; font-size: 10px;">${m}</span>
</label>
`).join('');
}
+130
View File
@@ -887,6 +887,9 @@ const WiFiMode = (function() {
clients.set(client.mac, client);
updateStats();
// Update client display if this client belongs to the selected network
updateClientInList(client);
if (onClientUpdate) onClientUpdate(client);
}
@@ -1135,6 +1138,9 @@ const WiFiMode = (function() {
// Show the drawer
elements.detailDrawer.classList.add('open');
// Fetch and display clients for this network
fetchClientsForNetwork(network.bssid);
}
function closeDetail() {
@@ -1147,6 +1153,130 @@ const WiFiMode = (function() {
});
}
// ==========================================================================
// Client Display
// ==========================================================================
async function fetchClientsForNetwork(bssid) {
if (!elements.detailClientList) return;
try {
const isAgentMode = typeof currentAgent !== 'undefined' && currentAgent !== 'local';
let response;
if (isAgentMode) {
// Route through agent proxy
response = await fetch(`/controller/agents/${currentAgent}/wifi/v2/clients?bssid=${encodeURIComponent(bssid)}&associated=true`);
} else {
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;
}
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';
}
}
function renderClientList(clientList, bssid) {
const container = elements.detailClientList?.querySelector('.wifi-client-list');
const countBadge = document.getElementById('wifiClientCountBadge');
if (!container) return;
// Update count badge
if (countBadge) {
countBadge.textContent = clientList.length;
}
// Render client cards
container.innerHTML = clientList.map(client => {
const rssi = client.rssi_current;
const signalClass = rssi >= -50 ? 'signal-strong' :
rssi >= -70 ? 'signal-medium' :
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
// Format last seen time
const lastSeen = client.last_seen ? formatTime(client.last_seen) : '--';
// Build probed SSIDs badges
let probesHtml = '';
if (client.probed_ssids && client.probed_ssids.length > 0) {
const probes = client.probed_ssids.slice(0, 5); // Show max 5
probesHtml = `
<div class="wifi-client-probes">
${probes.map(ssid => `<span class="wifi-client-probe-badge">${escapeHtml(ssid)}</span>`).join('')}
${client.probed_ssids.length > 5 ? `<span class="wifi-client-probe-badge">+${client.probed_ssids.length - 5}</span>` : ''}
</div>
`;
}
return `
<div class="wifi-client-card" data-mac="${escapeHtml(client.mac)}">
<div class="wifi-client-identity">
<span class="wifi-client-mac">${escapeHtml(client.mac)}</span>
<span class="wifi-client-vendor">${escapeHtml(client.vendor || 'Unknown vendor')}</span>
${probesHtml}
</div>
<div class="wifi-client-signal">
<span class="wifi-client-rssi ${signalClass}">${rssi !== null && rssi !== undefined ? rssi + ' dBm' : '--'}</span>
<span class="wifi-client-lastseen">${lastSeen}</span>
</div>
</div>
`;
}).join('');
}
function updateClientInList(client) {
// Check if this client belongs to the currently selected network
if (!selectedNetwork || client.associated_bssid !== selectedNetwork) {
return;
}
const container = elements.detailClientList?.querySelector('.wifi-client-list');
if (!container) return;
const existingCard = container.querySelector(`[data-mac="${client.mac}"]`);
if (existingCard) {
// Update existing card's RSSI and last seen
const rssiEl = existingCard.querySelector('.wifi-client-rssi');
const lastSeenEl = existingCard.querySelector('.wifi-client-lastseen');
if (rssiEl && client.rssi_current !== null && client.rssi_current !== undefined) {
const rssi = client.rssi_current;
const signalClass = rssi >= -50 ? 'signal-strong' :
rssi >= -70 ? 'signal-medium' :
rssi >= -85 ? 'signal-weak' : 'signal-very-weak';
rssiEl.textContent = rssi + ' dBm';
rssiEl.className = 'wifi-client-rssi ' + signalClass;
}
if (lastSeenEl && client.last_seen) {
lastSeenEl.textContent = formatTime(client.last_seen);
}
} else {
// New client for this network - re-fetch the full list
fetchClientsForNetwork(selectedNetwork);
}
}
// ==========================================================================
// Statistics
// ==========================================================================