-
Traceroute Results
-
+
+
+ Traceroute —
+
+
-
+
+
+
-
Port Scan Results
+
+
+ Port Scan Results
+
@@ -215,13 +350,20 @@ export const page = {
const lookupPingBtn = document.getElementById('lookup-ping-button');
const lookupTraceBtn = document.getElementById('lookup-trace-button');
const lookupScanBtn = document.getElementById('lookup-scan-button');
- const lookupPingRes = document.getElementById('lookup-ping-results');
- const lookupPingLoader= document.getElementById('lookup-ping-loader');
- const lookupPingOutput= document.getElementById('lookup-ping-output');
- const lookupPingError = document.getElementById('lookup-ping-error');
+
+ // 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 tracerouteOutput = document.querySelector('#traceroute-output pre');
+ 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');
@@ -233,7 +375,7 @@ export const page = {
const portScanStopBtn = document.getElementById('port-scan-stop-btn');
// ── State ────────────────────────────────────────────────────
- let map = null, lookupMap = null, currentIp = null, currentLookupIp = null;
+ let currentIp = null, currentLookupIp = null;
let eventSource = null, portScanEventSource = null;
// ── Helpers ──────────────────────────────────────────────────
@@ -321,13 +463,101 @@ export const page = {
}
// ── Copy buttons ─────────────────────────────────────────────
- setupCopyBtn(copyIpBtn, () => ipAddressSpan.textContent);
+ 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');
@@ -342,6 +572,7 @@ export const page = {
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');
@@ -370,7 +601,7 @@ export const page = {
}
updateField(document.getElementById('asn-org'), data.asn?.organization, asnLoader);
updateRdns(document.getElementById('rdns-list'), data.rdns, rdnsLoader, document.getElementById('rdns-error'));
- map = initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
+ initOrUpdateMap('map', data.geo?.latitude, data.geo?.longitude, mapEl, mapLoader, mapMessage);
} catch (err) {
console.error('Failed to fetch IP info:', err);
@@ -386,8 +617,7 @@ export const page = {
lookupMapLoader.classList.add('hidden');
lookupMapEl.classList.add('hidden');
lookupMapMsg.classList.add('hidden');
- lookupPingRes.classList.add('hidden');
- lookupPingLoader.classList.add('hidden');
+ pingSect.classList.add('hidden');
portScanSection.classList.add('hidden');
portScanOutput.innerHTML = '';
[lookupIpEl, document.getElementById('lookup-country'), document.getElementById('lookup-region'),
@@ -397,8 +627,6 @@ export const page = {
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 = '
-';
- lookupPingOutput.textContent = '';
- lookupPingError.textContent = '';
[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; }
@@ -460,7 +688,7 @@ export const page = {
}
updateField(document.getElementById('lookup-asn-org'), data.asn?.organization);
updateRdns(document.getElementById('lookup-rdns-list'), data.rdns, null, document.getElementById('lookup-rdns-error'));
- lookupMap = initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
+ initOrUpdateMap('lookup-map', data.geo?.latitude, data.geo?.longitude, lookupMapEl, lookupMapLoader, lookupMapMsg);
[lookupPingBtn, lookupTraceBtn, lookupScanBtn].forEach(b => { if (b) b.disabled = false; });
} catch (err) {
@@ -478,25 +706,44 @@ export const page = {
// ── Ping ─────────────────────────────────────────────────────
async function runPing(ip) {
- lookupPingRes.classList.remove('hidden');
- lookupPingLoader.classList.remove('hidden');
- lookupPingOutput.textContent = '';
- lookupPingError.textContent = '';
+ 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}`);
- let out = `--- Ping Statistics for ${ip} ---\n`;
- if (data.stats) {
- out += `Packets: ${data.stats.packets.transmitted} sent, ${data.stats.packets.received} received, ${data.stats.packets.lossPercent}% loss\n`;
- if (data.stats.rtt) out += `RTT (ms): min=${data.stats.rtt.min} avg=${data.stats.rtt.avg} max=${data.stats.rtt.max}\n`;
+
+ 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');
}
- out += `\n--- Raw Output ---\n${data.rawOutput || ''}`;
- lookupPingOutput.textContent = out;
+ 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) {
- lookupPingError.textContent = `Ping Error: ${err.message}`;
+ pingSectErr.textContent = `Ping failed: ${err.message}`;
+ pingSectMsg.textContent = '';
} finally {
- lookupPingLoader.classList.add('hidden');
+ pingSectLoader.classList.add('hidden');
}
}
@@ -504,11 +751,13 @@ export const page = {
function startTraceroute(ip) {
if (eventSource) { eventSource.close(); eventSource = null; }
tracerouteSection.classList.remove('hidden');
- tracerouteOutput.textContent = '';
+ 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)}`);
@@ -531,7 +780,7 @@ export const page = {
});
eventSource.addEventListener('end', e => {
try {
- const d = JSON.parse(e.data);
+ 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;
@@ -552,34 +801,74 @@ export const page = {
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.classList.add('fade-in');
div.textContent = text;
tracerouteOutput.appendChild(div);
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
}
function displayHop(hop) {
- const div = document.createElement('div');
- div.classList.add('hop-line', 'fade-in');
- const num = document.createElement('span'); num.classList.add('hop-number'); num.textContent = hop.hop || '?'; div.appendChild(num);
+ 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 ip = document.createElement('span'); ip.classList.add('hop-ip'); ip.textContent = hop.ip; div.appendChild(ip);
- if (hop.hostname) { const h = document.createElement('span'); h.classList.add('hop-hostname'); h.textContent = ` (${hop.hostname})`; div.appendChild(h); }
+ 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 t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; div.appendChild(t);
+ 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 {
- div.appendChild(document.createTextNode(hop.rawLine || 'Unknown hop'));
+ const line = document.createElement('div');
+ line.classList.add('hop-ip-line', 'text-gray-400');
+ line.textContent = hop.rawLine || 'Unknown hop';
+ body.appendChild(line);
}
- if (Array.isArray(hop.rtt)) {
- hop.rtt.forEach(r => {
- const s = document.createElement('span');
- s.classList.add(r === '*' ? 'hop-timeout' : 'hop-rtt');
- s.textContent = r === '*' ? ' *' : ` ${r} ms`;
- div.appendChild(s);
- });
- }
- tracerouteOutput.appendChild(div);
+
+ row.appendChild(body);
+ tracerouteOutput.appendChild(row);
tracerouteOutput.scrollTop = tracerouteOutput.scrollHeight;
}
@@ -591,6 +880,7 @@ export const page = {
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 = () => {};
@@ -650,6 +940,8 @@ export const page = {
portScanStopBtn.addEventListener('click', stopPortScan);
// ── Bootstrap ────────────────────────────────────────────────
+ populateBrowserFingerprint();
+ checkIPv6();
fetchIpInfo();
const params = new URLSearchParams(search);
diff --git a/frontend/app/shared.css b/frontend/app/shared.css
index e060ebf..6f0061a 100644
--- a/frontend/app/shared.css
+++ b/frontend/app/shared.css
@@ -62,13 +62,16 @@
color: #e5e7eb;
padding: 1rem;
border-radius: 0.375rem;
- max-height: 500px;
+ max-height: 400px;
overflow-y: auto;
- font-size: 0.875rem;
+ font-size: 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
}
+/* ── Tool action buttons (Ping / Traceroute / Port Scan) ──────── */
+.action-tool-btn { text-align: center; }
+
/* ── Scrollbar ─────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: rgba(31, 41, 55, 0.5); }
@@ -230,34 +233,45 @@ header.nav-open #main-nav { display: block; }
#ip-address-link:hover::after { transform: scaleX(1); transform-origin: bottom left; }
/* ── Home page — Traceroute output ─────────────────────────────── */
-#traceroute-output pre, .result-pre {
- white-space: pre-wrap;
- word-break: break-all;
- font-family: 'Courier New', Courier, monospace;
- background-color: rgba(0,0,0,.3);
- color: #e5e7eb;
- padding: 1rem;
- border-radius: 0.375rem;
- max-height: 400px;
- overflow-y: auto;
- font-size: 0.875rem;
- border: 1px solid rgba(255,255,255,.05);
- box-shadow: inset 0 2px 4px 0 rgba(0,0,0,.3);
+/* Hop rows — structured grid layout with RDNS on its own line */
+#traceroute-output .hop-row {
+ display: grid;
+ grid-template-columns: 2rem 1fr;
+ align-items: start;
+ gap: 0 0.5rem;
+ padding: 3px 4px;
+ border-radius: 4px;
+ border-left: 2px solid transparent;
+ transition: border-left-color .2s, background .2s;
}
-#traceroute-output .hop-line { margin-bottom: .25rem; padding-left: .5rem; border-left: 2px solid transparent; transition: border-left-color .3s; }
-#traceroute-output .hop-line:hover { border-left-color: #a855f7; background: rgba(255,255,255,.02); }
-#traceroute-output .hop-number { display: inline-block; width: 30px; text-align: right; margin-right: 15px; color: #6b7280; font-weight: bold; }
-#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; }
-#traceroute-output .hop-hostname { color: #c084fc; }
-#traceroute-output .hop-rtt { color: #34d399; margin-left: 8px; font-size: .85em; }
-#traceroute-output .hop-timeout { color: #f87171; }
-#traceroute-output .info-line { color: #fbbf24; font-style: italic; }
-#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 10px; }
-#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 15px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.1); padding-top: 10px; }
+#traceroute-output .hop-row:hover { border-left-color: #a855f7; background: rgba(255,255,255,.025); }
+#traceroute-output .hop-number { text-align: right; color: #4b5563; font-weight: 700; font-size: .8em; padding-top: 2px; }
+#traceroute-output .hop-body { min-width: 0; }
+#traceroute-output .hop-ip-line { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
+#traceroute-output .hop-ip { color: #60a5fa; font-weight: 500; flex-shrink: 0; }
+#traceroute-output .hop-rtts { display: flex; gap: 0.4rem; margin-left: auto; }
+#traceroute-output .hop-rtt { color: #34d399; font-size: .8em; }
+#traceroute-output .hop-timeout { color: #f87171; font-size: .85em; }
+/* RDNS hostname on its own line — clearly distinguishable from the IP */
+#traceroute-output .hop-rdns {
+ font-size: .75em;
+ color: #c084fc;
+ opacity: .85;
+ margin-top: 1px;
+ padding-left: 1px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Non-hop lines (info, error, end) */
+#traceroute-output .info-line { color: #fbbf24; font-style: italic; font-size: .85em; }
+#traceroute-output .error-line { color: #f87171; font-weight: bold; border-left: 3px solid #f87171; padding-left: 8px; }
+#traceroute-output .end-line { color: #d8b4fe; font-weight: bold; margin-top: 8px; text-transform: uppercase; letter-spacing: .05em; border-top: 1px solid rgba(255,255,255,.08); padding-top: 8px; }
/* ── Home page — Maps ───────────────────────────────────────────── */
-#map { height: 300px; }
-#lookup-map { height: 250px; }
+/* Containers use h-[420px] / h-[260px] in HTML; maps fill 100% via ID selector (higher specificity than Tailwind) */
+#map { height: 420px; }
+#lookup-map { height: 260px; }
/* ── ASN page — Graph ───────────────────────────────────────────── */
#graph-container { width: 100%; height: 600px; background: rgba(0,0,0,.3); border-radius: .75rem; border: 1px solid rgba(255,255,255,.06); overflow: hidden; position: relative; }