()
| 6747 | let html = `<div class="mb-2 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 6748 | html += `<p class="text-xs text-gray-500 mb-2">Interface: ${escapeHtml(d.interface || '—')} · active gateway: <span class="font-mono">${escapeHtml(d.gateway || '—')}</span>${d.learned ? ' · <span class="text-gray-400">baseline learned now</span>' : ''}</p>`; |
| 6749 | |
| 6750 | // DHCP servers table |
| 6751 | const servers = d.servers || []; |
| 6752 | if (servers.length) { |
| 6753 | html += '<p class="text-xs uppercase text-gray-400 mt-2 mb-1">DHCP servers that answered (' + servers.length + ')</p>' + |
| 6754 | '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 6755 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">Server</th><th class="px-2 py-1">Offered gateway</th><th class="px-2 py-1">Offered DNS</th><th class="px-2 py-1">Lease</th><th class="px-2 py-1">Status</th></tr>' + |
| 6756 | '</thead><tbody>' + |
| 6757 | servers.map(s => { |
| 6758 | const badge = s.rogue ? '<span class="text-red-300">🛑 rogue</span>' |
| 6759 | : (s.trusted ? '<span class="text-green-400">✓ trusted</span>' : '<span class="text-amber-300">? new</span>'); |
| 6760 | const gwBad = d.gateway && s.router && s.router !== d.gateway; |
| 6761 | return `<tr class="border-t border-slate-800"> |
| 6762 | <td class="px-2 py-1 font-mono ${s.rogue ? 'text-red-300' : ''}">${escapeHtml(s.server_id || '—')}</td> |
| 6763 | <td class="px-2 py-1 font-mono ${gwBad ? 'text-red-300' : ''}">${escapeHtml(s.router || '—')}</td> |
| 6764 | <td class="px-2 py-1 font-mono">${escapeHtml((s.dns || []).join(', ') || '—')}</td> |
| 6765 | <td class="px-2 py-1">${escapeHtml(s.lease || '—')}</td> |
| 6766 | <td class="px-2 py-1">${badge}</td> |
| 6767 | </tr>`; |
| 6768 | }).join('') + |
| 6769 | '</tbody></table>'; |
| 6770 | } else { |
| 6771 | html += '<p class="text-xs text-gray-400 mt-2">No DHCP server answered (static addressing, or the pool may be exhausted).</p>'; |
| 6772 | } |
| 6773 | |
| 6774 | // Starvation stats |
| 6775 | const st = d.starvation || {}; |
| 6776 | if (st.captured) { |
| 6777 | html += `<p class="text-xs text-gray-400 mt-2">Starvation watch: <span class="text-gray-200">${st.clients || 0}</span> distinct clients / <span class="text-gray-200">${st.requests || 0}</span> requests in the capture window.</p>`; |
| 6778 | } else if (st.error) { |
| 6779 | html += `<p class="text-xs text-gray-600 mt-2">Starvation capture skipped: ${escapeHtml(st.error)}</p>`; |
| 6780 | } |
| 6781 | if (d.arp_verdict && d.arp_verdict !== 'clean' && d.arp_verdict !== 'unknown') { |
| 6782 | html += `<p class="text-xs text-red-300 mt-1">Gateway ARP verdict: ${escapeHtml(d.arp_verdict)}</p>`; |
| 6783 | } |
| 6784 | if (d.reasons && d.reasons.length) { |
nothing calls this directly
no test coverage detected