()
| 7350 | o.value = i.name; |
| 7351 | const tag = i.type === 'wifi' ? ' (WiFi)' : i.type === 'ethernet' ? ' (LAN)' : (i.type ? ' (' + i.type + ')' : ''); |
| 7352 | o.textContent = i.name + tag; |
| 7353 | sel.appendChild(o); |
| 7354 | }); |
| 7355 | sel.dataset.filled = '1'; |
| 7356 | }).catch(() => {}); |
| 7357 | } |
| 7358 | async function runNdpWatch() { |
| 7359 | const out = document.getElementById('ndp-results'); |
| 7360 | if (!out) return; |
| 7361 | const btn = (typeof event !== 'undefined' && event && event.target) ? event.target : null; |
| 7362 | const ifaceSel = document.getElementById('ndp-iface'); |
| 7363 | const iface = ifaceSel && ifaceSel.value ? ifaceSel.value : ''; |
| 7364 | const secsEl = document.getElementById('ndp-secs'); |
| 7365 | const secs = secsEl && secsEl.value ? secsEl.value : '12'; |
| 7366 | _ndBusy(btn, true, 'Listening…'); |
| 7367 | out.classList.remove('hidden'); |
| 7368 | out.innerHTML = '<p class="text-sm text-gray-400">Passively capturing Neighbor Solicitation / Advertisement on the segment…</p>'; |
| 7369 | try { |
| 7370 | _ndpFillIfaces(); |
| 7371 | const qs = '?seconds=' + encodeURIComponent(secs) + (iface ? '&interface=' + encodeURIComponent(iface) : ''); |
| 7372 | const d = await fetchAPI('/api/net/ndp-watch' + qs); |
| 7373 | if (!d || d.success === false) { |
| 7374 | const msg = (d && d.error) || 'failed'; |
| 7375 | let extra = ''; |
| 7376 | if (d && d.missing_tool) extra = ' <button onclick="installNetTool(\'tcpdump\', this, runNdpWatch)" class="ml-2 underline text-cyan-400">Install tcpdump</button>'; |
| 7377 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml(msg) + extra + '</p>'; |
| 7378 | return; |
| 7379 | } |
| 7380 | const [cls, label] = _NDP_VERDICT_STYLE[d.verdict] || _NDP_VERDICT_STYLE.unknown; |
| 7381 | let html = `<div class="mb-2 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 7382 | html += `<p class="text-xs text-gray-500 mb-2">Interface: ${escapeHtml(d.interface || '—')} · ${d.na_count} NA (${d.rate}/s) · ${d.ns_count} NS${d.dad_count ? ' · ' + d.dad_count + ' DAD' : ''}${d.learned ? ' · <span class="text-gray-400">baseline learned now</span>' : ''}</p>`; |
| 7383 | const hosts = d.hosts || []; |
| 7384 | if (hosts.length) { |
| 7385 | html += '<p class="text-xs uppercase text-gray-400 mt-2 mb-1">Advertised neighbours (' + hosts.length + ')</p>' + |
| 7386 | '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 7387 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">IPv6 target</th><th class="px-2 py-1">Claimed by MAC(s)</th><th class="px-2 py-1">Trusted MAC</th><th class="px-2 py-1">State</th></tr>' + |
| 7388 | '</thead><tbody>' + |
| 7389 | hosts.map(h => { |
| 7390 | const bad = h.conflict || (h.baseline && h.macs.length === 1 && h.macs[0] !== h.baseline); |
| 7391 | return `<tr class="border-t border-slate-800"> |
| 7392 | <td class="px-2 py-1 font-mono ${bad ? 'text-red-300' : 'text-gray-200'}">${escapeHtml(h.ip)}</td> |
| 7393 | <td class="px-2 py-1 font-mono ${bad ? 'text-red-300' : 'text-gray-400'}">${escapeHtml((h.macs || []).join(', '))}</td> |
| 7394 | <td class="px-2 py-1 font-mono text-gray-500">${escapeHtml(h.baseline || '—')}</td> |
| 7395 | <td class="px-2 py-1">${h.conflict ? '<span class="text-red-300">CONFLICT</span>' : bad ? '<span class="text-red-300">CHANGED</span>' : '<span class="text-gray-500">ok</span>'}</td> |
| 7396 | </tr>`; |
| 7397 | }).join('') + '</tbody></table>'; |
| 7398 | } |
| 7399 | if (d.reasons && d.reasons.length) { |
nothing calls this directly
no test coverage detected