(endpoint)
| 11605 | } |
| 11606 | const loc = [r.city, r.region, r.country].filter(Boolean).map(escapeHtml).join(', ') || '—'; |
| 11607 | return `<tr class="border-t border-slate-800"> |
| 11608 | <td class="px-3 py-1.5 font-mono font-semibold">${escapeHtml(r.interface)}</td> |
| 11609 | <td class="px-3 py-1.5">${ispCell(r)}</td> |
| 11610 | <td class="px-3 py-1.5 font-mono">${escapeHtml(String(r.asn || '—'))}</td> |
| 11611 | <td class="px-3 py-1.5 font-mono">${escapeHtml(String(r.public_ip || '—'))}</td> |
| 11612 | <td class="px-3 py-1.5">${loc}</td> |
| 11613 | <td class="px-3 py-1.5 text-gray-500">${escapeHtml(String(r.source || '—'))}</td> |
| 11614 | <td class="px-3 py-1.5">${vpnCell(r)}</td> |
| 11615 | </tr>`; |
| 11616 | }).join(''); |
| 11617 | out.innerHTML = `<table class="min-w-full text-sm text-gray-300 whitespace-nowrap"> |
| 11618 | <thead><tr class="text-left text-xs uppercase text-gray-500"> |
| 11619 | <th class="px-3 py-1.5">Interface</th><th class="px-3 py-1.5">ISP</th> |
| 11620 | <th class="px-3 py-1.5">ASN</th><th class="px-3 py-1.5">Public IP</th> |
| 11621 | <th class="px-3 py-1.5">Location</th><th class="px-3 py-1.5">Source</th> |
| 11622 | <th class="px-3 py-1.5">VPN</th> |
| 11623 | </tr></thead><tbody>${rows}</tbody></table>`; |
| 11624 | } catch (e) { |
| 11625 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 11626 | } finally { |
| 11627 | _ndBusy(btn, false); |
| 11628 | } |
| 11629 | } |
| 11630 | |
| 11631 | function exportIspCsv() { |
| 11632 | _ndDownloadCsv('interface_isp', |
| 11633 | ['Interface', 'ISP', 'ASN', 'Public IP', 'City', 'Region', 'Country', 'Source', |
| 11634 | 'Behind VPN', 'Known VPN IP', 'VPN kind', 'VPN endpoint', 'VPN provider', |
| 11635 | 'Tunnel iface', 'Error'], |
| 11636 | (_ndLastIsp || []).map(r => [r.interface, r.isp || r.org, r.asn, r.public_ip, |
| 11637 | r.city, r.region, r.country, r.source, |
| 11638 | r.behind_vpn ? 'yes' : 'no', |
| 11639 | r.vpn_ip_match === true ? 'yes' : r.vpn_ip_match === false ? 'no' : 'unchecked', |
| 11640 | r.vpn_kind, r.vpn_endpoint, r.vpn_provider, |
| 11641 | r.iface_is_vpn ? 'yes' : 'no', r.error])); |
| 11642 | } |
| 11643 | |
| 11644 | function showDiscoveredSubtab(name) { |
| 11645 | const views = { main: 'disc-sub-main', credentials: 'disc-sub-credentials' }; |
| 11646 | Object.keys(views).forEach(key => { |
| 11647 | const el = document.getElementById(views[key]); |
| 11648 | if (el) el.classList.toggle('hidden', key !== name); |
| 11649 | _setSubtabActive(document.getElementById('disc-subtab-' + key), key === name); |
| 11650 | }); |
| 11651 | if (name === 'credentials') { |
| 11652 | loadCredentials(); |
| 11653 | } |
| 11654 | } |
| 11655 | |
| 11656 | function refreshCurrentTab() { |
| 11657 | loadTabData(currentTab); |
| 11658 | addConsoleMessage(`Refreshed ${currentTab} data`, 'info'); |
| 11659 | } |
| 11660 |
no test coverage detected