()
| 7761 | if (!chipsEl || !listEl) return; |
| 7762 | if (!d || d.success === false) return; |
| 7763 | if (!d.enabled) { |
| 7764 | chipsEl.innerHTML = '<span class="text-xs text-gray-500">Watchtower is off — enable it in Diagnostics to aggregate the watcher daemons here.</span>'; |
| 7765 | listEl.innerHTML = ''; if (srcEl) srcEl.textContent = ''; |
| 7766 | return; |
| 7767 | } |
| 7768 | const s = d.summary || {}; |
| 7769 | const bySev = s.by_severity || {}; |
| 7770 | const chips = ['critical', 'high', 'medium', 'low', 'info'] |
| 7771 | .filter(k => bySev[k]).map(k => _wtSevChip(k, bySev[k])); |
| 7772 | chipsEl.innerHTML = chips.length ? chips.join(' ') |
| 7773 | : '<span class="px-2.5 py-1 rounded border bg-green-950/40 border-green-900 text-green-400">✓ All clear — no watcher alerts</span>'; |
| 7774 | const alerts = (d.alerts || []).slice(0, 5); |
| 7775 | listEl.innerHTML = alerts.map(a => { |
| 7776 | const [cls] = _WT_SEV_STYLE[a.severity] || _WT_SEV_STYLE.info; |
| 7777 | const t = a.ts ? new Date(a.ts * 1000).toLocaleString() : ''; |
| 7778 | return `<div class="rounded-lg border ${cls} px-3 py-2 text-sm"> |
| 7779 | <span class="text-xs uppercase tracking-wide opacity-80">${escapeHtml(a.label || a.source)}</span> |
| 7780 | <span class="ml-1">${escapeHtml(a.title || '')}</span> |
| 7781 | <span class="text-xs opacity-60 ml-1">· ${escapeHtml(t)}</span> |
| 7782 | </div>`; |
| 7783 | }).join(''); |
| 7784 | if (srcEl) { |
| 7785 | const srcs = s.sources || []; |
| 7786 | const on = srcs.filter(x => x.present).length; |
| 7787 | srcEl.textContent = srcs.length |
| 7788 | ? `${on}/${srcs.length} watchers logging: ` + srcs.map(x => (x.present ? '● ' : '○ ') + (x.label || x.name)).join(' ') |
| 7789 | : ''; |
| 7790 | } |
| 7791 | } |
| 7792 | async function refreshDashWatchtower() { |
| 7793 | try { renderDashWatchtower(await fetchAPI('/api/net/watchtower?limit=5&min_severity=medium')); } |
| 7794 | catch (e) { /* offline — leave as-is */ } |
| 7795 | refreshIncidents('dash-incidents'); |
| 7796 | } |
| 7797 | |
| 7798 | // ---- ARP Poisoning card (on-demand) --------------------------------------- |
| 7799 | const _ARP_VERDICT_STYLE = { |
| 7800 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ No ARP spoofing detected'], |
| 7801 | suspicious: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ Possible ARP spoofing'], |
| 7802 | spoofed: ['bg-red-950/60 border-red-800 text-red-300', '🛑 ARP spoofing / MITM detected'], |
| 7803 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', '— Could not determine (no gateway / no ARP reply)'], |
| 7804 | }; |
| 7805 | function _arpFillIfaces() { |
| 7806 | const sel = document.getElementById('arp-check-iface'); |
| 7807 | if (!sel || sel.dataset.filled === '1') return Promise.resolve(); |
| 7808 | return fetchAPI('/api/net/interfaces').then(x => { |
| 7809 | (x.interfaces || []).forEach(i => { |
| 7810 | const o = document.createElement('option'); |
| 7811 | o.value = i.name; |
| 7812 | const tag = i.type === 'wifi' ? ' (WiFi)' : i.type === 'ethernet' ? ' (LAN)' : (i.type ? ' (' + i.type + ')' : ''); |
| 7813 | o.textContent = i.name + tag; |
| 7814 | sel.appendChild(o); |
| 7815 | }); |
| 7816 | sel.dataset.filled = '1'; |
| 7817 | }).catch(() => {}); |
| 7818 | } |
nothing calls this directly
no test coverage detected