()
| 2420 | const hov = _wifiState.hoverBssid === a.bssid; |
| 2421 | // Pixel box → BSSID, for hover/click. Recorded in draw order, so a |
| 2422 | // reverse scan finds whatever is visually on top. |
| 2423 | canvas._wifiHits.push({ bssid: a.bssid, x0: Math.min(xL, xC - 2), x1: Math.max(xR, xC + 2), y0: y - 12, y1: H - padB }); |
| 2424 | if (_wifiState.view === 'bar') { |
| 2425 | ctx.fillStyle = col + (sel ? 'ff' : hov ? 'ee' : 'cc'); |
| 2426 | ctx.fillRect(xL, y, Math.max(3, xR - xL), (H - padB) - y); |
| 2427 | ctx.strokeStyle = sel || hov ? '#fff' : col; ctx.lineWidth = sel ? 2 : hov ? 1.5 : 1; |
| 2428 | ctx.strokeRect(xL, y, Math.max(3, xR - xL), (H - padB) - y); |
| 2429 | } else { |
| 2430 | // Dome: bell curve centred on channel, spanning its width |
| 2431 | ctx.beginPath(); ctx.moveTo(xL, H - padB); |
| 2432 | const steps = 28; |
| 2433 | for (let s = 0; s <= steps; s++) { |
| 2434 | const t = s / steps; const x = xL + (xR - xL) * t; |
| 2435 | const bell = Math.cos((t - 0.5) * Math.PI); // 0..1..0 |
| 2436 | const yy = (H - padB) - bell * ((H - padB) - y); |
| 2437 | ctx.lineTo(x, yy); |
| 2438 | } |
| 2439 | ctx.lineTo(xR, H - padB); ctx.closePath(); |
| 2440 | const grad = ctx.createLinearGradient(0, y, 0, H - padB); |
| 2441 | grad.addColorStop(0, col + (sel ? 'cc' : hov ? '99' : '66')); grad.addColorStop(1, col + '08'); |
| 2442 | ctx.fillStyle = grad; ctx.fill(); |
| 2443 | ctx.strokeStyle = sel || hov ? '#fff' : col; ctx.lineWidth = sel ? 2.5 : hov ? 2 : 1.5; ctx.stroke(); |
| 2444 | } |
| 2445 | // WIDS-flagged AP: red dashed locator line + outline so it stands out |
| 2446 | // even in a crowded band (highlight persists until dismissed). |
| 2447 | if (_wifiState.flagged && _wifiState.flagged.bssid === a.bssid) { |
| 2448 | ctx.save(); |
| 2449 | ctx.strokeStyle = '#ef4444'; ctx.lineWidth = 1; ctx.setLineDash([5, 4]); |
| 2450 | ctx.beginPath(); ctx.moveTo(xC, padT); ctx.lineTo(xC, H - padB); ctx.stroke(); |
| 2451 | ctx.lineWidth = 2; ctx.setLineDash([4, 3]); |
| 2452 | ctx.strokeRect(xL - 2, y - 2, Math.max(3, xR - xL) + 4, (H - padB) - y + 2); |
| 2453 | ctx.restore(); |
| 2454 | ctx.fillStyle = '#f87171'; ctx.font = `bold ${10 * K}px sans-serif`; ctx.textAlign = 'center'; |
| 2455 | ctx.fillText('⚠ WIDS', xC, y - 16 * K); |
| 2456 | } |
| 2457 | // Label the SSID at the peak — with the level and channel alongside it |
| 2458 | // on the big surface, where there's room to read them. |
| 2459 | const label = a.ssid || 'hidden'; |
| 2460 | const cap = o.big ? 22 : 14; |
| 2461 | ctx.fillStyle = sel || hov ? '#fff' : '#cbd5e1'; |
| 2462 | ctx.font = (sel || hov ? 'bold ' : '') + (10 * K) + 'px sans-serif'; ctx.textAlign = 'center'; |
| 2463 | ctx.fillText(label.length > cap ? label.slice(0, cap - 1) + '…' : label, xC, y - 4 * K); |
| 2464 | if (o.big) { |
| 2465 | ctx.fillStyle = col; ctx.font = '10px sans-serif'; |
| 2466 | ctx.fillText(`${a.signal} dBm · ch${a.channel}/${a.width}`, xC, y - 17); |
| 2467 | } |
| 2468 | }); |
| 2469 | canvas._wifiGeom = { bands, ranges, padL, padR, padT, padB, W, H, K, chFor, dbmFor }; |
| 2470 | // Bluetooth / BLE overlay on the 2.4 GHz segment (device-activity estimate) |
| 2471 | if (_wifiState.btOn && _wifiState.bt && ranges['2.4']) { |
| 2472 | _wifiDrawBtOverlay(ctx, ranges['2.4'], xFor, { padT, padB, H, plotH, yFor }); |
| 2473 | } |
| 2474 | // Zigbee / 802.15.4 overlay on the 2.4 GHz segment (device-activity estimate) |
| 2475 | if (_wifiState.zbOn && _wifiState.zb && ranges['2.4']) { |
| 2476 | _wifiDrawZbOverlay(ctx, ranges['2.4'], xFor, { padT, padB, H, plotH, yFor }); |
| 2477 | } |
| 2478 | } |
| 2479 |
no test coverage detected