// frontend/app/asn-lookup.js 'use strict'; const API_BASE = '/api'; // ─── State ──────────────────────────────────────────────────────────────────── let currentData = null; let showAllPrefixes = false; // ─── DOM Refs ───────────────────────────────────────────────────────────────── 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 loadingMsg = document.getElementById('loading-msg'); const resultsSection = document.getElementById('results-section'); // ─── Helpers ────────────────────────────────────────────────────────────────── function showError(msg) { errorBox.textContent = msg; errorBox.classList.remove('hidden'); loadingSection.classList.add('hidden'); resultsSection.classList.add('hidden'); if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); } function hideError() { errorBox.classList.add('hidden'); } function setLoading(msg = 'Querying RIPE Stat & PeeringDB…') { hideError(); loadingMsg.textContent = msg; loadingSection.classList.remove('hidden'); resultsSection.classList.add('hidden'); // After 3s show a hint that large ASes can be slow if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); window._loadingHintTimer = setTimeout(() => { const hint = document.getElementById('loading-hint'); if (hint) hint.classList.remove('hidden'); }, 3000); } function updateUrlParam(asn) { const url = new URL(window.location); url.searchParams.set('asn', asn); window.history.pushState({}, '', url); } // ─── Main Lookup ────────────────────────────────────────────────────────────── 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…'); updateUrlParam(asn); try { const res = await fetch(`${API_BASE}/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}`); } } // ─── Render ─────────────────────────────────────────────────────────────────── function renderResults(data) { // Show results FIRST so the graph container has real dimensions (clientWidth > 0) loadingSection.classList.add('hidden'); resultsSection.classList.remove('hidden'); // Reset loading hint for next lookup if (window._loadingHintTimer) clearTimeout(window._loadingHintTimer); const hint = document.getElementById('loading-hint'); if (hint) hint.classList.add('hidden'); // Header document.getElementById('res-asn').textContent = `AS${data.asn}`; document.getElementById('res-name').textContent = data.name || 'Unknown'; const announcedBadge = document.getElementById('res-announced-badge'); if (announcedBadge) { if (data.announced) announcedBadge.classList.remove('hidden'); else announcedBadge.classList.add('hidden'); } const typeBadge = document.getElementById('res-type-badge'); if (typeBadge) { typeBadge.textContent = data.type || ''; typeBadge.classList.toggle('hidden', !data.type); } const peeringPolicy = data.peeringdb?.peeringPolicy; const policyContainer = document.getElementById('res-policy-container'); const policyEl = document.getElementById('res-policy'); if (policyContainer && policyEl) { if (peeringPolicy) { policyEl.textContent = peeringPolicy; policyContainer.classList.remove('hidden'); } else { policyContainer.classList.add('hidden'); } } const website = data.peeringdb?.website; const websiteContainer = document.getElementById('res-website-container'); const websiteEl = document.getElementById('res-website'); if (websiteContainer && websiteEl) { if (website) { websiteEl.href = website; websiteEl.textContent = website.replace(/^https?:\/\//, '').replace(/\/$/, ''); websiteContainer.classList.remove('hidden'); } else { websiteContainer.classList.add('hidden'); } } // Rich Info Grid const richInfo = document.getElementById('res-rich-info'); let hasRichInfo = false; const fields = [ { id: 'type', value: data.peeringdb?.infoType }, { id: 'scope', value: data.peeringdb?.infoScope }, { id: 'traffic', value: data.peeringdb?.infoTraffic }, { id: 'ratio', value: data.peeringdb?.infoRatio } ]; fields.forEach(f => { const container = document.getElementById(`res-info-${f.id}-container`); const el = document.getElementById(`res-info-${f.id}`); if (container && el) { if (f.value) { el.textContent = f.value; container.classList.remove('hidden'); hasRichInfo = true; } else { container.classList.add('hidden'); } } }); if (richInfo) { if (hasRichInfo) richInfo.classList.remove('hidden'); else richInfo.classList.add('hidden'); } 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 ?? '?'; // Prefixes + IXPs (before graph — these are cheap) renderPrefixes(data.prefixes); renderIxps(data.peeringdb?.ixps); renderNeighbourTable('upstream-table', data.graph?.level2?.upstreams ?? [], 'blue'); renderNeighbourTable('downstream-table', data.graph?.level2?.downstreams ?? [], 'green'); // Graph LAST — needs the container to be visible for clientWidth if (data.graph) renderGraph(data.graph); } // ─── Prefix List ───────────────────────────────────────────────────────────── function renderPrefixes(prefixes) { const list = document.getElementById('prefix-list'); const empty = document.getElementById('prefix-empty'); const toggle = document.getElementById('prefix-toggle'); if (!prefixes || prefixes.length === 0) { 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); if (list) list.innerHTML = toShow.map(p => `${p}`).join(''); if (toggle) toggle.textContent = showAllPrefixes ? 'Show less' : `Show all (${prefixes.length})`; } document.getElementById('prefix-toggle').addEventListener('click', () => { showAllPrefixes = !showAllPrefixes; if (currentData) renderPrefixes(currentData.prefixes); }); // ─── IXP List ───────────────────────────────────────────────────────────────── function renderIxps(ixps) { const list = document.getElementById('ixp-list'); const empty = document.getElementById('ixp-empty'); if (!ixps || ixps.length === 0) { if (list) list.innerHTML = ''; empty?.classList.remove('hidden'); return; } empty?.classList.add('hidden'); if (list) { list.innerHTML = ixps.map(ix => `
None reported.
`; return; } const colClass = colour === 'blue' ? 'text-blue-400' : 'text-green-400'; el.innerHTML = nodes.map(n => `