feat: add company enrichment, ASPA timeout guard, map side panel, OIM telecoms

- /api/enrich: Wikipedia + website meta scraping with redirect following
- ASPA /api/aspa: 18s hard timeout guard + 8s per-call limit
- WHOIS: defensive null check
- Map: replace popups with left side panel
- Map: OIM Telecoms fiber layer (OpenInfraMap vector tiles)
- Map layer toggles: fix source-exists early-return bug
- Provider graph: fix text colors for light background
- Network Health: defensive HTML response check
This commit is contained in:
Rene Fichtmueller 2026-03-30 05:42:38 +02:00
parent df2e176b35
commit 96950992df
3 changed files with 256 additions and 72 deletions

View File

@ -601,14 +601,22 @@ function lookupAspaFromRpki(asn) {
// PeeringDB authenticated fetch helper
function fetchPeeringDB(path, options) {
// PeeringDB semaphore — limits concurrent PDB requests to avoid 429 rate-limits
const pdbSemaphore = new Semaphore(5);
// PeeringDB authenticated fetch helper (throttled via semaphore)
async function fetchPeeringDB(path, options) {
const url = PEERINGDB_API_URL + path;
const headers = { "User-Agent": UA };
if (PEERINGDB_API_KEY) {
headers["Authorization"] = "Api-Key " + PEERINGDB_API_KEY;
}
return fetchJSON(url, { ...options, headers: { ...(options && options.headers || {}), ...headers } });
await pdbSemaphore.acquire();
try {
return await fetchJSON(url, { ...options, headers: { ...(options && options.headers || {}), ...headers } });
} finally {
pdbSemaphore.release();
}
}
// PeeringDB fetch with exponential backoff retries (handles rate-limits under concurrent load).
@ -3507,23 +3515,32 @@ const server = http.createServer(async (req, res) => {
}
}
// 3. Fallback: scrape website meta description
// 3. Fallback: scrape website meta description (follows up to 3 redirects)
function fetchPage(pageUrl, hops) {
if (hops <= 0) return Promise.resolve(null);
return new Promise((resolve) => {
const mod = pageUrl.startsWith("https") ? https : http;
const req = mod.get(pageUrl, { headers: { "User-Agent": UA_SCRAPE }, timeout: 6000 }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume(); // drain to free socket
const next = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, pageUrl).href;
return resolve(fetchPage(next, hops - 1));
}
let data = "";
res.on("data", (c) => { data += c; if (data.length > 40000) { req.destroy(); resolve(data); } });
res.on("end", () => resolve(data));
});
req.on("error", () => resolve(null));
req.on("timeout", () => { req.destroy(); resolve(null); });
});
}
if (!description && website) {
let wsUrl = website;
if (!wsUrl.startsWith("http")) wsUrl = "https://" + wsUrl;
const aboutUrl = wsUrl.replace(/\/$/, "") + "/about";
const tryUrls = [aboutUrl, wsUrl];
for (const tryUrl of tryUrls) {
const resp = await new Promise((resolve) => {
const mod = tryUrl.startsWith("https") ? https : http;
const req = mod.get(tryUrl, { headers: { "User-Agent": UA_SCRAPE }, timeout: 5000 }, (res) => {
let data = "";
res.on("data", (c) => { data += c; if (data.length > 40000) { req.destroy(); resolve(data); } });
res.on("end", () => resolve(data));
});
req.on("error", () => resolve(null));
req.on("timeout", () => { req.destroy(); resolve(null); });
});
const resp = await fetchPage(tryUrl, 3);
if (!resp) continue;
const metaPatterns = [
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']{20,500})["']/i,

View File

@ -508,14 +508,22 @@ a:hover{color:var(--purple)}
<button class="map-layer-btn active" id="layerBtnPops" onclick="toggleMapLayer('pops',this)" style="padding:3px 10px;font-size:.72rem;border-radius:0;border:1px solid var(--cyan);color:var(--cyan);background:rgba(3,105,161,.08);cursor:pointer;font-family:var(--mono)">&#9679; ASN PoPs</button>
<button class="map-layer-btn" id="layerBtnCables" onclick="toggleMapLayer('cables',this)" style="padding:3px 10px;font-size:.72rem;border-radius:0;border:1px solid var(--border);color:var(--muted);background:transparent;cursor:pointer;font-family:var(--mono)">&#9642; Submarine Cables</button>
<button class="map-layer-btn" id="layerBtnGlobalFacs" onclick="toggleMapLayer('globalFacs',this)" style="padding:3px 10px;font-size:.72rem;border-radius:0;border:1px solid var(--border);color:var(--muted);background:transparent;cursor:pointer;font-family:var(--mono)">&#9675; Global Datacenters</button>
<button class="map-layer-btn" id="layerBtnTelecoms" onclick="toggleMapLayer('telecoms',this)" style="padding:3px 10px;font-size:.72rem;border-radius:0;border:1px solid var(--border);color:var(--muted);background:transparent;cursor:pointer;font-family:var(--mono)">&#9644; OIM Telecoms</button>
<span id="mapLoadingIndicator" style="font-size:.7rem;color:var(--muted);margin-left:4px;display:none;font-family:var(--mono)">Loading...</span>
</div>
<div id="networkMap" style="height:500px;border-radius:0;background:#1C1917;position:relative"></div>
<div style="display:flex;gap:0;height:500px">
<div id="mapSidePanel" style="width:220px;flex-shrink:0;background:#111;border-right:1px solid #333;overflow-y:auto;padding:14px 12px;font-family:var(--mono);font-size:.75rem;color:#8892a4;display:flex;flex-direction:column;gap:4px">
<div style="color:#4a5568;font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px">Click point for details</div>
<div id="mapSidePanelContent" style="flex:1"></div>
</div>
<div id="networkMap" style="flex:1;background:#1C1917;position:relative"></div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;font-size:.7rem;color:var(--muted);font-family:var(--mono)">
<span><span style="color:var(--orange)">&#9679;</span> IXP</span>
<span><span style="color:var(--cyan)">&#9679;</span> Datacenter/Facility</span>
<span><span style="color:var(--green)">&#9644;</span> Submarine Cable</span>
<span><span style="color:var(--border-light)">&#9675;</span> Global Datacenter (PeeringDB)</span>
<span><span style="color:#f7ae54">&#9644;</span> OIM Fiber/Telecoms</span>
</div>
</div>
</section>
@ -930,6 +938,7 @@ async function doLookup() {
if (d.ix_presence && d.ix_presence.connections) loadPeeringRecommendations(currentAsn, d.ix_presence.connections, d);
// Load ASPA and bgproutes.io data asynchronously
loadOverviewEnrichment(raw, d.network ? d.network.name : '', d.network ? d.network.website : '');
loadHealthReport(raw);
loadAspaData(raw);
loadAspaVerifyData(raw);
@ -1164,6 +1173,9 @@ function renderDashboard(d) {
if (n.org_name) ov += '<div style="margin-top:.4rem;font-size:.85rem;color:var(--dim)">' + escHtml(n.org_name) + '</div>';
if (n.notes) ov += '<div style="margin-top:.3rem;font-size:.8rem;color:var(--dim);line-height:1.5;max-height:4.5em;overflow:hidden">' + escHtml(n.notes) + '</div>';
// Enrichment placeholder — filled async by loadOverviewEnrichment()
ov += '<div id="overviewEnrich" style="margin-top:.6rem"></div>';
ov += '<div class="ext-links">';
if (n.peeringdb_id) ov += '<a class="ext-link" href="https://www.peeringdb.com/net/' + n.peeringdb_id + '" target="_blank">PeeringDB</a>';
ov += '<a class="ext-link" href="https://bgp.he.net/AS' + n.asn + '" target="_blank">bgp.he.net</a>';
@ -1322,13 +1334,14 @@ function renderDashboard(d) {
// ============================================================
// Global Infrastructure Map (MapLibre GL)
// Layers: ASN PoPs | Submarine Cables | Global Datacenters
// Layers: ASN PoPs | Submarine Cables | Global Datacenters | OIM Telecoms
// ============================================================
var _pcMap = null;
var _pcMapData = null; // current ASN data
var _mapLayers = { pops: true, cables: false, globalFacs: false };
var _mapLayers = { pops: true, cables: false, globalFacs: false, telecoms: false };
var _cablesLoaded = false;
var _globalFacsLoaded = false;
var _telecomsLoaded = false;
function toggleMapLayer(layer, btn) {
_mapLayers[layer] = !_mapLayers[layer];
@ -1350,14 +1363,25 @@ function toggleMapLayer(layer, btn) {
} else if (layer === 'globalFacs') {
if (active && !_globalFacsLoaded) { _loadGlobalFacs(); return; }
if (_pcMap.getLayer('global-facs')) _pcMap.setLayoutProperty('global-facs', 'visibility', active ? 'visible' : 'none');
} else if (layer === 'telecoms') {
if (active && !_telecomsLoaded) { _loadTelecoms(); return; }
['oim-telecoms-line', 'oim-telecoms-dc'].forEach(function(id) {
if (_pcMap.getLayer(id)) _pcMap.setLayoutProperty(id, 'visibility', active ? 'visible' : 'none');
});
}
}
function _layerColor(l) {
return l === 'pops' ? '#7dcfff' : l === 'cables' ? '#9ece6a' : '#bb9af7';
if (l === 'pops') return '#7dcfff';
if (l === 'cables') return '#9ece6a';
if (l === 'globalFacs') return '#bb9af7';
return '#f7ae54'; // telecoms
}
function _layerRgb(l) {
return l === 'pops' ? '125,207,255' : l === 'cables' ? '158,206,106' : '187,154,247';
if (l === 'pops') return '125,207,255';
if (l === 'cables') return '158,206,106';
if (l === 'globalFacs') return '187,154,247';
return '247,174,84'; // telecoms
}
function _showMapLoader(show) {
@ -1365,31 +1389,49 @@ function _showMapLoader(show) {
if (el) el.style.display = show ? 'inline' : 'none';
}
function _showMapPanel(html) {
var el = document.getElementById('mapSidePanelContent');
if (el) el.innerHTML = html;
}
function _mapPanelItem(label, color, title, subtitle) {
return '<div style="border-top:1px solid #222;padding-top:10px;margin-top:6px">' +
'<div style="font-size:.6rem;text-transform:uppercase;letter-spacing:.08em;color:' + color + ';margin-bottom:5px">' + label + '</div>' +
'<div style="font-size:.82rem;font-weight:700;color:#e2e8f0;line-height:1.35;word-break:break-word">' + title + '</div>' +
(subtitle ? '<div style="font-size:.7rem;color:#6b7280;margin-top:4px;line-height:1.3">' + subtitle + '</div>' : '') +
'</div>';
}
function _loadCables() {
_showMapLoader(true);
fetch('/api/submarine-cables').then(function(r) { return r.json(); }).then(function(geo) {
if (!_pcMap) return;
_cablesLoaded = true;
_showMapLoader(false);
if (_pcMap.getSource('cables')) return;
if (_pcMap.getSource('cables')) {
['cables-glow','cables-line'].forEach(function(id) {
if (_pcMap.getLayer(id)) _pcMap.setLayoutProperty(id, 'visibility', 'visible');
});
return;
}
var before = _pcMap.getLayer('pops-fac') ? 'pops-fac' : undefined;
_pcMap.addSource('cables', { type: 'geojson', data: geo });
_pcMap.addLayer({
id: 'cables-glow', type: 'line', source: 'cables',
layout: { 'line-cap': 'round', visibility: 'visible' },
paint: { 'line-color': '#9ece6a', 'line-width': 4, 'line-opacity': 0.08 }
}, 'pops-fac');
paint: { 'line-color': '#9ece6a', 'line-width': 4, 'line-opacity': 0.12 }
}, before);
_pcMap.addLayer({
id: 'cables-line', type: 'line', source: 'cables',
layout: { 'line-cap': 'round', visibility: 'visible' },
paint: { 'line-color': '#9ece6a', 'line-width': 1.2, 'line-opacity': 0.55 }
}, 'pops-fac');
paint: { 'line-color': '#9ece6a', 'line-width': 1.5, 'line-opacity': 0.7 }
}, before);
_pcMap.on('click', 'cables-line', function(e) {
var props = e.features[0].properties;
new maplibregl.Popup({ closeButton: false, className: 'pc-popup' })
.setLngLat(e.lngLat)
.setHTML('<b style="color:#9ece6a">' + (props.name || 'Submarine Cable') + '</b>' +
(props.color ? '<br><span style="color:#8892a4;font-size:11px">Color: ' + props.color + '</span>' : ''))
.addTo(_pcMap);
_showMapPanel(_mapPanelItem('Submarine Cable', '#9ece6a',
escHtml(props.name || 'Unnamed Cable'),
props.color ? 'Color code: ' + escHtml(props.color) : null
));
});
_pcMap.on('mouseenter', 'cables-line', function() { _pcMap.getCanvas().style.cursor = 'pointer'; });
_pcMap.on('mouseleave', 'cables-line', function() { _pcMap.getCanvas().style.cursor = ''; });
@ -1407,24 +1449,111 @@ function _loadGlobalFacs() {
return { type: 'Feature', geometry: { type: 'Point', coordinates: [f.lng, f.lat] },
properties: { name: f.name, city: f.city, country: f.country } };
});
var before = _pcMap.getLayer('pops-fac') ? 'pops-fac' : undefined;
_pcMap.addSource('global-facs', { type: 'geojson', data: { type: 'FeatureCollection', features: features } });
_pcMap.addLayer({
id: 'global-facs', type: 'circle', source: 'global-facs',
layout: { visibility: 'visible' },
paint: { 'circle-radius': 3, 'circle-color': '#bb9af7', 'circle-opacity': 0.5, 'circle-stroke-width': 0.5, 'circle-stroke-color': '#bb9af7' }
}, 'pops-fac');
paint: { 'circle-radius': 3, 'circle-color': '#bb9af7', 'circle-opacity': 0.65, 'circle-stroke-width': 0.5, 'circle-stroke-color': '#bb9af7' }
}, before);
_pcMap.on('click', 'global-facs', function(e) {
var p = e.features[0].properties;
new maplibregl.Popup({ closeButton: false, className: 'pc-popup' })
.setLngLat(e.lngLat)
.setHTML('<b style="color:#bb9af7">' + p.name + '</b><br><span style="font-size:11px;color:#8892a4">' + (p.city || '') + (p.country ? ', ' + p.country : '') + '</span>')
.addTo(_pcMap);
_showMapPanel(_mapPanelItem('Datacenter (PeeringDB)', '#bb9af7',
escHtml(p.name || 'Unnamed'),
[(p.city || ''), (p.country || '')].filter(Boolean).join(', ') || null
));
});
_pcMap.on('mouseenter', 'global-facs', function() { _pcMap.getCanvas().style.cursor = 'pointer'; });
_pcMap.on('mouseleave', 'global-facs', function() { _pcMap.getCanvas().style.cursor = ''; });
}).catch(function() { _showMapLoader(false); _globalFacsLoaded = false; });
}
function _loadTelecoms() {
if (!_pcMap) return;
_telecomsLoaded = true;
_showMapLoader(true);
// Add OpenInfraMap telecoms vector source
if (!_pcMap.getSource('oim-telecoms')) {
_pcMap.addSource('oim-telecoms', {
type: 'vector',
tiles: ['https://openinframap.org/map/telecoms/{z}/{x}/{y}.pbf'],
maxzoom: 17,
attribution: '<a href="https://openinframap.org/copyright" target="_blank">Open Infrastructure Map</a>'
});
}
var before = _pcMap.getLayer('pops-fac') ? 'pops-fac' : undefined;
try {
// Glasfaser-Leitungen (solid, gut sichtbar ab Zoom 2)
if (!_pcMap.getLayer('oim-telecoms-line')) {
_pcMap.addLayer({
id: 'oim-telecoms-line',
type: 'line',
source: 'oim-telecoms',
'source-layer': 'telecoms_communication_line',
minzoom: 2,
layout: { visibility: 'visible', 'line-cap': 'round', 'line-join': 'round' },
paint: {
'line-color': '#f7ae54',
'line-width': ['interpolate', ['linear'], ['zoom'], 2, 1, 6, 1.5, 10, 2.5, 14, 4],
'line-opacity': 0.8
}
}, before);
}
} catch(e) { console.warn('OIM telecoms line layer error:', e); }
try {
// Datacenters als Kreise (ab Zoom 4)
if (!_pcMap.getLayer('oim-telecoms-dc')) {
_pcMap.addLayer({
id: 'oim-telecoms-dc',
type: 'circle',
source: 'oim-telecoms',
'source-layer': 'telecoms_data_center_point',
minzoom: 4,
layout: { visibility: 'visible' },
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 5, 12, 8],
'circle-color': '#f7ae54',
'circle-opacity': 0.85,
'circle-stroke-width': 1.5,
'circle-stroke-color': '#1a1a1a',
'circle-stroke-opacity': 0.6
}
}, before);
}
} catch(e) { console.warn('OIM telecoms dc layer error:', e); }
if (_pcMap.getLayer('oim-telecoms-line')) {
_pcMap.on('click', 'oim-telecoms-line', function(e) {
var p = e.features[0].properties;
_showMapPanel(_mapPanelItem('OIM Glasfaser-Leitung', '#f7ae54',
escHtml(p.name || p.operator || 'Unnamed cable'),
[p.type, p.location].filter(Boolean).join(' · ') || null
));
});
_pcMap.on('mouseenter', 'oim-telecoms-line', function() { _pcMap.getCanvas().style.cursor = 'pointer'; });
_pcMap.on('mouseleave', 'oim-telecoms-line', function() { _pcMap.getCanvas().style.cursor = ''; });
}
if (_pcMap.getLayer('oim-telecoms-dc')) {
_pcMap.on('click', 'oim-telecoms-dc', function(e) {
var p = e.features[0].properties;
_showMapPanel(_mapPanelItem('OIM Telecoms', '#f7ae54',
escHtml(p.name || p.operator || 'Unnamed facility'),
p.type ? p.type.replace(/_/g,' ') : null
));
});
_pcMap.on('mouseenter', 'oim-telecoms-dc', function() { _pcMap.getCanvas().style.cursor = 'pointer'; });
_pcMap.on('mouseleave', 'oim-telecoms-dc', function() { _pcMap.getCanvas().style.cursor = ''; });
}
_showMapLoader(false);
}
function renderNetworkMap(d) {
var mapCard = document.getElementById('mapCard');
var mapDiv = document.getElementById('networkMap');
@ -1461,7 +1590,7 @@ function renderNetworkMap(d) {
mapCard.style.display = 'block';
// Destroy previous map
if (_pcMap) { _pcMap.remove(); _pcMap = null; _cablesLoaded = false; _globalFacsLoaded = false; }
if (_pcMap) { _pcMap.remove(); _pcMap = null; _cablesLoaded = false; _globalFacsLoaded = false; _telecomsLoaded = false; }
setTimeout(function() {
_pcMap = new maplibregl.Map({
@ -1499,14 +1628,10 @@ function renderNetworkMap(d) {
var p = e.features[0].properties;
var color = p.type === 'ix' ? '#ff9e64' : '#7dcfff';
var label = p.type === 'ix' ? 'IXP' : 'Datacenter';
new maplibregl.Popup({ closeButton: false, className: 'pc-popup' })
.setLngLat(e.lngLat)
.setHTML('<div style="font-family:Inter,sans-serif;font-size:12px;min-width:140px">' +
'<span style="font-size:10px;color:' + color + ';text-transform:uppercase;letter-spacing:.05em">' + label + '</span><br>' +
'<b style="color:#e2e8f0">' + p.name + '</b>' +
(p.detail ? '<br><span style="color:#8892a4;font-size:11px">' + p.detail + '</span>' : '') +
'</div>')
.addTo(_pcMap);
_showMapPanel(_mapPanelItem(label, color,
escHtml(p.name || ''),
p.detail ? escHtml(p.detail) : null
));
});
_pcMap.on('mouseenter', layerId, function() { _pcMap.getCanvas().style.cursor = 'pointer'; });
_pcMap.on('mouseleave', layerId, function() { _pcMap.getCanvas().style.cursor = ''; });
@ -1521,6 +1646,7 @@ function renderNetworkMap(d) {
// Restore active overlay layers
if (_mapLayers.cables && !_cablesLoaded) _loadCables();
if (_mapLayers.globalFacs && !_globalFacsLoaded) _loadGlobalFacs();
if (_mapLayers.telecoms && !_telecomsLoaded) _loadTelecoms();
});
}, 50);
}
@ -2142,10 +2268,10 @@ function renderProviderGraph(asn, providers) {
function provCard(p, color, label) {
var n = escHtml(p.name || '');
var f = p.frequency_pct ? p.frequency_pct.toFixed(0) + '%' : '';
return '<div onclick="lookupAsn(' + p.asn + ')" style="cursor:pointer;background:' + color + '08;border:1px solid ' + color + '30;border-radius:10px;padding:.65rem .85rem;display:flex;align-items:center;gap:.65rem;transition:all .15s" onmouseenter="this.style.transform=\'translateY(-1px)\';this.style.borderColor=\'' + color + '\'" onmouseleave="this.style.transform=\'none\';this.style.borderColor=\'' + color + '30\'">' +
'<div style="width:36px;height:36px;border-radius:50%;background:' + color + '18;border:2px solid ' + color + '60;display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.6rem;font-weight:800;color:' + color + '">' + label + '</span></div>' +
'<div style="flex:1;min-width:0"><div style="font-weight:700;font-size:.85rem;color:#e2e8f0">AS' + p.asn + '</div><div style="font-size:.72rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + n + '</div></div>' +
(f ? '<div style="font-size:.72rem;font-weight:700;color:' + color + ';padding:.15rem .45rem;border-radius:5px;background:' + color + '12;flex-shrink:0">' + f + '</div>' : '') +
return '<div onclick="lookupAsn(' + p.asn + ')" style="cursor:pointer;background:#fff;border:1.5px solid ' + color + '55;border-radius:6px;padding:.6rem .85rem;display:flex;align-items:center;gap:.65rem;transition:all .15s;box-shadow:0 1px 3px rgba(0,0,0,.06)" onmouseenter="this.style.transform=\'translateY(-1px)\';this.style.borderColor=\'' + color + '\';this.style.boxShadow=\'0 3px 8px rgba(0,0,0,.1)\'" onmouseleave="this.style.transform=\'none\';this.style.borderColor=\'' + color + '55\';this.style.boxShadow=\'0 1px 3px rgba(0,0,0,.06)\'">' +
'<div style="width:34px;height:34px;border-radius:50%;background:' + color + '20;border:2px solid ' + color + ';display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.58rem;font-weight:800;color:' + color + '">' + label + '</span></div>' +
'<div style="flex:1;min-width:0"><div style="font-weight:700;font-size:.85rem;color:#1a202c;letter-spacing:.01em">AS' + p.asn + '</div><div style="font-size:.72rem;color:#4a5568;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.1rem">' + n + '</div></div>' +
(f ? '<div style="font-size:.72rem;font-weight:700;color:' + color + ';padding:.15rem .45rem;border-radius:4px;background:' + color + '18;flex-shrink:0;border:1px solid ' + color + '40">' + f + '</div>' : '') +
'</div>';
}
@ -2154,7 +2280,7 @@ function renderProviderGraph(asn, providers) {
function section(items, color, title, label, limit) {
if (!items.length) return '';
var s = '<div style="margin-bottom:1.25rem"><div style="font-size:.7rem;font-weight:700;color:' + color + ';text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem;display:flex;align-items:center;gap:.4rem"><span style="width:7px;height:7px;border-radius:50%;background:' + color + '"></span> ' + title + ' (' + items.length + ')</div>';
var s = '<div style="margin-bottom:1.5rem"><div style="font-size:.7rem;font-weight:800;color:' + color + ';text-transform:uppercase;letter-spacing:.1em;margin-bottom:.6rem;padding-bottom:.4rem;border-bottom:1.5px solid ' + color + '30;display:flex;align-items:center;gap:.5rem"><span style="width:8px;height:8px;border-radius:50%;background:' + color + ';flex-shrink:0"></span> ' + title + ' (' + items.length + ')</div>';
s += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.4rem">';
var show = items.slice(0, limit);
show.forEach(function(p) { s += provCard(p, color, label); });
@ -2174,7 +2300,7 @@ function renderProviderGraph(asn, providers) {
h += section(transit, '#60a5fa', 'Transit Providers', 'TR', 12);
h += section(peers, '#4ade80', 'IX / Peers', 'IX', 12);
h += '<div style="font-size:.7rem;color:var(--dim);text-align:center;margin-top:.75rem">' + providers.length + ' providers total (Tier 1: ' + tier1.length + ' \u00b7 Transit: ' + transit.length + ' \u00b7 IX/Peer: ' + peers.length + ')</div>';
h += '<div style="font-size:.72rem;color:#64748b;text-align:center;margin-top:1rem;padding-top:.75rem;border-top:1px solid var(--border)">' + providers.length + ' providers total &nbsp;·&nbsp; Tier 1: ' + tier1.length + ' &nbsp;·&nbsp; Transit: ' + transit.length + ' &nbsp;·&nbsp; IX/Peer: ' + peers.length + '</div>';
$('providerGraphContent').innerHTML = h;
}
@ -2516,18 +2642,42 @@ $('asnInput').addEventListener('keydown', function(e) {
}
})();
async function loadOverviewEnrichment(asn, name, website) {
var el = document.getElementById('overviewEnrich');
if (!el) return;
try {
var url = '/api/enrich?asn=' + asn + '&name=' + encodeURIComponent(name || '');
if (website) url += '&website=' + encodeURIComponent(website);
var resp = await fetch(url);
if (!resp.ok) return;
var text = await resp.text();
if (!text || text[0] === '<') return;
var d = JSON.parse(text);
if (!d.description) return;
var h = '<div style="margin-top:.5rem;padding:.6rem .75rem;background:rgba(0,0,0,.03);border-left:3px solid var(--border);font-size:.82rem;color:var(--text-dim);line-height:1.6;border-radius:0 4px 4px 0">';
h += '<span style="font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);display:block;margin-bottom:.25rem">About</span>';
h += escHtml(d.description);
if (d.wiki_url) h += ' <a href="' + escAttr(d.wiki_url) + '" target="_blank" style="font-size:.72rem;color:var(--blue);white-space:nowrap">Wikipedia →</a>';
h += '</div>';
el.innerHTML = h;
} catch(e) { /* silently fail */ }
}
async function loadHealthReport(asn) {
$('healthContent').innerHTML = '<div class="section-loading">Running comprehensive validation (13 checks)...</div>';
try {
var resp = await fetch('/api/validate?asn=' + asn);
var d = await resp.json();
if (!resp.ok) { $('healthContent').innerHTML = '<div style="color:var(--muted);font-size:.85rem">Health report unavailable (server ' + resp.status + ')</div>'; return; }
var text = await resp.text();
if (!text || text[0] === '<') { $('healthContent').innerHTML = '<div style="color:var(--muted);font-size:.85rem">Health report temporarily unavailable</div>'; return; }
var d = JSON.parse(text);
if (d.error) {
$('healthContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">Validation failed: ' + escHtml(d.error) + '</div>';
return;
}
renderHealthReport(d);
} catch (e) {
$('healthContent').innerHTML = '<div style="color:var(--red);font-size:.85rem">Validation failed: ' + escHtml(e.message) + '</div>';
$('healthContent').innerHTML = '<div style="color:var(--muted);font-size:.85rem">Health report temporarily unavailable</div>';
}
}
@ -3149,15 +3299,15 @@ function termKeydown(e) {
<!-- ─── Terminal Feedback Trigger Button ────────────────────────────── -->
<button id="termBtn" onclick="toggleTerm()" title="Feedback Terminal"
onmouseover="this.style.borderColor='rgba(0,255,65,.7)';this.style.boxShadow='0 4px 24px rgba(0,0,0,.5),0 0 32px rgba(0,255,65,.2)'"
onmouseout="this.style.borderColor='rgba(0,255,65,.35)';this.style.boxShadow='0 4px 20px rgba(0,0,0,.4),0 0 20px rgba(0,255,65,.07)'"
style="position:fixed;bottom:24px;right:24px;width:52px;height:52px;background:rgba(10,10,10,.88);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid rgba(0,255,65,.35);border-radius:6px;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;font-weight:600;letter-spacing:.04em;cursor:pointer;z-index:9999;display:flex;align-items:center;justify-content:center;transition:border-color .2s,box-shadow .2s;box-shadow:0 4px 20px rgba(0,0,0,.4),0 0 20px rgba(0,255,65,.07)">$_</button>
onmouseover="this.style.opacity='1';this.style.borderColor='rgba(0,255,65,.7)';this.style.boxShadow='0 4px 24px rgba(0,0,0,.5),0 0 32px rgba(0,255,65,.2)'"
onmouseout="this.style.opacity='0.25';this.style.borderColor='rgba(0,255,65,.35)';this.style.boxShadow='0 4px 20px rgba(0,0,0,.4),0 0 20px rgba(0,255,65,.07)'"
style="position:fixed;bottom:24px;right:24px;width:52px;height:52px;opacity:0.25;background:rgba(10,10,10,.88);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border:1px solid rgba(0,255,65,.35);border-radius:6px;color:#00ff41;font-family:'IBM Plex Mono','Courier New',monospace;font-size:.82rem;font-weight:600;letter-spacing:.04em;cursor:pointer;z-index:9999;display:flex;align-items:center;justify-content:center;transition:border-color .2s,box-shadow .2s,opacity .4s ease;box-shadow:0 4px 20px rgba(0,0,0,.4),0 0 20px rgba(0,255,65,.07)">$_</button>
<!-- ─── Terminal Feedback Panel ─────────────────────────────────────── -->
<div id="termPanel"
onmouseover="this.style.opacity='0.5'"
onmouseout="this.style.opacity='0.75'"
style="display:none;opacity:0.75;transition:opacity .4s ease;position:fixed;bottom:86px;right:24px;width:500px;background:rgba(10,10,10,.88);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);border:1px solid rgba(0,255,65,.22);border-radius:8px;box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 40px rgba(0,255,65,.05);font-family:'IBM Plex Mono','Courier New',monospace;font-size:.78rem;z-index:10000;flex-direction:column;overflow:hidden">
onmouseover="this.style.opacity='1'"
onmouseout="this.style.opacity='0.25'"
style="display:none;opacity:0.25;transition:opacity .4s ease;position:fixed;bottom:86px;right:24px;width:500px;background:rgba(10,10,10,.88);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);border:1px solid rgba(0,255,65,.22);border-radius:8px;box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 40px rgba(0,255,65,.05);font-family:'IBM Plex Mono','Courier New',monospace;font-size:.78rem;z-index:10000;flex-direction:column;overflow:hidden">
<!-- Title bar -->
<div style="background:rgba(0,0,0,.5);padding:.45rem .8rem;display:flex;align-items:center;gap:.45rem;border-bottom:1px solid rgba(0,255,65,.1);flex-shrink:0">
<span onclick="closeTerm()" style="width:11px;height:11px;border-radius:50%;background:#ff5f56;display:inline-block;cursor:pointer;flex-shrink:0"></span>

View File

@ -601,14 +601,22 @@ function lookupAspaFromRpki(asn) {
// PeeringDB authenticated fetch helper
function fetchPeeringDB(path, options) {
// PeeringDB semaphore — limits concurrent PDB requests to avoid 429 rate-limits
const pdbSemaphore = new Semaphore(5);
// PeeringDB authenticated fetch helper (throttled via semaphore)
async function fetchPeeringDB(path, options) {
const url = PEERINGDB_API_URL + path;
const headers = { "User-Agent": UA };
if (PEERINGDB_API_KEY) {
headers["Authorization"] = "Api-Key " + PEERINGDB_API_KEY;
}
return fetchJSON(url, { ...options, headers: { ...(options && options.headers || {}), ...headers } });
await pdbSemaphore.acquire();
try {
return await fetchJSON(url, { ...options, headers: { ...(options && options.headers || {}), ...headers } });
} finally {
pdbSemaphore.release();
}
}
// PeeringDB fetch with exponential backoff retries (handles rate-limits under concurrent load).
@ -3507,23 +3515,32 @@ const server = http.createServer(async (req, res) => {
}
}
// 3. Fallback: scrape website meta description
// 3. Fallback: scrape website meta description (follows up to 3 redirects)
function fetchPage(pageUrl, hops) {
if (hops <= 0) return Promise.resolve(null);
return new Promise((resolve) => {
const mod = pageUrl.startsWith("https") ? https : http;
const req = mod.get(pageUrl, { headers: { "User-Agent": UA_SCRAPE }, timeout: 6000 }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume(); // drain to free socket
const next = res.headers.location.startsWith("http") ? res.headers.location : new URL(res.headers.location, pageUrl).href;
return resolve(fetchPage(next, hops - 1));
}
let data = "";
res.on("data", (c) => { data += c; if (data.length > 40000) { req.destroy(); resolve(data); } });
res.on("end", () => resolve(data));
});
req.on("error", () => resolve(null));
req.on("timeout", () => { req.destroy(); resolve(null); });
});
}
if (!description && website) {
let wsUrl = website;
if (!wsUrl.startsWith("http")) wsUrl = "https://" + wsUrl;
const aboutUrl = wsUrl.replace(/\/$/, "") + "/about";
const tryUrls = [aboutUrl, wsUrl];
for (const tryUrl of tryUrls) {
const resp = await new Promise((resolve) => {
const mod = tryUrl.startsWith("https") ? https : http;
const req = mod.get(tryUrl, { headers: { "User-Agent": UA_SCRAPE }, timeout: 5000 }, (res) => {
let data = "";
res.on("data", (c) => { data += c; if (data.length > 40000) { req.destroy(); resolve(data); } });
res.on("end", () => resolve(data));
});
req.on("error", () => resolve(null));
req.on("timeout", () => { req.destroy(); resolve(null); });
});
const resp = await fetchPage(tryUrl, 3);
if (!resp) continue;
const metaPatterns = [
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']{20,500})["']/i,