mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
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:
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "2026-01-11_fae1348c",
|
||||
"downloaded": "2026-01-12T15:55:42.769654Z"
|
||||
"version": "2026-02-01_ba81b697",
|
||||
"downloaded": "2026-02-04T17:06:54.806043Z"
|
||||
}
|
||||
11
config.py
11
config.py
@@ -7,10 +7,19 @@ import os
|
||||
import sys
|
||||
|
||||
# Application version
|
||||
VERSION = "2.12.1"
|
||||
VERSION = "2.13.0"
|
||||
|
||||
# Changelog - latest release notes (shown on welcome screen)
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "2.13.0",
|
||||
"date": "February 2026",
|
||||
"highlights": [
|
||||
"WiFi client display in AP detail drawer",
|
||||
"Real-time client updates via SSE streaming",
|
||||
"Probed SSID badges for connected clients",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.12.1",
|
||||
"date": "February 2026",
|
||||
|
||||
3
setup.sh
3
setup.sh
@@ -1092,4 +1092,7 @@ main() {
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
# Clear traps before exiting to prevent spurious errors during cleanup
|
||||
trap - ERR EXIT
|
||||
exit 0
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-panel: #0f1218;
|
||||
--bg-card: #151a23;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-panel: #0f1218;
|
||||
--bg-card: #141a24;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
/* CSS Variables (inherited from main theme) */
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--text-primary: #e0e0e0;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-panel: #0f1218;
|
||||
--bg-card: #151a23;
|
||||
@@ -440,7 +440,7 @@ body {
|
||||
padding: 10px 15px;
|
||||
background: rgba(74, 158, 255, 0.05);
|
||||
border-bottom: 1px solid rgba(74, 158, 255, 0.1);
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
@@ -512,7 +512,7 @@ body {
|
||||
}
|
||||
|
||||
.vessel-name {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-cyan);
|
||||
@@ -606,7 +606,7 @@ body {
|
||||
}
|
||||
|
||||
.vessel-item-name {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
@@ -1150,7 +1150,7 @@ body {
|
||||
}
|
||||
|
||||
.dsc-distress-alert .dsc-alert-header {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--accent-red);
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', ui-monospace, 'SF Mono', monospace;
|
||||
--font-mono: 'Space Mono', ui-monospace, 'SF Mono', 'Consolas', monospace;
|
||||
|
||||
/* Font sizes */
|
||||
--text-xs: 10px;
|
||||
|
||||
@@ -1,67 +1,18 @@
|
||||
/* Local font declarations for offline mode */
|
||||
|
||||
/* Inter - Primary UI font */
|
||||
/* Space Mono - Console font */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/Inter-Regular.woff2') format('woff2');
|
||||
src: url('/static/vendor/fonts/SpaceMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-family: 'Space Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* JetBrains Mono - Monospace/code font */
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/JetBrainsMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/JetBrainsMono-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/JetBrainsMono-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/static/vendor/fonts/JetBrainsMono-Bold.woff2') format('woff2');
|
||||
src: url('/static/vendor/fonts/SpaceMono-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -323,6 +323,11 @@
|
||||
box-shadow: 0 6px 14px rgba(5, 9, 15, 0.35);
|
||||
}
|
||||
|
||||
/* Position relative needed for absolute positioned icon children */
|
||||
.nav-tool-btn {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-nav-btn:focus-visible,
|
||||
.mode-nav-dropdown-btn:focus-visible,
|
||||
.nav-action-btn:focus-visible,
|
||||
@@ -330,3 +335,62 @@
|
||||
outline: 2px solid var(--accent-cyan, #4d7dbf);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Nav tool button SVG sizing and styling */
|
||||
.nav-tool-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
/* Theme toggle icon states */
|
||||
.nav-tool-btn .icon-sun,
|
||||
.nav-tool-btn .icon-moon {
|
||||
position: absolute;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon-sun {
|
||||
opacity: 0;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.nav-tool-btn .icon-moon {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .nav-tool-btn .icon-sun {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
[data-theme="light"] .nav-tool-btn .icon-moon {
|
||||
opacity: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Effects/animations toggle icon states */
|
||||
.nav-tool-btn .icon-effects-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-animations="off"] .nav-tool-btn .icon-effects-on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-animations="off"] .nav-tool-btn .icon-effects-off {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
/* Tactical dark palette */
|
||||
--bg-primary: #0a0c10;
|
||||
--bg-secondary: #0f1218;
|
||||
@@ -3808,6 +3808,86 @@ header h1 .tagline {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wifi-client-count-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
background: var(--accent-cyan);
|
||||
color: var(--bg-primary);
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wifi-client-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wifi-client-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.wifi-client-identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wifi-client-mac {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.wifi-client-vendor {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.wifi-client-probes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.wifi-client-probe-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 3px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.wifi-client-signal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.wifi-client-rssi {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.wifi-client-lastseen {
|
||||
font-size: 9px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* WiFi Responsive */
|
||||
@media (max-width: 1400px) {
|
||||
.wifi-main-content {
|
||||
@@ -6019,7 +6099,7 @@ body::before {
|
||||
}
|
||||
|
||||
.module-header {
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-cyan);
|
||||
@@ -6197,7 +6277,7 @@ body::before {
|
||||
/* Listening Mode Selector Buttons */
|
||||
.radio-mode-btn {
|
||||
padding: 12px 24px;
|
||||
font-family: 'Orbitron', 'JetBrains Mono', monospace;
|
||||
font-family: 'Orbitron', 'Space Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-dark: #0a0c10;
|
||||
--bg-panel: #0f1218;
|
||||
--bg-card: #151a23;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ==========================================================================
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
{% endif %}
|
||||
<!-- Core CSS variables -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_dashboard.css') }}">
|
||||
@@ -1387,7 +1389,7 @@ ACARS: ${r.statistics.acarsMessages} messages`;
|
||||
// Range label
|
||||
const rangeNm = Math.round(maxRange * ratio);
|
||||
ctx.fillStyle = 'rgba(0, 255, 255, 0.5)';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillText(`${rangeNm}`, this.centerX + r + 5, this.centerY + 4);
|
||||
});
|
||||
}
|
||||
@@ -1528,7 +1530,7 @@ ACARS: ${r.statistics.acarsMessages} messages`;
|
||||
// Label
|
||||
if (blip.callsign || blip.selected) {
|
||||
ctx.fillStyle = '#00ffff';
|
||||
ctx.font = '9px JetBrains Mono';
|
||||
ctx.font = '9px Space Mono';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(blip.callsign || blip.icao, blip.x + 8, blip.y - 5);
|
||||
if (blip.altitude) {
|
||||
@@ -1646,7 +1648,7 @@ ACARS: ${r.statistics.acarsMessages} messages`;
|
||||
// Range label
|
||||
const rangeNm = Math.round(maxRange * ratio);
|
||||
ctx.fillStyle = 'rgba(0, 255, 255, 0.4)';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillText(`${rangeNm}nm`, centerX + r + 5, centerY);
|
||||
});
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@
|
||||
|
||||
if (!points.length) {
|
||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||
ctx.font = '12px "JetBrains Mono", monospace';
|
||||
ctx.font = '12px "Space Mono", monospace';
|
||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||
return;
|
||||
}
|
||||
@@ -478,7 +478,7 @@
|
||||
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
|
||||
if (!series.length) {
|
||||
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
|
||||
ctx.font = '12px "JetBrains Mono", monospace';
|
||||
ctx.font = '12px "Space Mono", monospace';
|
||||
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
|
||||
return;
|
||||
}
|
||||
@@ -519,7 +519,7 @@
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
|
||||
ctx.font = '11px "JetBrains Mono", monospace';
|
||||
ctx.font = '11px "Space Mono", monospace';
|
||||
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
|
||||
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
{% endif %}
|
||||
<!-- Core CSS variables -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/ais_dashboard.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||
|
||||
@@ -707,7 +707,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
|
||||
<h6>Connected Clients</h6>
|
||||
<h6>Connected Clients <span class="wifi-client-count-badge" id="wifiClientCountBadge"></span></h6>
|
||||
<div class="wifi-client-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4259,7 +4259,7 @@
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -4275,7 +4275,7 @@
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -6891,7 +6891,7 @@
|
||||
|
||||
// Draw total in center
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 16px JetBrains Mono';
|
||||
ctx.font = 'bold 16px Space Mono';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(total, cx, cy);
|
||||
@@ -8661,7 +8661,7 @@
|
||||
// Label
|
||||
if (el > 0) {
|
||||
ctx.fillStyle = '#444';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(el + '°', cx, cy - r + 12);
|
||||
}
|
||||
@@ -8734,7 +8734,7 @@
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '11px JetBrains Mono';
|
||||
ctx.font = '11px Space Mono';
|
||||
ctx.fillText(pass.satellite, maxX + 10, maxY - 5);
|
||||
}
|
||||
}
|
||||
@@ -9319,14 +9319,10 @@
|
||||
// Theme toggle functions
|
||||
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';
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
html.removeAttribute('data-theme');
|
||||
} else {
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
html.setAttribute('data-theme', newTheme);
|
||||
|
||||
// Save to localStorage for instant load on next visit
|
||||
localStorage.setItem('intercept-theme', newTheme);
|
||||
@@ -9358,10 +9354,8 @@
|
||||
// Load saved theme and animations on page load
|
||||
(function () {
|
||||
// First apply localStorage theme for instant load (no flash)
|
||||
const localTheme = localStorage.getItem('intercept-theme');
|
||||
if (localTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
const localTheme = localStorage.getItem('intercept-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', localTheme);
|
||||
|
||||
// Apply animations preference
|
||||
const localAnimations = localStorage.getItem('intercept-animations');
|
||||
@@ -9377,11 +9371,7 @@
|
||||
const serverTheme = data.value;
|
||||
if (serverTheme !== localTheme) {
|
||||
// Server has different theme, apply it
|
||||
if (serverTheme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', serverTheme);
|
||||
localStorage.setItem('intercept-theme', serverTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<title>{% block title %}iNTERCEPT{% endblock %} // iNTERCEPT</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
|
||||
|
||||
{# Fonts - Conditional CDN/Local loading #}
|
||||
{% if offline_settings and offline_settings.fonts_source == 'local' %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||
{% else %}
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
{% endif %}
|
||||
{# Fonts - Conditional CDN/Local loading #}
|
||||
{% if offline_settings and offline_settings.fonts_source == 'local' %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||||
{% else %}
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
{% endif %}
|
||||
|
||||
{# Core CSS (Design System) #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
@@ -137,7 +137,7 @@
|
||||
}
|
||||
|
||||
// Apply saved theme
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const savedTheme = localStorage.getItem('intercept-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--font-sans: 'JetBrains Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Space Mono', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'Space Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-tertiary: #1a1a2e;
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
const current = html.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
localStorage.setItem('intercept-theme', next);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,12 +257,12 @@
|
||||
|
||||
// Apply saved preferences and start clock
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const savedTheme = localStorage.getItem('intercept-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
const savedAnimations = localStorage.getItem('animations');
|
||||
const savedAnimations = localStorage.getItem('intercept-animations');
|
||||
if (savedAnimations) {
|
||||
document.documentElement.setAttribute('data-animations', savedAnimations);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Web Fonts</span>
|
||||
<span class="settings-label-desc">JetBrains Mono</span>
|
||||
<span class="settings-label-desc">Space Mono</span>
|
||||
</div>
|
||||
<select id="fontsSource" class="settings-select" onchange="Settings.setFontsSource(this.value)">
|
||||
<option value="cdn">Google Fonts (Online)</option>
|
||||
@@ -105,7 +105,7 @@
|
||||
<span class="asset-badge checking" id="statusInter">Checking...</span>
|
||||
</div>
|
||||
<div class="asset-status-row">
|
||||
<span class="asset-name">JetBrains Mono</span>
|
||||
<span class="asset-name">Space Mono</span>
|
||||
<span class="asset-badge checking" id="statusJetbrains">Checking...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
{% endif %}
|
||||
<!-- Core CSS variables -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/satellite_dashboard.css') }}">
|
||||
@@ -593,7 +595,7 @@
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillText(el + '°', cx + 5, cy - r + 12);
|
||||
}
|
||||
|
||||
@@ -961,7 +963,7 @@
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 212, 255, 0.4)';
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillText(elRing + '°', cx + 5, cy - r + 12);
|
||||
}
|
||||
|
||||
@@ -1028,7 +1030,7 @@
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
|
||||
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
|
||||
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
|
||||
} else {
|
||||
@@ -1077,7 +1079,7 @@
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(satellites[selectedSatellite]?.name || 'SAT', x, y - 20);
|
||||
|
||||
ctx.font = '10px JetBrains Mono';
|
||||
ctx.font = '10px Space Mono';
|
||||
ctx.fillStyle = el > 0 ? '#00ff88' : '#ff4444';
|
||||
ctx.fillText(el.toFixed(1) + '°', x, y + 25);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user