import { API, setupCopyBtn } from '../shared.js';
export const page = {
title: 'IP Info & Tools',
template: () => `
Your Digital Footprint
Location Details
Country: -
Region: -
City: -
Zip: -
Coords: -
Time: -
Browser Fingerprint
User Agent string
IP Address / Domain Lookup
Result for:
Geolocation
Country: -
Region: -
City: -
Zip: -
Coords: -
Time: -
Location Map
Ping —
min - ms
avg - ms
max - ms
Raw output
`,
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);
const v6Flags = document.getElementById('privacy-flags-v6');
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(':') && ip6 !== currentIp) {
// Separate IPv6 address — show it and run its own privacy check
document.getElementById('ipv6-address').textContent = ip6;
document.getElementById('ipv6-row').classList.remove('hidden');
copyIpv6Btn.classList.remove('hidden');
if (v6Flags) v6Flags.innerHTML = '';
fetchPrivacyFlags(ip6, 6);
} else {
// No IPv6, timed out, or same exit as IPv4 (VPN tunnels both)
if (v6Flags) v6Flags.innerHTML = 'N/A';
}
} catch {
if (v6Flags) v6Flags.innerHTML = 'N/A';
} 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, version = 4) {
const container = document.getElementById(`privacy-flags-v${version}`);
const loader = document.getElementById(`privacy-loader-v${version}`);
if (!container) return;
try {
const r = await fetch(`${API}/privacy/${encodeURIComponent(ip)}`);
const data = await r.json();
loader?.remove();
if (!data.flags?.length) {
container.innerHTML = '—';
return;
}
container.innerHTML = data.flags.map(f =>
`${f.label}`
).join('');
} catch {
loader?.remove();
container.innerHTML = 'N/A';
}
}
// ── 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, 4);
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();
fetchIpInfo().then(() => checkIPv6());
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; }
};
}
};