feat(wifi): animated SVG proximity radar with sweep rotation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-03-26 22:26:47 +00:00
parent 0dbcb175c0
commit d1d44195c1
3 changed files with 102 additions and 29 deletions

View File

@@ -3855,6 +3855,16 @@ header h1 .tagline {
.wifi-zone.mid .wifi-zone-count { color: var(--accent-yellow); }
.wifi-zone.far .wifi-zone-count { color: var(--accent-red); }
.wifi-radar-sweep {
transform-origin: 105px 105px;
animation: wifi-radar-rotate 3s linear infinite;
}
@keyframes wifi-radar-rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* WiFi Analysis Panel (RIGHT) */
.wifi-analysis-panel {
display: flex;

View File

@@ -167,7 +167,6 @@ const WiFiMode = (function() {
initScanModeTabs();
initNetworkFilters();
initSortControls();
initProximityRadar();
initChannelChart();
scheduleRender({ table: true, stats: true, radar: true, chart: true });
@@ -201,7 +200,6 @@ const WiFiMode = (function() {
networkFilters: document.getElementById('wifiNetworkFilters'),
// Visualizations
proximityRadar: document.getElementById('wifiProximityRadar'),
channelChart: document.getElementById('wifiChannelChart'),
channelBandTabs: document.getElementById('wifiChannelBandTabs'),
@@ -1077,7 +1075,7 @@ const WiFiMode = (function() {
if (pendingRender.table) renderNetworks();
if (pendingRender.stats) updateStats();
if (pendingRender.radar) updateProximityRadar();
if (pendingRender.radar) renderRadar(Array.from(networks.values()));
if (pendingRender.chart) updateChannelChart();
if (pendingRender.detail && selectedBssid) {
updateDetailPanel(selectedBssid, { refreshClients: false });
@@ -1506,36 +1504,58 @@ const WiFiMode = (function() {
// Proximity Radar
// ==========================================================================
function initProximityRadar() {
if (!elements.proximityRadar) return;
// Simple hash of BSSID string → stable angle in radians
function bssidToAngle(bssid) {
let hash = 0;
for (let i = 0; i < bssid.length; i++) {
hash = (hash * 31 + bssid.charCodeAt(i)) & 0xffffffff;
}
return (hash >>> 0) / 0xffffffff * 2 * Math.PI;
}
// Initialize radar component
if (typeof ProximityRadar !== 'undefined') {
ProximityRadar.init('wifiProximityRadar', {
mode: 'wifi',
size: 280,
onDeviceClick: (bssid) => selectNetwork(bssid),
function renderRadar(networksList) {
const dotsGroup = document.getElementById('wifiRadarDots');
if (!dotsGroup) return;
const dots = [];
const zoneCounts = { immediate: 0, near: 0, far: 0 };
networksList.forEach(network => {
const rssi = network.rssi_current ?? -100;
const strength = Math.max(0, Math.min(1, (rssi + 100) / 80));
const dotR = 5 + (1 - strength) * 90; // stronger = closer to centre
const angle = bssidToAngle(network.bssid);
const cx = 105 + dotR * Math.cos(angle);
const cy = 105 + dotR * Math.sin(angle);
// Zone counts
if (dotR < 35) zoneCounts.immediate++;
else if (dotR < 70) zoneCounts.near++;
else zoneCounts.far++;
// Visual radius by zone
const vr = dotR < 35 ? 6 : dotR < 70 ? 4.5 : 3;
// Colour by security
const sec = (network.security || '').toLowerCase();
const colour = sec === 'open' || sec === '' ? '#e25d5d'
: sec.includes('wpa') ? '#38c180'
: sec.includes('wep') ? '#d6a85e'
: '#484f58';
dots.push(`
<circle cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" r="${vr * 1.5}"
fill="${colour}" opacity="0.12"/>
<circle cx="${cx.toFixed(1)}" cy="${cy.toFixed(1)}" r="${vr}"
fill="${colour}" opacity="0.9" filter="url(#wifi-glow-sm)"/>
`);
});
}
}
function updateProximityRadar() {
if (typeof ProximityRadar === 'undefined') return;
dotsGroup.innerHTML = dots.join('');
// Convert networks to radar-compatible format
const devices = Array.from(networks.values()).map(n => ({
device_key: n.bssid,
device_id: n.bssid,
name: n.essid || '[Hidden]',
rssi_current: n.rssi_current,
rssi_ema: n.rssi_ema,
proximity_band: n.proximity_band,
estimated_distance_m: n.estimated_distance_m,
is_new: n.is_new,
heuristic_flags: n.heuristic_flags || [],
}));
ProximityRadar.updateDevices(devices);
if (elements.zoneImmediate) elements.zoneImmediate.textContent = zoneCounts.immediate;
if (elements.zoneNear) elements.zoneNear.textContent = zoneCounts.near;
if (elements.zoneFar) elements.zoneFar.textContent = zoneCounts.far;
}
// ==========================================================================

View File

@@ -876,7 +876,50 @@
<!-- CENTER: Proximity Radar -->
<div class="wifi-radar-panel">
<h5>Proximity Radar</h5>
<div id="wifiProximityRadar" class="wifi-radar-container"></div>
<div id="wifiProximityRadar" class="wifi-radar-container">
<svg width="100%" viewBox="0 0 210 210" id="wifiRadarSvg">
<defs>
<clipPath id="wifi-radar-clip">
<circle cx="105" cy="105" r="100"/>
</clipPath>
<filter id="wifi-glow-sm">
<feGaussianBlur stdDeviation="2.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="wifi-glow-md">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background rings (static) -->
<circle cx="105" cy="105" r="100" fill="none" stroke="#00b4d8" stroke-width="0.5" opacity="0.12"/>
<circle cx="105" cy="105" r="70" fill="none" stroke="#00b4d8" stroke-width="0.5" opacity="0.18"/>
<circle cx="105" cy="105" r="40" fill="none" stroke="#00b4d8" stroke-width="0.5" opacity="0.25"/>
<circle cx="105" cy="105" r="15" fill="none" stroke="#00b4d8" stroke-width="0.5" opacity="0.35"/>
<!-- Crosshairs -->
<line x1="5" y1="105" x2="205" y2="105" stroke="#00b4d8" stroke-width="0.3" opacity="0.1"/>
<line x1="105" y1="5" x2="105" y2="205" stroke="#00b4d8" stroke-width="0.3" opacity="0.1"/>
<!-- Rotating sweep group -->
<g class="wifi-radar-sweep" clip-path="url(#wifi-radar-clip)">
<!-- Primary trailing arc: 60° -->
<path d="M105,105 L105,5 A100,100 0 0,1 191.6,155 Z" fill="#00b4d8" opacity="0.08"/>
<!-- Secondary trailing arc: 90° -->
<path d="M105,105 L105,5 A100,100 0 0,1 205,105 Z" fill="#00b4d8" opacity="0.04"/>
<!-- Sweep line -->
<line x1="105" y1="105" x2="105" y2="5" stroke="#00b4d8" stroke-width="1.5" opacity="0.7"
filter="url(#wifi-glow-sm)"/>
</g>
<!-- Centre dot -->
<circle cx="105" cy="105" r="3" fill="#00b4d8" opacity="0.8"/>
<!-- Network dots (managed by renderRadar()) -->
<g id="wifiRadarDots"></g>
</svg>
</div>
<div class="wifi-zone-summary">
<div class="wifi-zone near">
<span class="wifi-zone-count" id="wifiZoneImmediate">0</span>