()
| 5390 | domainHtml = d.domains.map(escapeHtml).join('<br>'); |
| 5391 | } else if (d.guessed_domain) { |
| 5392 | domainHtml = escapeHtml(d.guessed_domain) + |
| 5393 | ' <span class="text-amber-400 text-xs">(guessed from gateway/FQDN)</span>'; |
| 5394 | } else { |
| 5395 | domainHtml = '<span class="text-gray-500">none advertised</span>'; |
| 5396 | } |
| 5397 | row('Domain / search', domainHtml); |
| 5398 | |
| 5399 | // Host name / FQDN |
| 5400 | let hostHtml = escapeHtml(String(d.hostname || '—')); |
| 5401 | if (d.fqdn) hostHtml += ' <span class="text-gray-500">(' + escapeHtml(d.fqdn) + ')</span>'; |
| 5402 | row('This host', hostHtml); |
| 5403 | |
| 5404 | // Nameservers |
| 5405 | let nsHtml; |
| 5406 | if (d.nameservers && d.nameservers.length) { |
| 5407 | nsHtml = d.nameservers.map(escapeHtml).join('<br>'); |
| 5408 | if (d.dns_stub) nsHtml += '<br><span class="text-gray-500 text-xs">upstream via systemd-resolved stub</span>'; |
| 5409 | } else { |
| 5410 | nsHtml = '<span class="text-gray-500">—</span>'; |
| 5411 | } |
| 5412 | row('Nameservers', nsHtml); |
| 5413 | |
| 5414 | // Gateway |
| 5415 | let gwHtml = d.interface |
| 5416 | ? '<span class="text-gray-500">none — no route via ' + escapeHtml(d.interface) + ' (segment without a gateway, or DHCP still pending)</span>' |
| 5417 | : '<span class="text-gray-500">—</span>'; |
| 5418 | if (d.gateway && d.gateway.ip) { |
| 5419 | gwHtml = escapeHtml(d.gateway.ip); |
| 5420 | if (d.gateway.ptr) gwHtml += ' <span class="text-gray-500">(' + escapeHtml(d.gateway.ptr) + ')</span>'; |
| 5421 | } |
| 5422 | row(d.interface ? 'Gateway via ' + d.interface : 'Default gateway', gwHtml); |
| 5423 | |
| 5424 | // Traffic via VPN — a local tunnel on the default route is shown |
| 5425 | // instantly; anything else needs the *egress* checked (VPN/Tor on the |
| 5426 | // router is invisible locally), which makes outbound calls, so that |
| 5427 | // runs on demand via the button (checkVpnEgress → /api/net/vpn-check). |
| 5428 | const ve = d.vpn_egress; |
| 5429 | if (ve) { |
| 5430 | let veHtml; |
| 5431 | if (ve.via_vpn) { |
| 5432 | const via = escapeHtml(String(ve.interface || 'tunnel')) |
| 5433 | + (ve.kind ? ' · ' + escapeHtml(ve.kind) : '') |
| 5434 | + (ve.endpoint ? ' → ' + escapeHtml(ve.endpoint) : ''); |
| 5435 | veHtml = '<span class="text-amber-300">yes</span> <span class="text-gray-500">(via ' + via + ')</span>'; |
| 5436 | } else { |
| 5437 | veHtml = '<span id="vpn-egress-result" class="text-gray-500">no local tunnel</span> ' |
| 5438 | + '<select id="vpn-egress-iface" class="bg-slate-800 border border-slate-700 rounded text-xs text-gray-300 px-1 py-0.5 mx-1">' |
| 5439 | + '<option value="">auto (default route)</option></select>' |
| 5440 | + '<button onclick="checkVpnEgress()" class="text-xs text-cyan-400 hover:text-cyan-300 underline">check egress (catches VPN/Tor on the router)</button>'; |
| 5441 | } |
| 5442 | row('Traffic via VPN', veHtml); |
| 5443 | } |
| 5444 | |
| 5445 | const scopeNote = d.interface |
| 5446 | ? `<p class="text-[11px] text-cyan-400/80 mt-2">Scoped to <span class="font-mono">${escapeHtml(d.interface)}</span> — gateway + nameservers are what this interface's own network provides.</p>` : ''; |
| 5447 | const src = (d.sources && d.sources.length) |
| 5448 | ? `<p class="text-[11px] text-gray-500 mt-2">Sources: ${d.sources.map(escapeHtml).join(', ')}</p>` : ''; |
no test coverage detected