(interfaceName, enable, button)
| 8726 | const d = await fetchAPI('/api/net/mac-watch?scan=' + (scan ? '1' : '0') + (iface ? '&interface=' + encodeURIComponent(iface) : '')); |
| 8727 | if (!d || d.success === false) { |
| 8728 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml((d && d.error) || 'failed') + '</p>'; |
| 8729 | return; |
| 8730 | } |
| 8731 | _ndLastMacWatch = d; |
| 8732 | const [cls, label] = _MACWATCH_VERDICT_STYLE[d.verdict] || _MACWATCH_VERDICT_STYLE.unknown; |
| 8733 | const s = d.summary || {}; |
| 8734 | let html = `<div class="mb-3 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 8735 | html += `<p class="text-xs text-gray-500 mb-2">Source: ${escapeHtml(d.source || '—')}</p>`; |
| 8736 | |
| 8737 | // Summary chips |
| 8738 | const chip = (n, txt, bad) => |
| 8739 | `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-slate-800 border border-slate-700 text-xs ${bad && n ? 'text-red-300' : 'text-gray-300'}"> |
| 8740 | <span class="font-mono font-semibold">${n}</span>${escapeHtml(txt)}</span>`; |
| 8741 | html += '<div class="flex flex-wrap gap-2 mb-3">' + |
| 8742 | chip(s.observed || 0, ' seen') + |
| 8743 | chip(s.spoofed || 0, ' spoofed', true) + |
| 8744 | chip(s.clones || 0, ' cloned', true) + |
| 8745 | chip(s.past_events || 0, ' past events', true) + |
| 8746 | chip(s.randomized || 0, ' randomized') + |
| 8747 | chip(s.virtual || 0, ' virtual') + |
| 8748 | chip(s.tracks || 0, ' tracked devices') + |
| 8749 | '</div>'; |
| 8750 | |
| 8751 | // Spoofed / disguised-vendor MACs |
| 8752 | if (d.spoofed && d.spoofed.length) { |
| 8753 | html += '<p class="text-xs uppercase text-red-400 mt-3 mb-1">Spoofed (vendor OUI + locally-administered bit)</p>' + |
| 8754 | '<ul class="text-xs text-gray-300 list-disc pl-5">' + |
| 8755 | d.spoofed.map(c => `<li class="font-mono">${escapeHtml(c.mac)} <span class="text-gray-500">(${escapeHtml(c.vendor || '?')})</span> → ${escapeHtml((c.ips || []).join(', ') || 'unknown IP')}</li>`).join('') + |
| 8756 | '</ul>'; |
| 8757 | } |
| 8758 | // Cloned MACs (one MAC, many IPs) |
| 8759 | if (d.clones && d.clones.length) { |
| 8760 | html += '<p class="text-xs uppercase text-red-400 mt-3 mb-1">Cloned (one MAC, several IPs)</p>' + |
| 8761 | '<ul class="text-xs text-gray-300 list-disc pl-5">' + |
| 8762 | d.clones.map(c => `<li class="font-mono">${escapeHtml(c.mac)} → ${escapeHtml((c.ips || []).slice(0, 6).join(', '))}${(c.ips || []).length > 6 ? '…' : ''}</li>`).join('') + |
| 8763 | '</ul>'; |
| 8764 | } |
| 8765 | // Tracked devices (rotating randomized MACs) |
| 8766 | if (d.tracks && d.tracks.length) { |
| 8767 | html += '<p class="text-xs uppercase text-amber-400 mt-3 mb-1">Tracked devices (rotating randomized MACs)</p>'; |
| 8768 | html += d.tracks.map(t => |
| 8769 | `<div class="mb-2 pl-3 border-l-2 border-amber-700/60"> |
no test coverage detected