rebase www template

This commit is contained in:
2026-04-27 11:45:33 +02:00
parent 07bc5ffd9f
commit a42f1b87e9
27 changed files with 2574 additions and 4615 deletions
+349
View File
@@ -0,0 +1,349 @@
import { API } from '../shared.js';
export const page = {
title: 'ASN Lookup',
template: () => `
<div class="container mx-auto max-w-6xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-2 text-center text-gradient">AS / ASN Lookup</h1>
<p class="text-center text-gray-400 text-sm mb-8">Peering graph, prefixes &amp; IXP connections for any Autonomous System</p>
<div class="flex flex-col sm:flex-row gap-3 mb-6 max-w-2xl mx-auto">
<input type="text" id="asn-input" placeholder="Enter ASN (e.g. 15169 or AS3320)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="lookup-button" disabled
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200">
Lookup
</button>
</div>
<div id="error-box" class="hidden max-w-2xl mx-auto mb-6 p-4 bg-red-900/30 border border-red-500/40 text-red-300 rounded-lg text-sm"></div>
<div id="loading-section" class="hidden flex flex-col items-center gap-3 py-16">
<div class="loader" style="width:40px;height:40px;border-width:5px;"></div>
<p class="text-gray-400 text-sm" id="loading-msg">Querying RIPE Stat &amp; PeeringDB…</p>
<p class="text-xs text-amber-400/80 bg-amber-400/10 border border-amber-400/20 rounded-lg px-4 py-2 max-w-sm text-center mt-1">
⏳ Large ASes (Cloudflare, Google, Tier-1 carriers) can take up to 15 s on first lookup — subsequent lookups are cached for 7 days.
</p>
</div>
<div id="results-section" class="hidden fade-in">
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center gap-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-1">
<span id="res-asn" class="font-mono text-2xl font-bold text-purple-400"></span>
<span id="res-announced-badge" class="hidden text-xs px-2 py-0.5 bg-green-500/20 border border-green-500/40 text-green-400 rounded-full">Announced</span>
<span id="res-type-badge" class="text-xs px-2 py-0.5 bg-blue-500/20 border border-blue-500/40 text-blue-300 rounded-full"></span>
</div>
<h2 id="res-name" class="text-xl font-semibold text-white mb-1"></h2>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-400 mb-2">
<span id="res-policy-container" class="hidden">Peering Policy: <span id="res-policy" class="text-gray-200"></span></span>
<span id="res-website-container" class="hidden">Website: <a id="res-website" href="#" target="_blank" rel="noopener" class="text-purple-400 hover:text-purple-300 transition-colors"></a></span>
</div>
<div id="res-rich-info" class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-4 pt-4 border-t border-gray-700/50 hidden">
<div id="res-info-type-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Type</div><div id="res-info-type" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
<div id="res-info-scope-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Scope</div><div id="res-info-scope" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
<div id="res-info-traffic-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Traffic</div><div id="res-info-traffic" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
<div id="res-info-ratio-container" class="hidden"><div class="text-xs text-gray-500 uppercase tracking-widest">Ratio</div><div id="res-info-ratio" class="text-sm text-gray-200 capitalize mt-0.5"></div></div>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 text-center">
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3">
<div id="res-upstream-count" class="text-xl font-bold text-blue-400">—</div>
<div class="text-xs text-gray-400 mt-0.5">Upstreams</div>
</div>
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-3">
<div id="res-downstream-count" class="text-xl font-bold text-green-400">—</div>
<div class="text-xs text-gray-400 mt-0.5">Downstreams</div>
</div>
<div class="bg-purple-500/10 border border-purple-500/20 rounded-lg p-3 col-span-2 sm:col-span-1">
<div id="res-prefix-count" class="text-xl font-bold text-purple-400">—</div>
<div class="text-xs text-gray-400 mt-0.5">Prefixes</div>
</div>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-6 mb-6">
<div class="flex items-center gap-3 mb-4">
<h3 class="text-lg font-bold text-purple-300">Network Map</h3>
<div class="flex gap-3 text-xs text-gray-400">
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-gray-500"></span>Tier-1</span>
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-blue-500"></span>Upstream</span>
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-purple-500"></span>This AS</span>
<span class="flex items-center gap-1"><span class="inline-block w-3 h-3 rounded-full bg-green-500"></span>Downstream</span>
</div>
<span class="ml-auto text-xs text-gray-500">Scroll to zoom · Drag to pan · Click node to open</span>
</div>
<div id="graph-container">
<svg id="graph-svg"></svg>
<div id="graph-tooltip"></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="glass-card rounded-xl p-5">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest">Announced Prefixes</h3>
<button id="prefix-toggle" class="text-xs text-purple-400 hover:text-purple-300 transition-colors">Show all</button>
</div>
<div id="prefix-list" class="max-h-48 overflow-y-auto"></div>
<p id="prefix-empty" class="hidden text-sm text-gray-500 italic">No prefix data available.</p>
</div>
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">IXP Presence <span class="text-xs font-normal text-gray-500">(via PeeringDB)</span></h3>
<div id="ixp-list" class="space-y-1 text-sm max-h-48 overflow-y-auto">
<p id="ixp-empty" class="text-gray-500 italic text-sm">Not listed on PeeringDB.</p>
</div>
</div>
</div>
<div class="glass-card rounded-xl p-5">
<h3 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-3">Direct Neighbours <span class="text-xs font-normal text-gray-500">(Level 2 · via RIPE Stat)</span></h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-xs font-semibold text-blue-400 mb-2">↑ Upstreams (Transit Providers)</h4>
<div id="upstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
<div>
<h4 class="text-xs font-semibold text-green-400 mb-2">↓ Downstreams (Customers)</h4>
<div id="downstream-table" class="space-y-1 text-xs font-mono max-h-52 overflow-y-auto"></div>
</div>
</div>
</div>
</div>
</div>`,
async init(search) {
const asnInput = document.getElementById('asn-input');
const lookupButton = document.getElementById('lookup-button');
const errorBox = document.getElementById('error-box');
const loadingSection = document.getElementById('loading-section');
const resultsSection = document.getElementById('results-section');
let currentData = null;
let showAllPrefixes = false;
const syncBtn = () => { lookupButton.disabled = !asnInput.value.trim(); };
asnInput.addEventListener('input', syncBtn);
function showError(msg) {
errorBox.textContent = msg;
errorBox.classList.toggle('hidden', !msg);
loadingSection.classList.add('hidden');
resultsSection.classList.add('hidden');
}
function setLoading(msg) {
errorBox.classList.add('hidden');
document.getElementById('loading-msg').textContent = msg;
loadingSection.classList.remove('hidden');
resultsSection.classList.add('hidden');
}
async function doLookup(rawAsn) {
const asn = String(rawAsn || '').trim().toUpperCase().replace(/^AS/, '');
if (!asn || isNaN(Number(asn))) { showError('Please enter a valid ASN (e.g. 15169 or AS3320).'); return; }
setLoading('Querying RIPE Stat & PeeringDB…');
const url = new URL(location.href);
url.searchParams.set('asn', asn);
history.replaceState({}, '', url);
try {
const res = await fetch(`${API}/asn-lookup?asn=${encodeURIComponent(asn)}`);
const data = await res.json();
if (!res.ok || !data.success) { showError(data.error || `Request failed (HTTP ${res.status})`); return; }
currentData = data;
renderResults(data);
} catch (err) {
showError(`Network error: ${err.message}`);
}
}
function renderResults(data) {
loadingSection.classList.add('hidden');
resultsSection.classList.remove('hidden');
document.getElementById('res-asn').textContent = `AS${data.asn}`;
document.getElementById('res-name').textContent = data.name || 'Unknown';
const announcedBadge = document.getElementById('res-announced-badge');
announcedBadge.classList.toggle('hidden', !data.announced);
const typeBadge = document.getElementById('res-type-badge');
typeBadge.textContent = data.type || '';
typeBadge.classList.toggle('hidden', !data.type);
const policyContainer = document.getElementById('res-policy-container');
const policyEl = document.getElementById('res-policy');
if (data.peeringdb?.peeringPolicy) {
policyEl.textContent = data.peeringdb.peeringPolicy;
policyContainer.classList.remove('hidden');
} else { policyContainer.classList.add('hidden'); }
const websiteContainer = document.getElementById('res-website-container');
const websiteEl = document.getElementById('res-website');
if (data.peeringdb?.website) {
websiteEl.href = data.peeringdb.website;
websiteEl.textContent = data.peeringdb.website.replace(/^https?:\/\//, '').replace(/\/$/, '');
websiteContainer.classList.remove('hidden');
} else { websiteContainer.classList.add('hidden'); }
const richInfo = document.getElementById('res-rich-info');
let hasRich = false;
[['type', data.peeringdb?.infoType], ['scope', data.peeringdb?.infoScope],
['traffic', data.peeringdb?.infoTraffic], ['ratio', data.peeringdb?.infoRatio]].forEach(([id, val]) => {
const c = document.getElementById(`res-info-${id}-container`);
const e = document.getElementById(`res-info-${id}`);
if (c && e) { if (val) { e.textContent = val; c.classList.remove('hidden'); hasRich = true; } else c.classList.add('hidden'); }
});
richInfo.classList.toggle('hidden', !hasRich);
document.getElementById('res-upstream-count').textContent = data.graph?.level2?.upstreams?.length ?? '?';
document.getElementById('res-downstream-count').textContent = data.graph?.level2?.downstreams?.length ?? '?';
document.getElementById('res-prefix-count').textContent = data.prefixes?.length ?? '?';
renderPrefixes(data.prefixes);
renderIxps(data.peeringdb?.ixps);
renderNeighbourTable('upstream-table', data.graph?.level2?.upstreams ?? [], 'blue');
renderNeighbourTable('downstream-table', data.graph?.level2?.downstreams ?? [], 'green');
if (data.graph) renderGraph(data.graph);
}
function renderPrefixes(prefixes) {
const list = document.getElementById('prefix-list');
const empty = document.getElementById('prefix-empty');
const toggle = document.getElementById('prefix-toggle');
if (!prefixes?.length) { list.classList.add('hidden'); empty.classList.remove('hidden'); toggle.classList.add('hidden'); return; }
empty.classList.add('hidden'); toggle.classList.remove('hidden');
const toShow = showAllPrefixes ? prefixes : prefixes.slice(0, 20);
list.innerHTML = toShow.map(p => `<span class="prefix-tag">${p}</span>`).join('');
toggle.textContent = showAllPrefixes ? 'Show less' : `Show all (${prefixes.length})`;
}
document.getElementById('prefix-toggle').addEventListener('click', () => {
showAllPrefixes = !showAllPrefixes;
if (currentData) renderPrefixes(currentData.prefixes);
});
function renderIxps(ixps) {
const list = document.getElementById('ixp-list');
const empty = document.getElementById('ixp-empty');
if (!ixps?.length) { list.innerHTML = ''; empty.classList.remove('hidden'); return; }
empty.classList.add('hidden');
list.innerHTML = ixps.map(ix => `
<div class="ixp-row py-1.5 flex items-center justify-between gap-2 text-sm">
<span class="text-gray-200 truncate">${ix.name}</span>
<span class="text-xs text-gray-500 shrink-0">${ix.speed >= 1000 ? ix.speed / 1000 + 'G' : ix.speed + 'M'}</span>
</div>`).join('');
}
function renderNeighbourTable(elId, nodes, colour) {
const el = document.getElementById(elId);
if (!nodes?.length) { el.innerHTML = '<p class="text-gray-500 italic">None reported.</p>'; return; }
const col = colour === 'blue' ? 'text-blue-400' : 'text-green-400';
el.innerHTML = nodes.map(n => `
<div class="flex items-center gap-2 py-0.5 hover:bg-white/5 rounded px-1 cursor-pointer group"
onclick="window._router.navigate('/asn',{asn:'${n.asn}'})">
<span class="${col} font-bold w-14 shrink-0">AS${n.asn}</span>
<span class="text-gray-300 truncate flex-1 group-hover:text-white">${n.name || '—'}</span>
<span class="text-gray-600 shrink-0">${n.power ? 'pwr:' + n.power : ''}</span>
</div>`).join('');
}
function renderGraph(graph) {
const container = document.getElementById('graph-container');
const svg = d3.select('#graph-svg');
svg.selectAll('*').remove();
const W = container.clientWidth, H = container.clientHeight;
const nodeMap = new Map();
function addNode(asn, name, role) {
const key = String(asn);
if (!nodeMap.has(key)) nodeMap.set(key, { id: key, asn, name: name || `AS${asn}`, role });
}
addNode(graph.center.asn, graph.center.name, 'center');
const vizUp = graph.level2.upstreams.slice(0, 15);
const vizDown = graph.level2.downstreams.slice(0, 15);
vizUp.forEach(n => addNode(n.asn, n.name, 'upstream'));
vizDown.forEach(n => addNode(n.asn, n.name, 'downstream'));
graph.level3.forEach(d => d.upstreams.forEach(n => addNode(n.asn, n.name, 'tier1')));
const nodes = Array.from(nodeMap.values());
const links = [];
const cid = String(graph.center.asn);
vizUp.forEach(n => links.push({ source: String(n.asn), target: cid, type: 'upstream', power: n.power || 1 }));
vizDown.forEach(n => links.push({ source: cid, target: String(n.asn), type: 'downstream', power: n.power || 1 }));
graph.level3.forEach(d => d.upstreams.forEach(n => links.push({ source: String(n.asn), target: String(d.parentAsn), type: 'tier1', power: n.power || 1 })));
const uniqueLinks = Array.from(new Map(links.map(l => [`${l.source}-${l.target}`, l])).values());
const layerX = { tier1: W * 0.08, upstream: W * 0.3, center: W * 0.55, downstream: W * 0.8 };
nodes.forEach(n => { n.fx = layerX[n.role] ?? W / 2; });
const maxPow = Math.max(...uniqueLinks.map(l => l.power), 1);
const strokeSc = d3.scaleLinear().domain([0, maxPow]).range([0.5, 4]);
const nodeRadius = { center: 20, upstream: 11, downstream: 11, tier1: 8 };
const sim = d3.forceSimulation(nodes)
.force('link', d3.forceLink(uniqueLinks).id(d => d.id).distance(d => d.type === 'tier1' ? 90 : 120).strength(0.6))
.force('charge', d3.forceManyBody().strength(-220))
.force('y', d3.forceY(H / 2).strength(0.04))
.force('collide', d3.forceCollide().radius(d => nodeRadius[d.role] + 14))
.alphaDecay(0.025);
const g = svg.append('g');
svg.call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', evt => g.attr('transform', evt.transform)));
const link = g.append('g').selectAll('line').data(uniqueLinks).join('line')
.attr('class', d => `link link-${d.type}`)
.attr('stroke-width', d => strokeSc(d.power));
const tooltip = document.getElementById('graph-tooltip');
const node = g.append('g').selectAll('g').data(nodes).join('g')
.attr('class', d => `node node-${d.role}`)
.style('cursor', 'pointer')
.on('click', (_, d) => { if (d.role !== 'center') window._router.navigate('/asn', { asn: d.asn }); })
.on('mouseenter', (_, d) => {
tooltip.style.opacity = '1';
tooltip.innerHTML = `<strong class="text-purple-300">AS${d.asn}</strong><br><span class="text-gray-300">${d.name}</span><br><span class="text-gray-500 text-xs capitalize">${d.role === 'tier1' ? 'Tier-1 / Transit' : d.role}</span>`;
})
.on('mousemove', evt => {
const r = container.getBoundingClientRect();
let x = evt.clientX - r.left + 14, y = evt.clientY - r.top - 10;
if (x + 230 > W) x -= 244;
tooltip.style.left = x + 'px'; tooltip.style.top = y + 'px';
})
.on('mouseleave', () => { tooltip.style.opacity = '0'; })
.call(d3.drag()
.on('start', (evt, d) => { if (!evt.active) sim.alphaTarget(0.3).restart(); d.fy = d.y; })
.on('drag', (evt, d) => { d.fy = evt.y; })
.on('end', (evt, d) => { if (!evt.active) sim.alphaTarget(0); d.fy = null; }));
node.append('circle').attr('r', d => nodeRadius[d.role]);
node.append('text').attr('dy', d => nodeRadius[d.role] + 13).attr('font-size', d => d.role === 'center' ? 12 : 9).text(d => `AS${d.asn}`);
node.append('text').attr('dy', d => nodeRadius[d.role] + 23)
.attr('font-size', d => d.role === 'tier1' ? 7 : 8)
.attr('fill', d => d.role === 'tier1' ? '#6b7280' : '#9ca3af')
.text(d => {
if (!d.name) return '';
const max = d.role === 'center' ? 22 : d.role === 'tier1' ? 12 : 16;
return d.name.length > max ? d.name.slice(0, max) + '…' : d.name;
});
sim.on('tick', () => {
nodes.forEach(n => { n.y = Math.max(30, Math.min(H - 30, n.y)); });
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
}
lookupButton.addEventListener('click', () => doLookup(asnInput.value));
asnInput.addEventListener('keypress', e => { if (e.key === 'Enter' && !lookupButton.disabled) doLookup(asnInput.value); });
const params = new URLSearchParams(search);
const urlAsn = params.get('asn');
if (urlAsn) { asnInput.value = urlAsn; syncBtn(); doLookup(urlAsn); }
}
};
+102
View File
@@ -0,0 +1,102 @@
import { API, setupCopyBtn, showError } from '../shared.js';
export const page = {
title: 'DNS Lookup',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient">DNS Lookup</h1>
<div class="p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="dns-domain-input" placeholder="Enter domain (e.g., google.com)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<select id="dns-type-select"
class="px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 cursor-pointer">
<option value="ANY">ANY</option><option value="A">A</option><option value="AAAA">AAAA</option>
<option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option>
<option value="CNAME">CNAME</option><option value="SOA">SOA</option><option value="SRV">SRV</option>
<option value="PTR">PTR (Reverse)</option>
</select>
<button id="dns-lookup-button" disabled
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200">
Lookup DNS
</button>
</div>
<div id="dns-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<div id="dns-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-purple-300 flex items-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
DNS Results for: <span id="dns-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<button id="copy-dns-btn" class="copy-btn">copy</button>
</div>
<div id="dns-lookup-loader" class="loader hidden mb-4"></div>
<pre id="dns-lookup-output" class="result-pre"></pre>
</div>
</div>
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
</div>`,
async init(search) {
const input = document.getElementById('dns-domain-input');
const select = document.getElementById('dns-type-select');
const btn = document.getElementById('dns-lookup-button');
const errorEl = document.getElementById('dns-lookup-error');
const section = document.getElementById('dns-lookup-results-section');
const queryEl = document.getElementById('dns-lookup-query');
const loader = document.getElementById('dns-lookup-loader');
const output = document.getElementById('dns-lookup-output');
const copyBtn = document.getElementById('copy-dns-btn');
const syncBtn = () => { btn.disabled = !input.value.trim(); };
input.addEventListener('input', syncBtn);
setupCopyBtn(copyBtn, () => output.textContent);
async function doLookup() {
const domain = input.value.trim();
const type = select.value;
if (!domain) return;
const url = new URL(location.href);
url.searchParams.set('domain', domain);
url.searchParams.set('type', type);
history.replaceState({}, '', url);
showError(errorEl, null);
section.classList.remove('hidden');
loader.classList.remove('hidden');
output.textContent = '';
queryEl.textContent = `${domain} (${type})`;
try {
const r = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(domain)}&type=${encodeURIComponent(type)}`);
const data = await r.json();
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
output.textContent = JSON.stringify(data.records, null, 2);
} catch (err) {
showError(errorEl, err.message);
output.textContent = '';
} finally {
loader.classList.add('hidden');
}
}
btn.addEventListener('click', doLookup);
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
const params = new URLSearchParams(search);
const d = params.get('domain');
if (d) {
input.value = d;
const t = params.get('type');
if (t) select.value = t;
syncBtn();
doLookup();
}
}
};
+671
View File
@@ -0,0 +1,671 @@
import { API, setupCopyBtn } from '../shared.js';
export const page = {
title: 'IP Info & Tools',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient glitch-text">Your Digital Footprint</h1>
<!-- Own IP info -->
<div id="info-section" class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<!-- Left column -->
<div class="space-y-6 fade-in" style="animation-delay:.1s">
<div class="glass-card rounded-lg p-5 relative overflow-hidden group">
<div class="absolute top-0 right-0 p-2 opacity-10 group-hover:opacity-20 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
</div>
<h2 class="text-xs font-bold text-purple-400 uppercase tracking-widest mb-2">Your Public IP</h2>
<div id="ip-info" class="min-h-[40px] flex items-center gap-2">
<div id="ip-loader" class="loader"></div>
<a id="ip-address-link" href="#" class="text-3xl font-mono font-bold text-white tracking-tight break-all hidden hover:text-purple-300 transition-colors" title="Click for WHOIS Lookup">
<span id="ip-address"></span>
</a>
<button id="copy-ip-btn" class="copy-btn hidden">copy</button>
</div>
</div>
<div class="glass-card rounded-lg p-5 space-y-4">
<div>
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Location Details</h2>
<div id="geo-info" class="min-h-[80px] space-y-1 text-sm text-gray-300">
<div id="geo-loader" class="loader"></div>
<div class="hidden grid grid-cols-2 gap-x-2 gap-y-1">
<p><span class="text-gray-500">Country:</span> <span id="country" class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">Region:</span> <span id="region" class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="city" class="text-gray-200 font-medium">-</span></p>
<p><span class="text-gray-500">Zip:</span> <span id="postal" class="text-gray-200 font-medium">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="coords" class="font-mono text-xs text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="timezone" class="text-gray-200 font-medium">-</span></p>
<p id="geo-error" class="text-red-400 col-span-2 text-xs"></p>
</div>
</div>
</div>
<div>
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Network (ASN)</h2>
<div id="asn-info" class="min-h-[40px] space-y-1 text-sm text-gray-300">
<div id="asn-loader" class="loader"></div>
<div class="hidden">
<p><span class="text-gray-500">AS Number:</span> <span id="asn-number" class="font-mono text-purple-300">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="asn-org" class="font-medium text-white">-</span></p>
<p id="asn-error" class="text-red-400 text-xs"></p>
</div>
</div>
</div>
</div>
<div class="glass-card rounded-lg p-5">
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-2 border-b border-gray-700 pb-1">Hostname (rDNS)</h2>
<div id="rdns-info" class="min-h-[30px] text-sm text-gray-300">
<div id="rdns-loader" class="loader"></div>
<div class="hidden">
<ul id="rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
<p id="rdns-error" class="text-red-400 text-xs"></p>
</div>
</div>
</div>
</div>
<!-- Right column: map -->
<div class="space-y-4 fade-in" style="animation-delay:.2s">
<h2 class="text-lg font-semibold text-gray-200 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"/>
</svg>
Location Visualization
</h2>
<div id="map-container" class="bg-gray-800/50 rounded-lg min-h-[400px] h-full flex items-center justify-center relative border border-gray-700/50 shadow-inner overflow-hidden">
<div id="map-loader" class="loader absolute z-10"></div>
<div id="map" class="w-full h-full rounded-lg hidden z-0 opacity-80 hover:opacity-100 transition-opacity duration-700"></div>
<p id="map-message" class="text-gray-400 hidden absolute text-sm">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none rounded-lg ring-1 ring-inset ring-white/10"></div>
</div>
</div>
</div>
<!-- IP Lookup -->
<div class="mt-8 p-6 glass-card rounded-xl">
<h2 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-400 to-pink-500 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-pink-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
IP Address / Domain Lookup
</h2>
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="lookup-ip-input" placeholder="Enter IP or Domain (e.g., 8.8.8.8 or google.com)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="lookup-button" disabled
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200">
Lookup
</button>
</div>
<div id="lookup-error" class="text-red-400 mb-4 hidden p-3 bg-red-900/20 border border-red-500/30 rounded text-sm"></div>
<div id="lookup-results-section" class="hidden grid grid-cols-1 md:grid-cols-2 gap-8 mt-6 border-t border-gray-700/50 pt-6 fade-in">
<div class="space-y-6">
<h3 class="text-lg font-semibold text-gray-200">Result for: <span id="lookup-ip-address" class="font-mono text-purple-400 bg-purple-500/10 px-2 py-0.5 rounded"></span>
<button id="copy-lookup-ip-btn" class="copy-btn ml-2">copy</button>
</h3>
<div id="lookup-result-loader" class="loader hidden"></div>
<div id="lookup-geo-info" class="space-y-1 text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Geolocation</h4>
<div class="grid grid-cols-2 gap-x-2 gap-y-1">
<p><span class="text-gray-500">Country:</span> <span id="lookup-country" class="text-white">-</span></p>
<p><span class="text-gray-500">Region:</span> <span id="lookup-region" class="text-white">-</span></p>
<p><span class="text-gray-500">City:</span> <span id="lookup-city" class="text-white">-</span></p>
<p><span class="text-gray-500">Zip:</span> <span id="lookup-postal" class="text-white">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Coords:</span> <span id="lookup-coords" class="font-mono text-purple-300">-</span></p>
<p class="col-span-2"><span class="text-gray-500">Time:</span> <span id="lookup-timezone" class="text-white">-</span></p>
<p id="lookup-geo-error" class="text-red-400 col-span-2"></p>
</div>
</div>
<div id="lookup-asn-info" class="space-y-1 text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">ASN</h4>
<p><span class="text-gray-500">Number:</span> <span id="lookup-asn-number" class="text-white font-mono">-</span></p>
<p><span class="text-gray-500">Org:</span> <span id="lookup-asn-org" class="text-white">-</span></p>
<p id="lookup-asn-error" class="text-red-400"></p>
</div>
<div id="lookup-rdns-info" class="space-y-1 text-sm text-gray-300">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2 mt-4">Reverse DNS</h4>
<ul id="lookup-rdns-list" class="list-none space-y-1 font-mono text-xs text-green-400"><li>-</li></ul>
<p id="lookup-rdns-error" class="text-red-400"></p>
</div>
</div>
<div class="space-y-6">
<h4 class="font-bold text-gray-500 uppercase text-xs tracking-wider mb-2">Location Map</h4>
<div id="lookup-map-container" class="glass-panel rounded-lg min-h-[250px] flex items-center justify-center relative overflow-hidden">
<div id="lookup-map-loader" class="loader hidden absolute z-10"></div>
<div id="lookup-map" class="w-full rounded hidden opacity-90"></div>
<p id="lookup-map-message" class="text-gray-400 hidden absolute">Could not load map.</p>
<div class="absolute inset-0 pointer-events-none ring-1 ring-inset ring-white/10 rounded-lg"></div>
</div>
<div class="flex flex-wrap gap-2 pt-2">
<button id="lookup-ping-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Ping</button>
<button id="lookup-trace-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Trace</button>
<button id="lookup-scan-button" disabled class="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-gray-600 hover:border-gray-500 shadow-md">Port Scan</button>
</div>
<div id="lookup-ping-results" class="mt-4 text-sm hidden fade-in">
<h4 class="font-bold text-purple-400 mb-2 flex items-center gap-2">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> Ping Results
</h4>
<div id="lookup-ping-loader" class="loader hidden"></div>
<pre id="lookup-ping-output" class="result-pre mt-1"></pre>
<p id="lookup-ping-error" class="text-red-400 mt-2"></p>
</div>
</div>
</div>
</div>
<!-- Traceroute -->
<div id="traceroute-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Traceroute Results</h2>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3 text-sm">
<div id="traceroute-loader" class="loader hidden"></div>
<span id="traceroute-message" class="text-gray-300"></span>
</div>
<button id="traceroute-stop-btn" class="stop-btn hidden">■ Stop</button>
</div>
<div id="traceroute-output" class="rounded-lg overflow-hidden"><pre class="m-0"></pre></div>
</div>
<!-- Port Scan -->
<div id="port-scan-section" class="mt-8 p-6 glass-card rounded-xl hidden fade-in">
<h2 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4">Port Scan Results</h2>
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3 text-sm">
<div id="port-scan-loader" class="loader hidden"></div>
<span id="port-scan-message" class="text-gray-300"></span>
</div>
<button id="port-scan-stop-btn" class="stop-btn hidden">■ Stop</button>
</div>
<div id="port-scan-output" class="text-sm font-mono bg-gray-900/50 p-4 rounded-lg border border-gray-700/50 max-h-[300px] overflow-y-auto"></div>
</div>
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
</div>`,
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');
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');
const tracerouteSection = document.getElementById('traceroute-section');
const tracerouteOutput = document.querySelector('#traceroute-output pre');
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 map = null, lookupMap = null, 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 = '<li>No rDNS records found.</li>';
} else if (data?.error) {
if (listEl) listEl.innerHTML = '<li>-</li>';
if (errorEl) errorEl.textContent = data.error;
} else {
if (listEl) listEl.innerHTML = '<li>-</li>';
}
}
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>',
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);
// ── Lookup button enable/disable ─────────────────────────────
const syncLookupBtn = () => { lookupBtn.disabled = !lookupInput.value.trim(); };
lookupInput.addEventListener('input', syncLookupBtn);
// ── 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;
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 =
`<a href="/asn?asn=${asnNum}" class="hover:text-purple-200 underline decoration-dotted transition-colors" title="Open ASN Lookup">AS${asnNum}</a>`;
} 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'));
map = 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');
lookupPingRes.classList.add('hidden');
lookupPingLoader.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 = '<li>-</li>';
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; }
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=A`);
const data = await r.json();
if (r.ok && data.success && data.records?.length) {
ipToLookup = Array.isArray(data.records) ? data.records[0] : data.records;
} else {
const r2 = await fetch(`${API}/dns-lookup?domain=${encodeURIComponent(query)}&type=AAAA`);
const data2 = await r2.json();
if (r2.ok && data2.success && data2.records?.length) {
ipToLookup = Array.isArray(data2.records) ? data2.records[0] : data2.records;
} else {
throw new Error(data.error || 'No A or AAAA records found.');
}
}
} 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 =
`<a href="/asn?asn=${data.asn.number}" class="text-purple-400 hover:text-purple-300 underline decoration-dotted transition-colors font-mono">AS${data.asn.number}</a>`;
} 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'));
lookupMap = 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) {
lookupPingRes.classList.remove('hidden');
lookupPingLoader.classList.remove('hidden');
lookupPingOutput.textContent = '';
lookupPingError.textContent = '';
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`;
}
out += `\n--- Raw Output ---\n${data.rawOutput || ''}`;
lookupPingOutput.textContent = out;
} catch (err) {
lookupPingError.textContent = `Ping Error: ${err.message}`;
} finally {
lookupPingLoader.classList.add('hidden');
}
}
// ── Traceroute ───────────────────────────────────────────────
function startTraceroute(ip) {
if (eventSource) { eventSource.close(); eventSource = null; }
tracerouteSection.classList.remove('hidden');
tracerouteOutput.textContent = '';
tracerouteLoader.classList.remove('hidden');
tracerouteStopBtn.classList.remove('hidden');
tracerouteMessage.textContent = `Starting traceroute to ${ip}`;
globalError.classList.add('hidden');
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');
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);
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); }
} else if (hop.rtt?.every(r => r === '*')) {
const t = document.createElement('span'); t.classList.add('hop-timeout'); t.textContent = '* * *'; div.appendChild(t);
} else {
div.appendChild(document.createTextNode(hop.rawLine || 'Unknown hop'));
}
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);
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}`;
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 = `<span class="text-red-400">Error: ${data.error}</span>`;
} 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 <span class="font-bold w-12 inline-block">${data.port}</span> <span class="w-24 inline-block">(${data.service})</span>: <span class="font-bold ${col}">${lbl}</span>`;
}
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 ────────────────────────────────────────────────
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; }
};
}
};
+76
View File
@@ -0,0 +1,76 @@
import { API, showError } from '../shared.js';
export const page = {
title: 'MAC Vendor Lookup',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient">MAC Address Vendor Lookup</h1>
<div class="p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="mac-input" placeholder="Enter MAC address (e.g., 00:1A:2B:3C:4D:5E)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="mac-lookup-button" disabled
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200">
Find Vendor
</button>
</div>
<div id="mac-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<div id="mac-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<h3 class="text-lg font-semibold text-purple-300 mb-4 flex items-center justify-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
Vendor for: <span id="mac-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<div id="mac-lookup-loader" class="loader hidden mb-4 mx-auto"></div>
<pre id="mac-lookup-output" class="result-pre text-center text-xl"></pre>
</div>
</div>
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
</div>`,
async init(search) {
const input = document.getElementById('mac-input');
const btn = document.getElementById('mac-lookup-button');
const errorEl = document.getElementById('mac-lookup-error');
const section = document.getElementById('mac-lookup-results-section');
const queryEl = document.getElementById('mac-lookup-query');
const loader = document.getElementById('mac-lookup-loader');
const output = document.getElementById('mac-lookup-output');
const syncBtn = () => { btn.disabled = !input.value.trim(); };
input.addEventListener('input', syncBtn);
async function doLookup() {
const mac = input.value.trim();
if (!mac) return;
showError(errorEl, null);
section.classList.remove('hidden');
loader.classList.remove('hidden');
output.textContent = '';
queryEl.textContent = mac;
try {
const r = await fetch(`${API}/mac-lookup?mac=${encodeURIComponent(mac)}`);
const data = await r.json();
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
output.textContent = data.vendor || 'No vendor found.';
} catch (err) {
showError(errorEl, err.message);
output.textContent = '';
} finally {
loader.classList.add('hidden');
}
}
btn.addEventListener('click', doLookup);
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
const params = new URLSearchParams(search);
const m = params.get('mac');
if (m) { input.value = m; syncBtn(); doLookup(); }
}
};
+205
View File
@@ -0,0 +1,205 @@
export const page = {
title: 'Subnetz Rechner',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h2 class="text-3xl font-bold mb-8 text-center text-gradient">IP Subnetz Rechner</h2>
<form id="subnet-form" class="mb-8 glass-card p-6 rounded-xl">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div>
<label for="ip-address" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">IP Adresse:</label>
<input type="text" id="ip-address" name="ip-address" placeholder="z.B. 192.168.1.1" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
</div>
<div>
<label for="cidr" class="block text-gray-400 text-sm font-bold mb-2 uppercase tracking-wide">CIDR / Maske:</label>
<input type="text" id="cidr" name="cidr" placeholder="z.B. 24 oder 255.255.255.0" required
class="w-full px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all">
</div>
</div>
<div id="subnet-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<button type="submit"
class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white font-bold py-3 px-6 rounded-lg shadow-lg hover:shadow-purple-500/25 transition-all duration-200 ease-in-out transform hover:-translate-y-0.5">
Berechnen
</button>
</form>
<div id="results" class="glass-card rounded-xl p-6 hidden fade-in">
<h3 class="text-xl font-bold text-purple-300 border-b border-purple-500/30 pb-2 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Ergebnisse:
</h3>
<div class="space-y-3 text-sm">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Netzwerkadresse:</span>
<span id="network-address" class="font-mono text-white font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Broadcast-Adresse:</span>
<span id="broadcast-address" class="font-mono text-purple-400 font-semibold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Subnetzmaske:</span>
<span id="subnet-mask" class="font-mono text-gray-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Anzahl der Hosts:</span>
<span id="host-count" class="font-mono text-green-400 font-bold">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 border-b border-gray-700/50 pb-2">
<span class="text-gray-400">Erste Host-Adresse:</span>
<span id="first-host" class="font-mono text-blue-300">-</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<span class="text-gray-400">Letzte Host-Adresse:</span>
<span id="last-host" class="font-mono text-blue-300">-</span>
</div>
</div>
</div>
<!-- Example subnets -->
<div class="glass-card rounded-xl p-6 mt-8">
<h3 class="text-lg font-bold text-gray-400 uppercase tracking-wider border-b border-gray-700/50 pb-2 mb-4">
Beispiel-Subnetze (Private Adressbereiche)
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-left text-gray-400">
<thead class="text-xs uppercase bg-gray-800/50 text-gray-200">
<tr>
<th class="px-6 py-3">Bereich</th>
<th class="px-6 py-3">CIDR</th>
<th class="px-6 py-3">Subnetzmaske</th>
<th class="px-6 py-3">Beschreibung</th>
<th class="px-6 py-3">Aktion</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700/50">
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">192.168.0.0 192.168.255.255</td>
<td class="px-6 py-4 font-mono">/16 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.255.0.0</td>
<td class="px-6 py-4">Klasse C (oft als /24 genutzt)</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="192.168.1.1" data-cidr="24">Beispiel /24</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">172.16.0.0 172.31.255.255</td>
<td class="px-6 py-4 font-mono">/12 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.240.0.0</td>
<td class="px-6 py-4">Klasse B</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="172.16.10.5" data-cidr="16">Beispiel /16</span></td>
</tr>
<tr class="hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 font-mono text-white">10.0.0.0 10.255.255.255</td>
<td class="px-6 py-4 font-mono">/8 (Gesamt)</td>
<td class="px-6 py-4 font-mono">255.0.0.0</td>
<td class="px-6 py-4">Klasse A</td>
<td class="px-6 py-4"><span class="example-link text-purple-400 hover:text-purple-300 cursor-pointer underline" data-ip="10.0.50.100" data-cidr="8">Beispiel /8</span></td>
</tr>
</tbody>
</table>
</div>
<p class="mt-4 text-xs text-gray-500 italic">Klicken Sie auf "Beispiel", um die Felder auszufüllen und die Berechnung zu starten.</p>
</div>
</div>`,
init() {
const form = document.getElementById('subnet-form');
const ipInput = document.getElementById('ip-address');
const cidrInput = document.getElementById('cidr');
const errorEl = document.getElementById('subnet-error');
const resultsEl = document.getElementById('results');
function showInlineError(msg) {
errorEl.textContent = msg;
errorEl.classList.toggle('hidden', !msg);
}
function isValidIP(ip) {
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(ip);
}
function ipToBinary(ip) {
return ip.split('.').map(o => parseInt(o, 10).toString(2).padStart(8, '0')).join('');
}
function binaryToIp(b) {
const parts = [];
for (let i = 0; i < 32; i += 8) parts.push(parseInt(b.slice(i, i + 8), 2));
return parts.join('.');
}
function cidrToMask(cidr) {
return binaryToIp('1'.repeat(cidr) + '0'.repeat(32 - cidr));
}
function maskToCidr(mask) {
const b = ipToBinary(mask);
if (/^1*0*$/.test(b)) return b.replace(/0+$/, '').length;
return null;
}
function calculate() {
showInlineError(null);
const ip = ipInput.value.trim();
const cidrRaw = cidrInput.value.trim();
if (!isValidIP(ip)) { showInlineError('Bitte eine gültige IPv4-Adresse eingeben.'); return; }
let cidr, mask;
if (cidrRaw.includes('.')) {
if (!isValidIP(cidrRaw)) { showInlineError('Bitte eine gültige Subnetzmaske eingeben.'); return; }
cidr = maskToCidr(cidrRaw);
if (cidr === null) { showInlineError('Ungültige Subnetzmaske — muss eine kontinuierliche Folge von Einsen sein (z.B. 255.255.255.0).'); return; }
mask = cidrRaw;
} else {
cidr = parseInt(cidrRaw, 10);
if (isNaN(cidr) || cidr < 0 || cidr > 32) { showInlineError('Bitte einen gültigen CIDR-Wert (032) eingeben.'); return; }
mask = cidrToMask(cidr);
}
const ipBin = ipToBinary(ip);
const maskBin = '1'.repeat(cidr) + '0'.repeat(32 - cidr);
let netBin = '';
for (let i = 0; i < 32; i++) netBin += (parseInt(ipBin[i]) & parseInt(maskBin[i])).toString();
const hostBits = 32 - cidr;
const bcBin = netBin.slice(0, cidr) + '1'.repeat(hostBits);
const netNum = parseInt(netBin, 2);
const bcNum = parseInt(bcBin, 2);
let hosts, first, last;
if (hostBits >= 2) {
hosts = Math.pow(2, hostBits) - 2;
first = binaryToIp((netNum + 1).toString(2).padStart(32, '0'));
last = binaryToIp((bcNum - 1).toString(2).padStart(32, '0'));
} else if (cidr === 31) {
hosts = 2; first = binaryToIp(netBin); last = binaryToIp(bcBin);
} else {
hosts = 1; first = binaryToIp(netBin); last = binaryToIp(netBin);
}
document.getElementById('network-address').textContent = binaryToIp(netBin);
document.getElementById('broadcast-address').textContent = binaryToIp(bcBin);
document.getElementById('subnet-mask').textContent = mask;
document.getElementById('host-count').textContent = hosts.toLocaleString();
document.getElementById('first-host').textContent = first;
document.getElementById('last-host').textContent = last;
resultsEl.classList.remove('hidden');
}
form.addEventListener('submit', e => { e.preventDefault(); calculate(); });
document.querySelectorAll('.example-link').forEach(link => {
link.addEventListener('click', () => {
ipInput.value = link.dataset.ip;
cidrInput.value = link.dataset.cidr;
calculate();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
}
};
+88
View File
@@ -0,0 +1,88 @@
import { API, setupCopyBtn, showError } from '../shared.js';
export const page = {
title: 'WHOIS Lookup',
template: () => `
<div class="container mx-auto max-w-5xl glass-panel rounded-xl shadow-2xl p-6 md:p-8 backdrop-blur-xl border border-gray-800/50">
<h1 class="text-3xl font-bold mb-8 text-center text-gradient">WHOIS Lookup</h1>
<div class="p-6 glass-card rounded-xl">
<div class="flex flex-col sm:flex-row gap-3 mb-6">
<input type="text" id="whois-query-input" placeholder="Enter domain or IP (e.g., google.com or 8.8.8.8)"
class="flex-grow px-4 py-3 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono transition-all placeholder-gray-600">
<button id="whois-lookup-button" disabled
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200">
Lookup WHOIS
</button>
</div>
<div id="whois-lookup-error" class="hidden mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-400 text-sm"></div>
<div id="whois-lookup-results-section" class="hidden mt-6 border-t border-gray-700/50 pt-6 fade-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-purple-300 flex items-center gap-2">
<div class="w-1.5 h-6 bg-purple-500 rounded-full"></div>
WHOIS Results for: <span id="whois-lookup-query" class="font-mono text-purple-400 ml-1"></span>
</h3>
<button id="copy-whois-btn" class="copy-btn">copy</button>
</div>
<div id="whois-lookup-loader" class="loader hidden mb-4"></div>
<pre id="whois-lookup-output" class="result-pre"></pre>
</div>
</div>
<div id="global-error" class="mt-6 p-4 bg-red-900/50 border border-red-500/50 text-red-100 rounded-lg hidden backdrop-blur shadow-lg"></div>
</div>`,
async init(search) {
const input = document.getElementById('whois-query-input');
const btn = document.getElementById('whois-lookup-button');
const errorEl = document.getElementById('whois-lookup-error');
const section = document.getElementById('whois-lookup-results-section');
const queryEl = document.getElementById('whois-lookup-query');
const loader = document.getElementById('whois-lookup-loader');
const output = document.getElementById('whois-lookup-output');
const copyBtn = document.getElementById('copy-whois-btn');
const syncBtn = () => { btn.disabled = !input.value.trim(); };
input.addEventListener('input', syncBtn);
setupCopyBtn(copyBtn, () => output.textContent);
async function doLookup() {
const query = input.value.trim();
if (!query) return;
const url = new URL(location.href);
url.searchParams.set('query', query);
history.replaceState({}, '', url);
showError(errorEl, null);
section.classList.remove('hidden');
loader.classList.remove('hidden');
output.textContent = '';
queryEl.textContent = query;
try {
const r = await fetch(`${API}/whois-lookup?query=${encodeURIComponent(query)}`);
const data = await r.json();
if (!r.ok || !data.success) throw new Error(data.error || `HTTP ${r.status}`);
output.textContent = typeof data.result === 'string'
? data.result
: JSON.stringify(data.result, null, 2);
} catch (err) {
showError(errorEl, err.message);
output.textContent = '';
} finally {
loader.classList.add('hidden');
}
}
btn.addEventListener('click', doLookup);
input.addEventListener('keypress', e => { if (e.key === 'Enter' && !btn.disabled) doLookup(); });
const params = new URLSearchParams(search);
const q = params.get('query');
if (q) { input.value = q; syncBtn(); doLookup(); }
}
};