()
| 2348 | ${img ? '<h2>Heatmap</h2>' + img : ''} |
| 2349 | ${bands ? '<h2>Bands & channel plan</h2>' + bands : ''} |
| 2350 | ${intf ? '<h2>Interference</h2>' + intf : ''} |
| 2351 | ${issues.length ? '<h2>Security issues (' + issues.length + ')</h2><ul>' + issues.map(a => `<li><b>${_esc(a.ssid) || 'hidden'}</b> (${_esc(a.bssid)}): ${_esc(a.security_findings.join('; '))}</li>`).join('') + '</ul>' : ''} |
| 2352 | <h2>AP inventory</h2> |
| 2353 | <table><thead><tr><th>SSID</th><th>BSSID</th><th>Vendor</th><th>Band</th><th>Ch</th><th>Width</th><th>RSSI</th><th>SNR</th><th>Security</th></tr></thead><tbody>${rows}</tbody></table> |
| 2354 | <div class="foot">Generated by Ragnar WiFi Analyzer. Passive survey — estimates, not survey-grade measurements.</div> |
| 2355 | <script>window.onload=function(){setTimeout(function(){window.print();},250);};<\/script> |
| 2356 | </body></html>`; |
| 2357 | const w = window.open('', '_blank'); |
| 2358 | if (!w) { alert('Allow pop-ups to generate the report.'); return; } |
| 2359 | w.document.open(); w.document.write(html); w.document.close(); |
| 2360 | } |
| 2361 | |
| 2362 | function wifiRender() { |
| 2363 | const d = _wifiState.data; if (!d) return; |
| 2364 | if (!_wifiState.apView) _wifiState.apView = 'aps'; |
| 2365 | if (!_wifiState.sortKey) { _wifiState.sortKey = 'signal'; _wifiState.sortDir = -1; } |
| 2366 | // Band summary chips (+ noise floor + width advice) |
| 2367 | const summ = document.getElementById('wifi-band-summary'); |
| 2368 | const ratingColor = { clear: '#22c55e', moderate: '#eab308', congested: '#ef4444' }; |
| 2369 | let chips = Object.keys(d.spectrum).sort().map(b => { |
| 2370 | const s = d.spectrum[b]; |
| 2371 | const wa = s.width_advice ? ` · <span title="${s.width_advice.reason}">→ ${s.width_advice.mhz}MHz</span>` : ''; |
| 2372 | return `<span class="px-2 py-1 rounded" style="background:${_WIFI_BAND_COLOR[b]}22;border:1px solid ${_WIFI_BAND_COLOR[b]}66"> |
| 2373 | <b style="color:${_WIFI_BAND_COLOR[b]}">${b} GHz</b> · ${s.ap_count} AP · |
| 2374 | <span style="color:${ratingColor[s.rating]}">${s.rating}</span> · best ch ${s.recommend.join(', ')}${wa}</span>`; |
| 2375 | }).join(''); |
| 2376 | if (d.noise_floor != null) chips += `<span class="px-2 py-1 rounded bg-slate-800 border border-slate-700">noise floor ${d.noise_floor} dBm</span>`; |
| 2377 | summ.innerHTML = chips || '<span class="text-gray-500">No APs heard.</span>'; |
| 2378 | wifiRenderChanges(); |
| 2379 | // Legend |
| 2380 | document.getElementById('wifi-legend').innerHTML = |
| 2381 | '<span>Signal:</span>' + |
| 2382 | [['#22c55e', 'strong'], ['#84cc16', 'good'], ['#eab308', 'fair'], ['#f97316', 'weak'], ['#ef4444', 'v.weak']] |
| 2383 | .map(([c, l]) => `<span class="flex items-center gap-1"><span style="width:10px;height:10px;background:${c};border-radius:2px;display:inline-block"></span>${l}</span>`).join('') + |
| 2384 | '<span class="ml-2">◆ DFS/radar</span><span>⚠ security issue</span>'; |
| 2385 | wifiRenderTable(); |
| 2386 | // Interference |
| 2387 | const inf = d.interference; const ip = document.getElementById('wifi-interference'); |
| 2388 | let html = ''; |
| 2389 | if (inf.co_channel.length) html += '<div class="mb-2"><b class="text-amber-400">Co-channel:</b><ul class="mt-1 space-y-0.5">' + |
| 2390 | inf.co_channel.slice(0, 6).map(g => `<li>${g.band}GHz ch${g.channel}: ${g.count} APs sharing (${g.ssids.slice(0, 3).join(', ')})</li>`).join('') + '</ul></div>'; |
| 2391 | if (inf.adjacent_overlap.length) html += `<div><b class="text-orange-400">2.4GHz overlap:</b> ${inf.adjacent_overlap.length} overlapping pair(s) — use only ch 1/6/11.</div>`; |
| 2392 | if (!html) html = '<span class="text-green-400">No significant co-/adjacent-channel interference.</span>'; |
| 2393 | ip.innerHTML = html; |
| 2394 | _wifiDrawSpectrum(); |
| 2395 | _wifiFsRender(); |
| 2396 | if (_wifiHm.mesh) wifiHeatmapPopulateSsids(); else wifiHeatmapPopulateAps(); |
no test coverage detected