()
| 7822 | sel.appendChild(o); |
| 7823 | }); |
| 7824 | sel.dataset.filled = '1'; |
| 7825 | }).catch(() => {}); |
| 7826 | } |
| 7827 | async function runDtpWatch() { |
| 7828 | const out = document.getElementById('dtp-results'); |
| 7829 | if (!out) return; |
| 7830 | const btn = (typeof event !== 'undefined' && event && event.target) ? event.target : null; |
| 7831 | const ifaceSel = document.getElementById('dtp-iface'); |
| 7832 | const iface = ifaceSel && ifaceSel.value ? ifaceSel.value : ''; |
| 7833 | const secsEl = document.getElementById('dtp-secs'); |
| 7834 | const secs = secsEl && secsEl.value ? secsEl.value : '30'; |
| 7835 | _ndBusy(btn, true, 'Listening…'); |
| 7836 | out.classList.remove('hidden'); |
| 7837 | out.innerHTML = '<p class="text-sm text-gray-400">Passively capturing DTP frames (hellos are ~30s apart)…</p>'; |
| 7838 | try { |
| 7839 | _dtpFillIfaces(); |
| 7840 | const qs = '?seconds=' + encodeURIComponent(secs) + (iface ? '&interface=' + encodeURIComponent(iface) : ''); |
| 7841 | const d = await fetchAPI('/api/net/dtp-watch' + qs); |
| 7842 | if (!d || d.success === false) { |
| 7843 | const msg = (d && d.error) || 'failed'; |
| 7844 | let extra = ''; |
| 7845 | if (d && d.missing_tool) extra = ' <button onclick="installNetTool(\'tcpdump\', this, runDtpWatch)" class="ml-2 underline text-cyan-400">Install tcpdump</button>'; |
| 7846 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml(msg) + extra + '</p>'; |
| 7847 | return; |
| 7848 | } |
| 7849 | const [cls, label] = _DTP_VERDICT_STYLE[d.verdict] || _DTP_VERDICT_STYLE.unknown; |
| 7850 | let html = `<div class="mb-2 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 7851 | html += `<p class="text-xs text-gray-500 mb-2">Interface: ${escapeHtml(d.interface || '—')} · ${d.seconds}s · ${d.packet_count} DTP frame(s), ${d.speaker_count} speaker(s)${d.learned ? ' · <span class="text-gray-400">baseline learned now</span>' : ''}</p>`; |
| 7852 | if ((d.speakers || []).length) { |
| 7853 | html += '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 7854 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">Speaker MAC</th><th class="px-2 py-1">Status</th><th class="px-2 py-1">Trunk?</th><th class="px-2 py-1">Status</th></tr>' + |
| 7855 | '</thead><tbody>' + |
| 7856 | d.speakers.map(sp => { |
| 7857 | const badge = sp.baseline ? '<span class="text-green-400">✓ known</span>' : '<span class="text-amber-300">? new</span>'; |
| 7858 | const forming = sp.forming ? '<span class="text-red-300">forming</span>' : '<span class="text-gray-500">no</span>'; |
| 7859 | return `<tr class="border-t border-slate-800"> |
| 7860 | <td class="px-2 py-1 font-mono ${sp.baseline ? '' : 'text-amber-300'}">${escapeHtml(sp.src)}</td> |
| 7861 | <td class="px-2 py-1 font-mono">${escapeHtml(sp.status)}</td> |
| 7862 | <td class="px-2 py-1">${forming}</td> |
| 7863 | <td class="px-2 py-1">${badge}</td> |
| 7864 | </tr>`; |
| 7865 | }).join('') + |
| 7866 | '</tbody></table>'; |
| 7867 | } |
| 7868 | if (d.reasons && d.reasons.length) { |
| 7869 | html += '<ul class="text-xs text-gray-400 mt-2 list-disc pl-5">' + |
| 7870 | d.reasons.map(r => '<li>' + escapeHtml(r) + '</li>').join('') + '</ul>'; |
| 7871 | } |
| 7872 | (d.advisories || []).forEach(a => { |
| 7873 | html += `<div class="mt-2 px-3 py-2 rounded border text-xs text-gray-400 border-slate-700 bg-slate-900/40">${escapeHtml(a)}</div>`; |
| 7874 | }); |
| 7875 | out.innerHTML = html; |
| 7876 | } catch (e) { |
| 7877 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 7878 | } finally { |
| 7879 | _ndBusy(btn, false); |
no test coverage detected