(data)
| 4691 | } |
| 4692 | return `<div class="glass rounded-xl p-4 ${border}"><div class="font-semibold text-sm mb-1">${title}</div><div class="text-sm text-gray-300">${body}</div><div class="text-[11px] text-gray-500 mt-1">${x.detail || ''}${body.indexOf('wifiPivotFromDefense') >= 0 ? ' <span class="text-gray-600">· click a MAC to inspect it in the Spectrum Analyzer</span>' : ''}</div></div>`; |
| 4693 | }).join(''); |
| 4694 | } |
| 4695 | // Counts |
| 4696 | const c = d.counts || {}; |
| 4697 | let chips = [['frames', d.frames], ['deauth', c.deauth], ['beacons', c.beacon], ['probe-req', c.probe_req], ['probe-resp', c.probe_resp]] |
| 4698 | .map(([k, v]) => `<span class="px-3 py-1 rounded bg-slate-800 border border-slate-700"><b>${v || 0}</b> <span class="text-gray-400">${k}</span></span>`).join(''); |
| 4699 | // Airspace density vs the flood threshold — lets the user calibrate. |
| 4700 | const a = d.airspace; |
| 4701 | if (a) { |
| 4702 | const near = a.ssids >= a.beacon_ssid_threshold; |
| 4703 | chips += `<span class="px-3 py-1 rounded bg-slate-800 border ${near ? 'border-red-600/60 text-red-300' : 'border-slate-700'}" title="Distinct SSIDs / BSSIDs heard this capture, vs the beacon-flood threshold. Raise the threshold above your normal density.">` |
| 4704 | + `<b>${a.ssids}</b> SSIDs · <b>${a.bssids}</b> BSSIDs <span class="text-gray-500">(flood ≥ ${a.beacon_ssid_threshold})</span></span>`; |
| 4705 | } |
| 4706 | document.getElementById('wifidef-counts').innerHTML = chips; |
| 4707 | // AP table |
| 4708 | document.getElementById('wifidef-ap-count').textContent = `(${(d.aps || []).length})`; |
| 4709 | const body = document.getElementById('wifidef-ap-tbody'); |
| 4710 | const trusted = null; |
| 4711 | if (!d.aps || !d.aps.length) body.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-gray-500">No beacons captured (wrong channel? try “hop”).</td></tr>'; |
| 4712 | else body.innerHTML = d.aps.map(a => `<tr class="border-b border-slate-800/50 cursor-pointer hover:bg-slate-800/40" title="Open in Spectrum Analyzer" onclick="wifiPivotFromDefense('${a.bssid}','${encodeURIComponent(a.ssid || '').replace(/'/g, '%27')}')"> |
| 4713 | <td class="py-1 pr-2">${a.ssid || '<span class=\'text-gray-500 italic\'>hidden</span>'}</td> |
| 4714 | <td class="py-1 pr-2 font-mono text-[11px]">${a.bssid}</td> |
| 4715 | <td class="py-1 pr-2">${a.channel == null ? '—' : a.channel}</td> |
| 4716 | <td class="py-1 pr-2">${a.rssi == null ? '—' : a.rssi + ' dBm'}</td> |
| 4717 | <td class="py-1 pr-2">${a.beacons}</td></tr>`).join(''); |
| 4718 | } |
| 4719 | |
| 4720 | // ============================================================================ |
| 4721 | // Network diagnostics (Diagnostics / Switch & L2 / Interfaces sub-tabs) |
| 4722 | // Backend: /api/net/* (see network_diagnostics.py) |
| 4723 | // ============================================================================ |
| 4724 | |
| 4725 | // Last-fetched payloads for each net panel, so "Export CSV" has data to dump. |
| 4726 | let _ndLastLldp = [], _ndLastArp = null, _ndLastIfaces = [], |
| 4727 | _ndLastMtr = null, _ndLastIdentity = null, _ndLastIsp = [], |
| 4728 | _ndLastMacWatch = null; |
| 4729 | |
| 4730 | // Build a CSV from a header row + rows (arrays of cells) and download it. |
| 4731 | function _ndDownloadCsv(nameBase, header, rows) { |
| 4732 | if (!rows || !rows.length) { |
| 4733 | addConsoleMessage('Nothing to export yet — run it first', 'warning'); |
| 4734 | return; |
| 4735 | } |
| 4736 | const q = (c) => `"${String(c == null ? '' : c).replace(/"/g, '""')}"`; |
| 4737 | const csv = [header].concat(rows).map(r => r.map(q).join(',')).join('\n'); |
| 4738 | const blob = new Blob([csv], { type: 'text/csv' }); |
| 4739 | const url = URL.createObjectURL(blob); |
| 4740 | const a = document.createElement('a'); |
| 4741 | a.href = url; |
| 4742 | a.download = `ragnar_${nameBase}_${new Date().toISOString().slice(0, 10)}.csv`; |
| 4743 | a.click(); |
| 4744 | URL.revokeObjectURL(url); |
| 4745 | } |
| 4746 | |
| 4747 | function exportLldpCsv() { |
| 4748 | _ndDownloadCsv('switch_neighbors', |
| 4749 | ['Local IF', 'Protocol', 'Switch', 'Switch descr', 'Port', 'VLAN ID', 'VLAN name', 'PoE', 'Mgmt IP'], |
| 4750 | (_ndLastLldp || []).map(n => [n.local_interface, n.protocol, n.switch_name, |
no test coverage detected