()
| 2036 | document.getElementById('wifi-view-aps').classList.toggle('text-white', v === 'aps'); |
| 2037 | document.getElementById('wifi-view-nets').classList.toggle('bg-Ragnar-600', v === 'nets'); |
| 2038 | document.getElementById('wifi-view-nets').classList.toggle('text-white', v === 'nets'); |
| 2039 | document.getElementById('wifi-ap-table').classList.toggle('hidden', v !== 'aps'); |
| 2040 | document.getElementById('wifi-nets-view').classList.toggle('hidden', v !== 'nets'); |
| 2041 | wifiRenderTable(); |
| 2042 | } |
| 2043 | |
| 2044 | function wifiSort(key) { |
| 2045 | if (_wifiState.sortKey === key) _wifiState.sortDir *= -1; |
| 2046 | else { _wifiState.sortKey = key; _wifiState.sortDir = (key === 'ssid' || key === 'vendor') ? 1 : -1; } |
| 2047 | wifiRenderTable(); |
| 2048 | _wifiFsRenderList(); |
| 2049 | } |
| 2050 | |
| 2051 | // Filter + sort the survey the same way for every list that shows it (the |
| 2052 | // in-page table and the full-screen console). |
| 2053 | function _wifiFilterAps(q, std, issuesOnly) { |
| 2054 | const d = _wifiState.data; if (!d) return []; |
| 2055 | q = (q || '').toLowerCase(); |
| 2056 | const aps = d.aps.filter(a => |
| 2057 | (!q || (a.ssid || '').toLowerCase().includes(q) || (a.vendor || '').toLowerCase().includes(q) || a.bssid.includes(q)) |
| 2058 | && (!issuesOnly || (a.security_findings && a.security_findings.length)) |
| 2059 | && (!std || a.standard === std)); |
| 2060 | const k = _wifiState.sortKey || 'signal', dir = _wifiState.sortDir || -1; |
| 2061 | return aps.slice().sort((x, y) => { |
| 2062 | let a = x[k], b = y[k]; |
| 2063 | if (a == null) a = (typeof b === 'number') ? -9999 : ''; |
| 2064 | if (b == null) b = (typeof a === 'number') ? -9999 : ''; |
| 2065 | return (a < b ? -1 : a > b ? 1 : 0) * dir; |
| 2066 | }); |
| 2067 | } |
| 2068 | |
| 2069 | function wifiRenderTable() { |
| 2070 | const d = _wifiState.data; if (!d) return; |
| 2071 | if (_wifiState.apView === 'nets') return wifiRenderNets(); |
| 2072 | const aps = _wifiFilterAps( |
| 2073 | document.getElementById('wifi-ap-search').value, |
| 2074 | (document.getElementById('wifi-ap-std') || {}).value || '', |
| 2075 | document.getElementById('wifi-ap-issues').checked); |
| 2076 | const tb = document.getElementById('wifi-ap-tbody'); |
| 2077 | document.getElementById('wifi-ap-count').textContent = `(${aps.length}/${d.ap_count})`; |
| 2078 | if (!aps.length) { tb.innerHTML = '<tr><td colspan="9" class="py-4 text-center text-gray-500">No APs match.</td></tr>'; return; } |
| 2079 | tb.innerHTML = aps.map(a => { |
| 2080 | const isSel = _wifiState.selected === a.bssid; |
| 2081 | const bar = a.signal == null ? 0 : Math.max(4, Math.min(100, (a.signal + 100) / 70 * 100)); |
| 2082 | const issue = a.security_findings && a.security_findings.length; |
| 2083 | const flagged = _wifiState.flagged && _wifiState.flagged.bssid === a.bssid; |
| 2084 | // Selected row: inline Ragnar accent (custom-colour opacity classes aren't |
| 2085 | // in the prebuilt tailwind.css). Selection wins the left bar; WIDS red beats amber. |
| 2086 | const rowCls = isSel ? '' : 'hover:bg-slate-800/40' + (flagged ? ' border-l-2 border-l-red-500' : issue ? ' border-l-2 border-l-amber-500' : ''); |
| 2087 | const rowStyle = isSel ? ' style="background-color:rgba(2,132,199,0.25);border-left:3px solid rgb(56,189,248)"' : ''; |
| 2088 | const snr = a.snr != null ? `<span class="text-[10px] text-gray-500"> ${a.snr}dB</span>` : ''; |
| 2089 | const rate = (a.nss ? a.nss + 'ss' : '') + (a.max_phy_mbps ? ' ' + (a.max_phy_mbps >= 1000 ? (a.max_phy_mbps / 1000).toFixed(1) + 'G' : a.max_phy_mbps + 'M') : ''); |
| 2090 | const marker = isSel ? '<span style="color:rgb(56,189,248)" class="mr-0.5" title="selected — modelled in Signal Radius">►</span>' : ''; |
| 2091 | return `<tr class="border-b border-slate-800/50 cursor-pointer ${rowCls}"${rowStyle} onclick="wifiSelectAp('${a.bssid}')"> |
| 2092 | <td class="py-1 pr-2 whitespace-nowrap">${marker}${issue ? '<span title="' + a.security_findings.join('; ') + '" class="text-amber-400">⚠</span> ' : ''}${(a.ssid || '<span class=\'text-gray-500 italic\'>hidden</span>')}${_wifiGenBadge(a.standard)}${a.is_new ? ' <span class="text-[9px] px-1 rounded bg-emerald-600/30 text-emerald-300 align-middle" title="first seen this session">NEW</span>' : ''}${flagged ? ' <span class="text-[9px] px-1 rounded bg-red-600/30 text-red-300 align-middle" title="flagged by WiFi Defense">⚠ WIDS</span>' : ''}${a.dfs ? ' <span title="DFS/radar" style="color:#a78bfa">◆</span>' : ''}<div class="text-[10px] text-gray-600 font-mono">${a.bssid}${a.seen_count > 1 ? ' <span class="text-gray-700">·seen ' + a.seen_count + '×</span>' : ''}</div></td> |
| 2093 | <td class="py-1 pr-2 text-xs text-gray-400 whitespace-nowrap">${a.vendor || '—'}</td> |
| 2094 | <td class="py-1 pr-2"><span style="color:${_WIFI_BAND_COLOR[a.band]}">${a.band}</span></td> |
| 2095 | <td class="py-1 pr-2">${a.channel}</td> |
no test coverage detected