(state)
| 8620 | h += '<table class="w-full text-xs"><tr class="text-gray-500 text-left"><th class="pr-3">RTT min</th><th class="pr-3">θ offset</th><th class="pr-3">asymmetry</th><th>clocks</th></tr>'; |
| 8621 | h += '<tr><td class="pr-3 font-mono">' + (s.rtt_min_ms != null ? s.rtt_min_ms.toFixed(2) : '?') + ' ms</td>'; |
| 8622 | h += '<td class="pr-3 font-mono">' + (s.theta_ms != null ? s.theta_ms.toFixed(2) : '?') + ' ms</td>'; |
| 8623 | h += '<td class="pr-3 font-mono">' + (s.last.asymmetry_ms != null ? s.last.asymmetry_ms.toFixed(2) : '?') + ' ms</td>'; |
| 8624 | h += '<td>' + (s.clock_synced ? '<span class="text-green-400">synced (absolute)</span>' : '<span class="text-gray-400">unsynced (change-sensitive)</span>') + '</td></tr></table>'; |
| 8625 | } |
| 8626 | if (d.events && d.events.length) { |
| 8627 | h += '<div class="mt-2 text-xs font-semibold text-amber-300">' + d.events.length + ' asymmetry event(s)</div>'; |
| 8628 | for (const ev of d.events) { |
| 8629 | h += '<div class="text-xs mt-1 border-l-2 border-amber-400 pl-2">'; |
| 8630 | h += 'Δ ' + (ev.asymmetry_ms != null ? ev.asymmetry_ms.toFixed(2) : '?') + ' ms · dir ' + escapeHtml(ev.direction || '?'); |
| 8631 | if (ev.attribution) h += ' — <span class="text-cyan-400">' + escapeHtml(ev.attribution) + '</span>'; |
| 8632 | if (ev.control_plane && ev.control_plane.covering_prefix) { |
| 8633 | h += '<div class="text-gray-400 pl-1">prefix ' + escapeHtml(ev.control_plane.covering_prefix) + ' origin AS' + escapeHtml(String(ev.control_plane.origin_as ?? '?')) + (ev.control_plane.flapping ? ' <span class="text-red-300">flapping</span>' : '') + '</div>'; |
| 8634 | } |
| 8635 | h += '</div>'; |
| 8636 | } |
| 8637 | } |
| 8638 | out.innerHTML = h; |
| 8639 | } catch (e) { |
| 8640 | out.innerHTML = '<span class="text-red-300">' + escapeHtml(e.message) + '</span>'; |
| 8641 | } |
| 8642 | } |
| 8643 | |
| 8644 | // ---- Routing-scanner detector self-test (+ optional Scapy) ------------------ |
| 8645 | function _scapyLegLabel(sc) { |
| 8646 | if (!sc) return ''; |
| 8647 | if (sc.ran) return sc.pass ? '<span class="text-green-400">e2e ✓</span>' : '<span class="text-red-300">e2e ✗</span>'; |
| 8648 | return '<span class="text-gray-500">e2e skipped</span>'; |
| 8649 | } |
| 8650 | async function runRoutingSelftest() { |
| 8651 | const out = document.getElementById('routing-selftest-results'); |
| 8652 | if (!out) return; |
| 8653 | const btn = (typeof event !== 'undefined' && event && event.target) ? event.target : null; |
| 8654 | _ndBusy(btn, true, 'Running…'); |
| 8655 | out.classList.remove('hidden'); |
| 8656 | out.innerHTML = '<p class="text-sm text-gray-400">Running IGMP / OSPF / BGP detector self-tests…</p>'; |
| 8657 | try { |
| 8658 | const d = await fetchAPI('/api/net/routing-selftest'); |
| 8659 | if (!d || d.success === undefined) { |
| 8660 | out.innerHTML = '<p class="text-sm text-red-400">Self-test failed to run.</p>'; |
| 8661 | return; |
| 8662 | } |
| 8663 | // Scapy status + install button |
| 8664 | const status = document.getElementById('scapy-status'); |
| 8665 | const instBtn = document.getElementById('scapy-install-btn'); |
| 8666 | if (status) status.innerHTML = d.scapy_available |
| 8667 | ? 'Scapy: <span class="text-green-400">installed</span> — end-to-end leg active' |
| 8668 | : 'Scapy: <span class="text-amber-300">not installed</span> — end-to-end leg skipped'; |
| 8669 | if (instBtn) instBtn.classList.toggle('hidden', !!d.scapy_available); |
| 8670 | |
| 8671 | const names = { igmp: 'IGMP Watch', ipv6: 'IPv6 First-Hop Watch', ndp: 'NDP Watch (IPv6 neighbor spoofing)', raguard: 'IPv6 RA Guard', ntp: 'NTP Watch', icmp: 'ICMP Watch', snmp: 'SNMP Watch', cert: 'Cert Watch', tls: 'TLS Watch (passive JA4/QUIC)', stp: 'STP/BPDU Watch (spanning tree)', smb: 'SMB Watch (SMBv1 + poisoning + Kerberos downgrade)', relay: 'Relay/Coercion Watch (NTLM relay)', ldap: 'LDAP Watch (Active Directory)', dtp: 'DTP Watch (VLAN hopping)', cdp: 'CDP Watch (Cisco Discovery leak/flood)', vtp: 'VTP Watch (VTP bomb / VLAN-DB wipe)', eigrp: 'EIGRP Watch (Cisco IGP)', isis: 'IS-IS Watch (IGP)', fhrp: 'FHRP Watch (HSRP/VRRP/GLBP/CARP)', ospf: 'OSPF Scanner', bgp: 'BGP Path Watch', |
| 8672 | bgp_speaker: 'BGP Speaker (codec/FSM/RIB)', path_asymmetry: 'Path Asymmetry (OWD)' }; |
| 8673 | const overall = d.success |
| 8674 | ? '<div class="mb-2 px-3 py-2 rounded border bg-green-950/40 border-green-900 text-green-400 text-sm">✓ All detector self-tests passed' + (d.scapy_available ? ' (including Scapy end-to-end)' : ' — install Scapy for the end-to-end leg') + '</div>' |
| 8675 | : '<div class="mb-2 px-3 py-2 rounded border bg-red-950/60 border-red-800 text-red-300 text-sm">🛑 A detector self-test FAILED — see below</div>'; |
| 8676 | let html = overall + |
| 8677 | '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 8678 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">Scanner</th><th class="px-2 py-1">Scenarios</th><th class="px-2 py-1">End-to-end</th><th class="px-2 py-1">Result</th></tr>' + |
| 8679 | '</thead><tbody>'; |
no test coverage detected