()
| 1893 | tb.innerHTML = d.devices.map(v => { |
| 1894 | const k = _BT_KIND[v.kind] || { c: '#94a3b8', t: v.kind || '?' }; |
| 1895 | const name = v.name || '<span class="text-gray-600">(no name)</span>'; |
| 1896 | const cls = v.major_class ? v.major_class + (v.services && v.services.length ? ` · ${v.services.join(', ')}` : '') : '—'; |
| 1897 | const rssi = (v.rssi == null) ? '—' : `${v.rssi}`; |
| 1898 | const rcol = (v.rssi == null) ? '#64748b' : _wifiSignalColor(v.rssi); |
| 1899 | const sel = _wifiState.btSelected === v.mac; |
| 1900 | return `<tr class="border-b border-slate-800/60 cursor-pointer hover:bg-slate-800/40${sel ? ' bg-sky-500/15' : ''}" onclick="wifiBtSelect('${v.mac}')" title="Click to highlight this device on the spectrum"> |
| 1901 | <td class="py-1 pr-3">${sel ? '<span class="text-sky-400">▸</span> ' : ''}${name}<div class="text-[10px] text-gray-600 font-mono">${v.mac}</div></td> |
| 1902 | <td class="py-1 pr-3"><span style="color:${k.c}">${k.t}</span>${v.randomized ? ' <span class="text-[9px] text-gray-500">rnd</span>' : ''}</td> |
| 1903 | <td class="py-1 pr-3">${v.vendor || '—'}</td> |
| 1904 | <td class="py-1 pr-3">${cls}</td> |
| 1905 | <td class="py-1 pr-3 text-right font-mono" style="color:${rcol}">${rssi}</td> |
| 1906 | </tr>`; |
| 1907 | }).join(''); |
| 1908 | } |
| 1909 | |
| 1910 | // Click a BT device row → highlight it on the 2.4 GHz spectrum (toggle off if |
| 1911 | // the same row is clicked again). Mirrors the Wi-Fi AP select behaviour. |
| 1912 | function wifiBtSelect(mac) { |
| 1913 | _wifiState.btSelected = (_wifiState.btSelected === mac) ? null : mac; |
| 1914 | _wifiFsRenderSide(); |
| 1915 | _wifiBtRender(); |
| 1916 | if (_wifiState.data) _wifiDrawSpectrum(); |
| 1917 | } |
| 1918 | |
| 1919 | // ---- Zigbee / 802.15.4 overlay (via an on-demand Huginn sniff) ------------- |
| 1920 | // The Zigbee toggle stays greyed until a HuginnESP companion is on USB. |
| 1921 | function _wifiZbCheck() { |
| 1922 | fetch('/api/net/zigbee/status').then(r => r.json()).then(st => { |
| 1923 | const ok = !!(st && st.available); |
| 1924 | _wifiState.zbAvailable = ok; |
| 1925 | const chk = document.getElementById('wifi-zb'), lab = document.getElementById('wifi-zb-label'); |
| 1926 | if (chk) chk.disabled = !ok; |
| 1927 | if (lab) { |
| 1928 | lab.classList.toggle('opacity-40', !ok); |
| 1929 | lab.classList.toggle('cursor-not-allowed', !ok); |
| 1930 | lab.classList.toggle('cursor-pointer', ok); |
| 1931 | lab.classList.toggle('text-gray-400', ok); |
| 1932 | lab.title = ok ? 'Sniff Zigbee / 802.15.4 with the connected HuginnESP' |
| 1933 | : (st && st.error ? st.error : 'Connect a HuginnESP (ESP32-C5) companion to sniff Zigbee'); |
| 1934 | } |
| 1935 | _wifiFsSyncGates(); |
| 1936 | }).catch(() => {}); |
| 1937 | } |
| 1938 | |
| 1939 | function wifiZbScan() { |
| 1940 | if (_wifiState.zbBusy) return; |
| 1941 | _wifiState.zbBusy = true; |
| 1942 | const st = document.getElementById('wifi-zb-status'); |
| 1943 | if (st) st.textContent = 'Sniffing 802.15.4 (ch 11–26)…'; |
| 1944 | fetch('/api/net/zigbee/scan?duration=8').then(r => r.json()).then(d => { |
| 1945 | _wifiState.zbBusy = false; |
| 1946 | if (d.error) { if (st) st.textContent = '⚠ ' + d.error; _wifiState.zb = null; if (_wifiState.data) _wifiDrawSpectrum(); return; } |
| 1947 | _wifiState.zb = d; |
| 1948 | if (_wifiState.zbSelected && !(d.devices || []).some(v => (v.addr || v.short_addr) === _wifiState.zbSelected)) |
| 1949 | _wifiState.zbSelected = null; |
| 1950 | const i = d.interference || {}; |
| 1951 | if (st) st.textContent = `${d.device_count} device(s) · ${i.channel_count || 0} ch` + (d.warning ? ' ⚠' : ''); |
| 1952 | _wifiZbRender(); |
nothing calls this directly
no test coverage detected