import { API, setupCopyBtn } from '../shared.js'; export const page = { title: 'IP Info & Tools', template: () => `

Your Digital Footprint

Your Public IP

Location Details

Network (ASN)

Hostname (rDNS)

Location

Browser Fingerprint

Browser

-

Operating System

-

Screen

-

Viewport

-

Language

-

Timezone

-

Color Depth

-

Privacy

-

User Agent string

IP Address / Domain Lookup

`, async init(search) { // ── DOM refs ────────────────────────────────────────────────── const ipAddressLink = document.getElementById('ip-address-link'); const ipAddressSpan = document.getElementById('ip-address'); const copyIpBtn = document.getElementById('copy-ip-btn'); const ipLoader = document.getElementById('ip-loader'); const geoLoader = document.getElementById('geo-loader'); const asnLoader = document.getElementById('asn-loader'); const rdnsLoader = document.getElementById('rdns-loader'); const mapLoader = document.getElementById('map-loader'); const mapEl = document.getElementById('map'); const mapMessage = document.getElementById('map-message'); const globalError = document.getElementById('global-error'); const lookupInput = document.getElementById('lookup-ip-input'); const lookupBtn = document.getElementById('lookup-button'); const lookupErrorEl = document.getElementById('lookup-error'); const lookupSection = document.getElementById('lookup-results-section'); const lookupIpEl = document.getElementById('lookup-ip-address'); const copyLookupIpBtn = document.getElementById('copy-lookup-ip-btn'); const lookupResLoader = document.getElementById('lookup-result-loader'); const lookupMapEl = document.getElementById('lookup-map'); const lookupMapLoader = document.getElementById('lookup-map-loader'); const lookupMapMsg = document.getElementById('lookup-map-message'); const lookupPingBtn = document.getElementById('lookup-ping-button'); const lookupTraceBtn = document.getElementById('lookup-trace-button'); const lookupScanBtn = document.getElementById('lookup-scan-button'); // Ping section const pingSect = document.getElementById('ping-section'); const pingTarget = document.getElementById('ping-target'); const pingSectLoader = document.getElementById('ping-section-loader'); const pingSectMsg = document.getElementById('ping-section-message'); const pingSectErr = document.getElementById('ping-section-error'); const pingStatsGrid = document.getElementById('ping-stats-grid'); const pingRttRange = document.getElementById('ping-rtt-range'); const pingRawOutput = document.getElementById('ping-raw-output'); const tracerouteSection = document.getElementById('traceroute-section'); const tracerouteTarget = document.getElementById('traceroute-target'); const tracerouteOutput = document.getElementById('traceroute-output'); const tracerouteLoader = document.getElementById('traceroute-loader'); const tracerouteMessage = document.getElementById('traceroute-message'); const tracerouteStopBtn = document.getElementById('traceroute-stop-btn'); const portScanSection = document.getElementById('port-scan-section'); const portScanOutput = document.getElementById('port-scan-output'); const portScanLoader = document.getElementById('port-scan-loader'); const portScanMessage = document.getElementById('port-scan-message'); const portScanStopBtn = document.getElementById('port-scan-stop-btn'); // ── State ──────────────────────────────────────────────────── let currentIp = null, currentLookupIp = null; let eventSource = null, portScanEventSource = null; // ── Helpers ────────────────────────────────────────────────── function showGlobalErr(msg) { if (!globalError) return; globalError.textContent = `Error: ${msg}`; globalError.classList.remove('hidden'); } function isValidIp(input) { const v4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/; const v6 = /^(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6}|:(?::[a-fA-F0-9]{1,4}){1,7}|::)$/; return v4.test(input) || v6.test(input); } function updateField(el, val, loaderEl = null, errorEl = null, def = '-') { if (loaderEl) loaderEl.classList.add('hidden'); if (errorEl) errorEl.textContent = ''; const container = el?.closest('div:not(.loader)'); if (container?.classList.contains('hidden')) container.classList.remove('hidden'); if (val && typeof val === 'object' && val.error) { if (el) el.textContent = def; if (errorEl) errorEl.textContent = val.error; } else if (val != null && val !== '') { if (el) el.textContent = val; } else { if (el) el.textContent = def; } } function updateRdns(listEl, data, loaderEl = null, errorEl = null) { if (loaderEl) loaderEl.classList.add('hidden'); if (listEl) listEl.innerHTML = ''; if (errorEl) errorEl.textContent = ''; const container = listEl?.closest('div:not(.loader)'); if (container?.classList.contains('hidden')) container.classList.remove('hidden'); if (Array.isArray(data)) { if (data.length > 0) data.forEach(h => { const li = document.createElement('li'); li.textContent = h; listEl.appendChild(li); }); else if (listEl) listEl.innerHTML = '
  • No rDNS records found.
  • '; } else if (data?.error) { if (listEl) listEl.innerHTML = '
  • -
  • '; if (errorEl) errorEl.textContent = data.error; } else { if (listEl) listEl.innerHTML = '
  • -
  • '; } } function initOrUpdateMap(mapId, lat, lon, mapElement, loaderEl, msgEl) { if (!mapElement || !loaderEl || !msgEl) return null; loaderEl.classList.add('hidden'); let inst = window[mapId + '_instance']; if (lat != null && lon != null) { mapElement.classList.remove('hidden'); msgEl.classList.add('hidden'); if (inst) { inst.setView([lat, lon], 13); inst.eachLayer(l => { if (l instanceof L.Marker) inst.removeLayer(l); }); L.marker([lat, lon]).addTo(inst).bindPopup('Approximate Location').openPopup(); } else { try { inst = L.map(mapId).setView([lat, lon], 13); L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap © CARTO', subdomains: 'abcd', maxZoom: 19 }).addTo(inst); L.marker([lat, lon]).addTo(inst).bindPopup('Approximate Location').openPopup(); window[mapId + '_instance'] = inst; } catch (e) { console.error('Map init failed:', e); mapElement.classList.add('hidden'); msgEl.classList.remove('hidden'); msgEl.textContent = 'Error initializing map.'; return null; } } setTimeout(() => { if (window[mapId + '_instance']) window[mapId + '_instance'].invalidateSize(); }, 100); return inst; } else { mapElement.classList.add('hidden'); msgEl.classList.remove('hidden'); msgEl.textContent = 'Map could not be loaded (missing coordinates).'; if (inst) { inst.remove(); window[mapId + '_instance'] = null; } return null; } } // ── Copy buttons ───────────────────────────────────────────── setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent); setupCopyBtn(copyLookupIpBtn, () => lookupIpEl.textContent); const copyIpv6Btn = document.getElementById('copy-ipv6-btn'); setupCopyBtn(copyIpv6Btn, () => document.getElementById('ipv6-address').textContent); // ── Lookup button enable/disable ───────────────────────────── const syncLookupBtn = () => { lookupBtn.disabled = !lookupInput.value.trim(); }; lookupInput.addEventListener('input', syncLookupBtn); // ── IPv6 detection ─────────────────────────────────────────── async function checkIPv6() { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 4000); try { const r = await fetch('https://ipv6.icanhazip.com', { signal: ctrl.signal }); const ip6 = r.ok ? (await r.text()).trim() : null; if (ip6 && ip6.includes(':')) { document.getElementById('ipv6-address').textContent = ip6; const row = document.getElementById('ipv6-row'); row.classList.remove('hidden'); copyIpv6Btn.classList.remove('hidden'); } } catch { /* no IPv6 or timeout — show nothing */ } finally { clearTimeout(timer); } } // ── Privacy / risk flags ────────────────────────────────────── const FLAG_COLORS = { green: 'bg-green-900/40 text-green-300 border border-green-700/50', orange: 'bg-orange-900/40 text-orange-300 border border-orange-700/50', yellow: 'bg-yellow-900/40 text-yellow-300 border border-yellow-700/50', red: 'bg-red-900/40 text-red-300 border border-red-700/50', }; async function fetchPrivacyFlags(ip) { const loader = document.getElementById('privacy-loader'); const container = document.getElementById('privacy-flags'); try { const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`); const data = await r.json(); if (!data.flags?.length) return; container.innerHTML = data.flags.map(f => `${f.label}` ).join(''); container.classList.remove('hidden'); } catch { /* silent fail */ } finally { loader?.classList.add('hidden'); } } // ── Browser fingerprint ─────────────────────────────────────── function populateBrowserFingerprint() { const ua = navigator.userAgent; function getBrowser() { if (/Edg\/(\d+)/.test(ua)) return `Edge ${RegExp.$1}`; if (/OPR\/(\d+)/.test(ua)) return `Opera ${RegExp.$1}`; if (/Chrome\/(\d+)/.test(ua)) return `Chrome ${RegExp.$1}`; if (/Firefox\/(\d+)/.test(ua)) return `Firefox ${RegExp.$1}`; if (/Version\/([\d.]+).*Safari/.test(ua)) return `Safari ${RegExp.$1}`; return 'Unknown'; } function getOS() { if (/Windows NT 10\.0/.test(ua)) return 'Windows 10 / 11'; if (/Windows NT 6\.3/.test(ua)) return 'Windows 8.1'; if (/Windows NT 6\.1/.test(ua)) return 'Windows 7'; if (/Mac OS X ([\d_]+)/.test(ua)) return `macOS ${RegExp.$1.replace(/_/g, '.')}`; if (/Android ([\d.]+)/.test(ua)) return `Android ${RegExp.$1}`; if (/iPhone OS ([\d_]+)/.test(ua)) return `iOS ${RegExp.$1.replace(/_/g, '.')}`; if (/iPad.*OS ([\d_]+)/.test(ua)) return `iPadOS ${RegExp.$1.replace(/_/g, '.')}`; if (/Linux/.test(ua)) return 'Linux'; return 'Unknown'; } document.getElementById('fp-browser').textContent = getBrowser(); document.getElementById('fp-os').textContent = getOS(); document.getElementById('fp-screen').textContent = `${screen.width} × ${screen.height}` + (window.devicePixelRatio !== 1 ? ` @ ${window.devicePixelRatio}×` : ''); document.getElementById('fp-viewport').textContent = `${window.innerWidth} × ${window.innerHeight} px`; document.getElementById('fp-language').textContent = navigator.language + (navigator.languages?.length > 1 ? ` (+${navigator.languages.length - 1} more)` : ''); document.getElementById('fp-timezone').textContent = Intl.DateTimeFormat().resolvedOptions().timeZone; document.getElementById('fp-color').textContent = `${screen.colorDepth}-bit`; const bits = []; if (navigator.cookieEnabled) bits.push('Cookies on'); else bits.push('Cookies off'); if (navigator.doNotTrack === '1') bits.push('DNT enabled'); const conn = navigator.connection; if (conn?.effectiveType) bits.push(conn.effectiveType.toUpperCase()); document.getElementById('fp-privacy').textContent = bits.join(' · '); document.getElementById('fp-ua').textContent = ua; } // ── Own IP fetch ───────────────────────────────────────────── async function fetchIpInfo() { globalError.classList.add('hidden'); [ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.remove('hidden')); ipAddressLink.classList.add('hidden'); copyIpBtn.classList.add('hidden'); mapEl.classList.add('hidden'); mapMessage.classList.add('hidden'); try { const r = await fetch(`${API}/ipinfo`); if (!r.ok) throw new Error(`${r.statusText} (${r.status})`); const data = await r.json(); currentIp = data.ip; fetchPrivacyFlags(data.ip); ipAddressSpan.textContent = data.ip; ipAddressLink.classList.remove('hidden'); copyIpBtn.classList.remove('hidden'); ipLoader.classList.add('hidden'); ipAddressLink.addEventListener('click', e => { e.preventDefault(); if (currentIp) window._router.navigate('/whois', { query: currentIp }); }); updateField(document.getElementById('country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('geo-error')); updateField(document.getElementById('region'), data.geo?.region); updateField(document.getElementById('city'), data.geo?.city); updateField(document.getElementById('postal'), data.geo?.postalCode); updateField(document.getElementById('coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null); updateField(document.getElementById('timezone'), data.geo?.timezone, geoLoader); const asnNum = (data.asn && !data.asn.error) ? data.asn.number : null; if (asnNum) { const asnContainer = document.getElementById('asn-number')?.closest('div:not(.loader)'); if (asnContainer) asnContainer.classList.remove('hidden'); document.getElementById('asn-number').innerHTML = `AS${asnNum}`; } else { updateField(document.getElementById('asn-number'), null, null, document.getElementById('asn-error'), data.asn?.error || '-'); } updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader); updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error')); initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage); } catch (err) { console.error('Failed to fetch IP info:', err); showGlobalErr(`Could not load IP information. ${err.message}`); [ipLoader, geoLoader, asnLoader, rdnsLoader, mapLoader].forEach(l => l?.classList.add('hidden')); } } // ── Lookup ─────────────────────────────────────────────────── function resetLookup() { lookupSection.classList.add('hidden'); lookupResLoader.classList.add('hidden'); lookupMapLoader.classList.add('hidden'); lookupMapEl.classList.add('hidden'); lookupMapMsg.classList.add('hidden'); pingSect.classList.add('hidden'); portScanSection.classList.add('hidden'); portScanOutput.innerHTML = ''; [lookupIpEl, document.getElementById('lookup-country'), document.getElementById('lookup-region'), document.getElementById('lookup-city'), document.getElementById('lookup-postal'), document.getElementById('lookup-coords'), document.getElementById('lookup-timezone'), document.getElementById('lookup-asn-number'), document.getElementById('lookup-asn-org'), document.getElementById('lookup-geo-error'), document.getElementById('lookup-asn-error'), document.getElementById('lookup-rdns-error')].forEach(el => { if (el) el.textContent = ''; }); document.getElementById('lookup-rdns-list').innerHTML = '
  • -
  • '; [lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = true; }); currentLookupIp = null; if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; } if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; } } async function doLookup(query) { resetLookup(); lookupErrorEl.classList.add('hidden'); globalError.classList.add('hidden'); const url = new URL(location.href); url.searchParams.set('ip', query); history.replaceState({}, '', url); lookupSection.classList.remove('hidden'); lookupResLoader.classList.remove('hidden'); lookupMapLoader.classList.remove('hidden'); let ipToLookup = query; if (!isValidIp(query)) { try { const r = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(query)}&type=ANY`); const data = await r.json(); if (r.ok && data.success) { const ip = data.records?.A?.[0] ?? data.records?.AAAA?.[0]; if (ip) ipToLookup = ip; else throw new Error('No A or AAAA records found.'); } else { throw new Error(data.error || 'DNS lookup failed.'); } } catch (err) { lookupErrorEl.textContent = `Error: Could not resolve domain — ${err.message}`; lookupErrorEl.classList.remove('hidden'); resetLookup(); return; } } try { const r = await fetch(`${API}/lookup?targetIp=${encodeURIComponent(ipToLookup)}`); const data = await r.json(); if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); currentLookupIp = data.ip; updateField(lookupIpEl, data.ip); updateField(document.getElementById('lookup-country'), data.geo?.countryName ? `${data.geo.countryName} (${data.geo.country})` : null, null, document.getElementById('lookup-geo-error')); updateField(document.getElementById('lookup-region'), data.geo?.region); updateField(document.getElementById('lookup-city'), data.geo?.city); updateField(document.getElementById('lookup-postal'), data.geo?.postalCode); updateField(document.getElementById('lookup-coords'), data.geo?.latitude ? `${data.geo.latitude}, ${data.geo.longitude}` : null); updateField(document.getElementById('lookup-timezone'), data.geo?.timezone); if (data.asn?.number) { document.getElementById('lookup-asn-number').innerHTML = `AS${data.asn.number}`; } else { updateField(document.getElementById('lookup-asn-number'), data.asn?.number, null, document.getElementById('lookup-asn-error')); } updateField(document.getElementById('lookup-asn-org'), data.asn?.organization); updateRdns(document.getElementById('lookup-rdns-list'), data.rdns, null, document.getElementById('lookup-rdns-error')); initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg); [lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = false; }); } catch (err) { lookupErrorEl.textContent = `Error: Lookup failed — ${err.message}`; lookupErrorEl.classList.remove('hidden'); lookupMapMsg.textContent = 'Map could not be loaded.'; lookupMapMsg.classList.remove('hidden'); lookupMapEl.classList.add('hidden'); lookupMapLoader.classList.add('hidden'); resetLookup(); } finally { lookupResLoader.classList.add('hidden'); } } // ── Ping ───────────────────────────────────────────────────── async function runPing(ip) { pingSect.classList.remove('hidden'); pingTarget.textContent = ip; pingSectLoader.classList.remove('hidden'); pingSectMsg.textContent = `Pinging ${ip}…`; pingSectErr.textContent = ''; pingStatsGrid.classList.add('hidden'); pingRttRange.classList.add('hidden'); pingRawOutput.textContent = ''; pingSect.scrollIntoView({ behavior: 'smooth', block: 'start' }); try { const r = await fetch(`${API}/ping?targetIp=${encodeURIComponent(ip)}`); const data = await r.json(); if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`); if (data.stats?.packets) { const loss = data.stats.packets.lossPercent; document.getElementById('ping-stat-sent').textContent = data.stats.packets.transmitted; document.getElementById('ping-stat-recv').textContent = data.stats.packets.received; document.getElementById('ping-stat-loss').textContent = `${loss}%`; document.getElementById('ping-stat-loss').className = `text-3xl font-bold font-mono ${loss === 0 || loss === '0' ? 'text-green-400' : loss >= 50 ? 'text-red-400' : 'text-yellow-400'}`; document.getElementById('ping-stat-rtt').textContent = data.stats.rtt ? `${data.stats.rtt.avg} ms` : '-'; pingStatsGrid.classList.remove('hidden'); } if (data.stats?.rtt) { document.getElementById('ping-rtt-min').textContent = data.stats.rtt.min; document.getElementById('ping-rtt-avg').textContent = data.stats.rtt.avg; document.getElementById('ping-rtt-max').textContent = data.stats.rtt.max; pingRttRange.classList.remove('hidden'); } pingRawOutput.textContent = data.rawOutput || ''; pingSectMsg.textContent = `Ping to ${ip} complete.`; } catch (err) { pingSectErr.textContent = `Ping failed: ${err.message}`; pingSectMsg.textContent = ''; } finally { pingSectLoader.classList.add('hidden'); } } // ── Traceroute ─────────────────────────────────────────────── function startTraceroute(ip) { if (eventSource) { eventSource.close(); eventSource = null; } tracerouteSection.classList.remove('hidden'); if (tracerouteTarget) tracerouteTarget.textContent = ip; tracerouteOutput.innerHTML = ''; tracerouteLoader.classList.remove('hidden'); tracerouteStopBtn.classList.remove('hidden'); tracerouteMessage.textContent = `Starting traceroute to ${ip}…`; globalError.classList.add('hidden'); tracerouteSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); eventSource = new EventSource(`${API}/traceroute?targetIp=${encodeURIComponent(ip)}`); eventSource.onopen = () => { tracerouteMessage.textContent = `Traceroute to ${ip} in progress…`; }; eventSource.onerror = () => { tracerouteMessage.textContent = eventSource.readyState === EventSource.CLOSED ? 'Connection closed.' : 'Connection error.'; tracerouteLoader.classList.add('hidden'); tracerouteStopBtn.classList.add('hidden'); eventSource.close(); }; eventSource.addEventListener('hop', e => { try { displayHop(JSON.parse(e.data)); } catch { displayTraceLine(`[Parse error: ${e.data}]`, 'error-line'); } }); eventSource.addEventListener('info', e => { try { displayTraceLine(JSON.parse(e.data).message, 'info-line'); } catch {} }); eventSource.addEventListener('error', e => { try { const d = JSON.parse(e.data); displayTraceLine(d.error, 'error-line'); } catch {} }); eventSource.addEventListener('end', e => { try { const d = JSON.parse(e.data); const msg = `Traceroute finished${d.exitCode === 0 ? ' successfully' : ` (exit code ${d.exitCode})`}.`; displayTraceLine(msg, 'end-line'); tracerouteMessage.textContent = msg; } catch {} tracerouteLoader.classList.add('hidden'); tracerouteStopBtn.classList.add('hidden'); eventSource.close(); }); } function stopTraceroute() { if (eventSource) { eventSource.close(); eventSource = null; } tracerouteLoader.classList.add('hidden'); tracerouteStopBtn.classList.add('hidden'); tracerouteMessage.textContent = 'Traceroute stopped.'; displayTraceLine('— Stopped by user —', 'info-line'); } function displayTraceLine(text, cls = '') { const div = document.createElement('div'); div.classList.add('px-2', 'py-0.5', 'fade-in'); if (cls) div.classList.add(cls); div.textContent = text; tracerouteOutput.appendChild(div); tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight; } function displayHop(hop) { const row = document.createElement('div'); row.classList.add('hop-row', 'fade-in'); // Hop number const numEl = document.createElement('span'); numEl.classList.add('hop-number'); numEl.textContent = hop.hop ?? '?'; row.appendChild(numEl); // Body: IP line + optional RDNS line below const body = document.createElement('div'); body.classList.add('hop-body'); if (hop.ip) { const ipLine = document.createElement('div'); ipLine.classList.add('hop-ip-line'); const ipEl = document.createElement('span'); ipEl.classList.add('hop-ip'); ipEl.textContent = hop.ip; ipLine.appendChild(ipEl); // RTTs — right-aligned via flex margin-left auto on the rtt group if (Array.isArray(hop.rtt)) { const rttsEl = document.createElement('span'); rttsEl.classList.add('hop-rtts'); hop.rtt.forEach(r => { const s = document.createElement('span'); s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt'); s.textContent = r === '*' ? '*' : `${r} ms`; rttsEl.appendChild(s); }); ipLine.appendChild(rttsEl); } body.appendChild(ipLine); // RDNS — own line, only when it differs from the IP if (hop.hostname && hop.hostname !== hop.ip) { const rdnsEl = document.createElement('div'); rdnsEl.classList.add('hop-rdns'); rdnsEl.textContent = hop.hostname; body.appendChild(rdnsEl); } } else if (hop.rtt?.every(r => r === '*')) { const line = document.createElement('div'); line.classList.add('hop-ip-line'); const t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; line.appendChild(t); body.appendChild(line); } else { const line = document.createElement('div'); line.classList.add('hop-ip-line', 'text-gray-400'); line.textContent = hop.rawLine || 'Unknown hop'; body.appendChild(line); } row.appendChild(body); tracerouteOutput.appendChild(row); tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight; } // ── Port Scan ──────────────────────────────────────────────── function startPortScan(ip) { if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; } portScanSection.classList.remove('hidden'); portScanOutput.innerHTML = ''; portScanLoader.classList.remove('hidden'); portScanStopBtn.classList.remove('hidden'); portScanMessage.textContent = `Starting port scan for ${ip}…`; portScanSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); portScanEventSource = new EventSource(`${API}/port-scan?targetIp=${encodeURIComponent(ip)}`); portScanEventSource.onopen = () => {}; portScanEventSource.onerror = () => { portScanMessage.textContent = 'Connection error during port scan.'; portScanLoader.classList.add('hidden'); portScanStopBtn.classList.add('hidden'); portScanEventSource.close(); }; portScanEventSource.addEventListener('info', e => { try { portScanMessage.textContent = JSON.parse(e.data).message; } catch {} }); portScanEventSource.addEventListener('port_status', e => { try { displayPortResult(JSON.parse(e.data)); } catch {} }); portScanEventSource.addEventListener('error', e => { try { displayPortResult({ error: JSON.parse(e.data).error }); } catch {} }); portScanEventSource.addEventListener('end', e => { try { portScanMessage.textContent = JSON.parse(e.data).message; } catch {} portScanLoader.classList.add('hidden'); portScanStopBtn.classList.add('hidden'); portScanEventSource.close(); }); } function stopPortScan() { if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; } portScanLoader.classList.add('hidden'); portScanStopBtn.classList.add('hidden'); portScanMessage.textContent = 'Port scan stopped.'; } function displayPortResult(data) { const div = document.createElement('div'); div.classList.add('mb-1', 'fade-in'); if (data.error) { div.innerHTML = `Error: ${data.error}`; } else { const colors = { open: 'text-green-400', closed: 'text-red-400', timeout: 'text-yellow-400' }; const labels = { open: 'OPEN', closed: 'CLOSED', timeout: 'TIMEOUT (Filtered?)' }; const col = colors[data.status] || 'text-gray-400'; const lbl = labels[data.status] || (data.status || '').toUpperCase(); div.innerHTML = `Port ${data.port} (${data.service}): ${lbl}`; } portScanOutput.appendChild(div); portScanOutput.scrollTop = portScanOutput.scrollHeight; } // ── Event listeners ────────────────────────────────────────── lookupBtn.addEventListener('click', () => { const q = lookupInput.value.trim(); if (q) doLookup(q); }); lookupInput.addEventListener('keypress', e => { if (e.key === 'Enter' && !lookupBtn.disabled) { const q = lookupInput.value.trim(); if (q) doLookup(q); } }); lookupPingBtn.addEventListener('click', () => { if (currentLookupIp) runPing(currentLookupIp); }); lookupTraceBtn.addEventListener('click', () => { if (currentLookupIp) startTraceroute(currentLookupIp); }); lookupScanBtn.addEventListener('click', () => { if (currentLookupIp) startPortScan(currentLookupIp); }); tracerouteStopBtn.addEventListener('click', stopTraceroute); portScanStopBtn.addEventListener('click', stopPortScan); // ── Bootstrap ──────────────────────────────────────────────── populateBrowserFingerprint(); checkIPv6(); fetchIpInfo(); const params = new URLSearchParams(search); const ipParam = params.get('ip'); if (ipParam) { lookupInput.value = ipParam; syncLookupBtn(); doLookup(ipParam); } // ── Cleanup ────────────────────────────────────────────────── return () => { if (eventSource) { eventSource.close(); eventSource = null; } if (portScanEventSource) { portScanEventSource.close(); portScanEventSource = null; } if (window['map_instance']) { window['map_instance'].remove(); window['map_instance'] = null; } if (window['lookup-map_instance']) { window['lookup-map_instance'].remove(); window['lookup-map_instance'] = null; } }; } };