- Fix org→country mapping: pre-cache 20k+ PeeringDB orgs at startup - 22k networks now have country codes (was 0 before) - Add file upload: CSV/TXT/PDF/XLS/DOC → ASN extraction → probe check - Export file results as printable PDF
489 lines
26 KiB
HTML
489 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Lia's Paradise — RIPE Atlas Coverage Explorer</title>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
:root{--bg:#0f0f1a;--card:#1a1b26;--border:#2a2b3d;--border-light:#363750;--text:#e2e8f0;--text-dim:#94a3b8;--muted:#64748b;--dim:#475569;--purple:#a78bfa;--blue:#60a5fa;--green:#4ade80;--orange:#fbbf24;--red:#f87171;--cyan:#22d3ee;--pink:#f472b6}
|
|
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
|
.header{background:linear-gradient(135deg,#1a1b26 0%,#1e1f30 100%);border-bottom:1px solid var(--border);padding:1.5rem 2rem;text-align:center}
|
|
.header h1{font-size:1.8rem;font-weight:800;background:linear-gradient(135deg,var(--pink),var(--purple),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:.25rem}
|
|
.header p{color:var(--text-dim);font-size:.85rem}
|
|
.easter-egg{font-size:.65rem;color:var(--dim);margin-top:.25rem;font-style:italic}
|
|
.controls{max-width:1400px;margin:1.5rem auto;padding:0 1.5rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:center}
|
|
.search-box{flex:1;min-width:200px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.7rem 1rem;font-size:.9rem;color:var(--text);font-family:inherit;outline:none}
|
|
.search-box:focus{border-color:var(--purple)}
|
|
.search-box::placeholder{color:var(--dim)}
|
|
.rir-tabs{display:flex;gap:.4rem;flex-wrap:wrap}
|
|
.rir-tab{padding:.5rem 1rem;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text-dim);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
|
|
.rir-tab:hover{border-color:var(--purple);color:var(--text)}
|
|
.rir-tab.active{background:linear-gradient(135deg,#5b21b6,#7c3aed);color:#fff;border-color:#7c3aed}
|
|
.export-btn{padding:.5rem 1.2rem;border-radius:8px;border:1px solid var(--cyan);background:transparent;color:var(--cyan);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
|
|
.export-btn:hover{background:var(--cyan);color:var(--bg)}
|
|
.stats-bar{max-width:1400px;margin:.75rem auto;padding:0 1.5rem;display:flex;gap:1.5rem;flex-wrap:wrap}
|
|
.stat-pill{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.5rem 1rem;display:flex;align-items:center;gap:.5rem}
|
|
.stat-pill .num{font-size:1.1rem;font-weight:800}
|
|
.stat-pill .label{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
|
|
.content{max-width:1400px;margin:1rem auto;padding:0 1.5rem}
|
|
.loading{text-align:center;padding:3rem;color:var(--muted);font-size:.9rem}
|
|
.loading .spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--purple);border-radius:50%;animation:spin .6s linear infinite;margin-right:.5rem;vertical-align:middle}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.country-section{margin-bottom:1.5rem}
|
|
.country-header{display:flex;align-items:center;gap:.75rem;padding:.6rem 1rem;background:var(--card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .2s;margin-bottom:.5rem}
|
|
.country-header:hover{border-color:var(--purple)}
|
|
.country-flag{font-size:1.2rem}
|
|
.country-name{font-weight:700;font-size:.95rem;flex:1}
|
|
.country-count{font-size:.75rem;color:var(--muted)}
|
|
.country-badge{padding:.2rem .6rem;border-radius:6px;font-size:.7rem;font-weight:700}
|
|
.badge-red{background:#f8717118;color:var(--red);border:1px solid #f8717130}
|
|
.badge-green{background:#4ade8018;color:var(--green);border:1px solid #4ade8030}
|
|
.badge-orange{background:#fbbf2418;color:var(--orange);border:1px solid #fbbf2430}
|
|
.asn-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.4rem;padding:0 0 .5rem 0}
|
|
.asn-row{display:flex;align-items:center;gap:.65rem;padding:.5rem .85rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.8rem;transition:all .15s;cursor:pointer}
|
|
.asn-row:hover{border-color:var(--purple);transform:translateY(-1px)}
|
|
.asn-num{font-weight:700;color:var(--purple);min-width:80px;font-family:'JetBrains Mono',monospace}
|
|
.asn-name{flex:1;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.asn-type{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--card);color:var(--dim);border:1px solid var(--border)}
|
|
.no-probe{color:var(--red);font-size:.65rem;font-weight:600}
|
|
.has-probe{color:var(--green);font-size:.65rem;font-weight:600}
|
|
.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:.75rem;margin-bottom:1.5rem}
|
|
.summary-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;text-align:center}
|
|
.summary-card .rir-name{font-size:.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem}
|
|
.summary-card .big-num{font-size:2rem;font-weight:800}
|
|
.summary-card .sub{font-size:.7rem;color:var(--dim);margin-top:.25rem}
|
|
.progress-bar{height:6px;background:var(--bg);border-radius:3px;margin-top:.5rem;overflow:hidden}
|
|
.progress-fill{height:100%;border-radius:3px;transition:width .5s ease}
|
|
.hidden{display:none}
|
|
.footer{text-align:center;padding:2rem;color:var(--dim);font-size:.7rem;margin-top:2rem;border-top:1px solid var(--border)}
|
|
.footer a{color:var(--purple);text-decoration:none}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Lia's Paradise</h1>
|
|
<p>RIPE Atlas Coverage Explorer — Networks without Atlas Probes, Anchors & Software Probes</p>
|
|
<div class="easter-egg">For Lia, who makes the Internet measurable — one probe at a time</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<input type="text" class="search-box" id="searchInput" placeholder="Search by country, ASN, or network name..." oninput="filterResults()">
|
|
<div class="rir-tabs" id="rirTabs">
|
|
<button class="rir-tab active" onclick="switchRIR('all',this)">All RIRs</button>
|
|
<button class="rir-tab" onclick="switchRIR('ripencc',this)">RIPE NCC</button>
|
|
<button class="rir-tab" onclick="switchRIR('arin',this)">ARIN</button>
|
|
<button class="rir-tab" onclick="switchRIR('apnic',this)">APNIC</button>
|
|
<button class="rir-tab" onclick="switchRIR('lacnic',this)">LACNIC</button>
|
|
<button class="rir-tab" onclick="switchRIR('afrinic',this)">AFRINIC</button>
|
|
</div>
|
|
<button class="export-btn" onclick="exportPDF()">Export PDF</button>
|
|
</div>
|
|
|
|
<div style="max-width:1400px;margin:.75rem auto;padding:0 1.5rem">
|
|
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1rem 1.5rem">
|
|
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
|
<div style="font-size:.8rem;font-weight:600;color:var(--pink)">FILE LOOKUP</div>
|
|
<div style="font-size:.75rem;color:var(--dim)">Upload a file with ASNs or company names — we'll check probe coverage for each</div>
|
|
<div style="flex:1"></div>
|
|
<label style="padding:.45rem 1rem;border-radius:8px;border:1px solid var(--pink);background:transparent;color:var(--pink);font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem" onmouseenter="this.style.background='var(--pink)';this.style.color='var(--bg)'" onmouseleave="this.style.background='transparent';this.style.color='var(--pink)'">
|
|
<span>Upload File</span>
|
|
<input type="file" id="fileUpload" accept=".csv,.txt,.pdf,.xls,.xlsx,.doc,.docx" style="display:none" onchange="handleFileUpload(this)">
|
|
</label>
|
|
<span id="fileStatus" style="font-size:.7rem;color:var(--dim)"></span>
|
|
</div>
|
|
<div id="fileResults" class="hidden" style="margin-top:1rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-bar" id="statsBar"></div>
|
|
|
|
<div id="summaryGrid" class="summary-grid" style="max-width:1400px;margin:1rem auto;padding:0 1.5rem"></div>
|
|
|
|
<div class="content" id="mainContent">
|
|
<div class="loading"><span class="spinner"></span>Loading Atlas coverage data across all RIRs... This may take a moment.</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<a href="/">Back to PeerCortex</a> · Data from <a href="https://atlas.ripe.net" target="_blank">RIPE Atlas</a> & <a href="https://www.peeringdb.com" target="_blank">PeeringDB</a>
|
|
<br>Made with love for the Atlas community
|
|
</div>
|
|
|
|
<script>
|
|
var allData = [];
|
|
var currentRIR = 'all';
|
|
var countryFlags = {};
|
|
|
|
function $(id) { return document.getElementById(id); }
|
|
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
|
|
// Country code to flag emoji
|
|
function flag(cc) {
|
|
if (!cc || cc.length !== 2) return '';
|
|
return String.fromCodePoint(...cc.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65));
|
|
}
|
|
|
|
// RIR mapping by country
|
|
var rirMap = {
|
|
ripencc: new Set(['AT','BE','BG','HR','CY','CZ','DK','EE','FI','FR','DE','GR','HU','IS','IE','IT','LV','LT','LU','MT','NL','NO','PL','PT','RO','SK','SI','ES','SE','CH','GB','UA','RU','TR','GE','AZ','AM','MD','BY','RS','BA','ME','MK','AL','XK','LI','MC','SM','VA','AD','FO','GL','AX','GG','JE','IM','BH','IQ','IR','IL','JO','KW','LB','OM','PS','QA','SA','SY','AE','YE','DZ','EG','LY','MA','TN','EH']),
|
|
arin: new Set(['US','CA','PR','VI','GU','AS','MP','MH','FM','PW','UM']),
|
|
apnic: new Set(['AU','NZ','JP','KR','CN','HK','MO','TW','IN','BD','PK','LK','NP','BT','MV','AF','MM','TH','VN','LA','KH','MY','SG','ID','PH','BN','TL','PG','FJ','WS','TO','VU','SB','KI','NR','TV','MN','KZ','KG','TJ','TM','UZ']),
|
|
lacnic: new Set(['MX','GT','BZ','SV','HN','NI','CR','PA','CO','VE','EC','PE','BO','CL','AR','UY','PY','BR','GY','SR','GF','CU','JM','HT','DO','TT','BB','AG','DM','GD','KN','LC','VC','BS','CW','AW','SX','BQ','TC','KY','BM']),
|
|
afrinic: new Set(['ZA','NG','KE','GH','TZ','UG','ET','RW','SN','CI','CM','CD','CG','GA','AO','MZ','ZW','ZM','MW','BW','NA','SZ','LS','MG','MU','SC','DJ','ER','SO','SD','SS','TD','NE','ML','BF','GW','GN','SL','LR','TG','BJ','CF','GQ']),
|
|
};
|
|
|
|
function getRIR(cc) {
|
|
if (!cc) return 'unknown';
|
|
for (var r in rirMap) { if (rirMap[r].has(cc.toUpperCase())) return r; }
|
|
return 'unknown';
|
|
}
|
|
|
|
async function loadData() {
|
|
try {
|
|
// Fetch PeeringDB networks with IX presence (active networks)
|
|
var resp = await fetch('/api/lia/coverage');
|
|
var d = await resp.json();
|
|
if (d.error) throw new Error(d.error);
|
|
allData = d.networks || [];
|
|
renderAll();
|
|
} catch (e) {
|
|
$('mainContent').innerHTML = '<div class="loading" style="color:var(--red)">Failed to load: ' + escHtml(e.message) + '</div>';
|
|
}
|
|
}
|
|
|
|
function renderAll() {
|
|
renderSummary();
|
|
renderStats();
|
|
renderCountries();
|
|
}
|
|
|
|
function renderSummary() {
|
|
var rir_stats = { ripencc: {total:0,noProbe:0}, arin: {total:0,noProbe:0}, apnic: {total:0,noProbe:0}, lacnic: {total:0,noProbe:0}, afrinic: {total:0,noProbe:0} };
|
|
var rirLabels = { ripencc:'RIPE NCC', arin:'ARIN', apnic:'APNIC', lacnic:'LACNIC', afrinic:'AFRINIC' };
|
|
var rirColors = { ripencc:'var(--blue)', arin:'var(--green)', apnic:'var(--orange)', lacnic:'var(--pink)', afrinic:'var(--cyan)' };
|
|
|
|
allData.forEach(function(n) {
|
|
var r = getRIR(n.country);
|
|
if (rir_stats[r]) {
|
|
rir_stats[r].total++;
|
|
if (!n.has_probe) rir_stats[r].noProbe++;
|
|
}
|
|
});
|
|
|
|
var h = '';
|
|
for (var r in rir_stats) {
|
|
var s = rir_stats[r];
|
|
var covPct = s.total > 0 ? ((s.total - s.noProbe) / s.total * 100).toFixed(1) : '0';
|
|
var col = rirColors[r];
|
|
h += '<div class="summary-card">';
|
|
h += '<div class="rir-name">' + rirLabels[r] + '</div>';
|
|
h += '<div class="big-num" style="color:' + col + '">' + s.noProbe.toLocaleString() + '</div>';
|
|
h += '<div class="sub">of ' + s.total.toLocaleString() + ' networks without a probe</div>';
|
|
h += '<div class="progress-bar"><div class="progress-fill" style="width:' + covPct + '%;background:' + col + '"></div></div>';
|
|
h += '<div class="sub" style="margin-top:.25rem">' + covPct + '% coverage</div>';
|
|
h += '</div>';
|
|
}
|
|
$('summaryGrid').innerHTML = h;
|
|
}
|
|
|
|
function renderStats() {
|
|
var filtered = getFiltered();
|
|
var noProbe = filtered.filter(function(n) { return !n.has_probe; }).length;
|
|
var withProbe = filtered.length - noProbe;
|
|
var countries = new Set(filtered.map(function(n) { return n.country; })).size;
|
|
|
|
var h = '';
|
|
h += '<div class="stat-pill"><span class="num" style="color:var(--red)">' + noProbe.toLocaleString() + '</span><span class="label">Without Probe</span></div>';
|
|
h += '<div class="stat-pill"><span class="num" style="color:var(--green)">' + withProbe.toLocaleString() + '</span><span class="label">With Probe</span></div>';
|
|
h += '<div class="stat-pill"><span class="num" style="color:var(--purple)">' + filtered.length.toLocaleString() + '</span><span class="label">Total Networks</span></div>';
|
|
h += '<div class="stat-pill"><span class="num" style="color:var(--cyan)">' + countries + '</span><span class="label">Countries</span></div>';
|
|
$('statsBar').innerHTML = h;
|
|
}
|
|
|
|
function getFiltered() {
|
|
var q = ($('searchInput').value || '').toLowerCase();
|
|
return allData.filter(function(n) {
|
|
if (currentRIR !== 'all' && getRIR(n.country) !== currentRIR) return false;
|
|
if (q) {
|
|
var haystack = (n.name + ' ' + n.country + ' ' + n.country_name + ' AS' + n.asn + ' ' + (n.info_type || '')).toLowerCase();
|
|
if (haystack.indexOf(q) < 0) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function renderCountries() {
|
|
var filtered = getFiltered();
|
|
|
|
// Group by country
|
|
var byCountry = {};
|
|
filtered.forEach(function(n) {
|
|
var cc = n.country || 'XX';
|
|
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
|
|
byCountry[cc].networks.push(n);
|
|
});
|
|
|
|
// Sort countries by number of networks without probes (descending)
|
|
var countries = Object.keys(byCountry).sort(function(a, b) {
|
|
var aNo = byCountry[a].networks.filter(function(n) { return !n.has_probe; }).length;
|
|
var bNo = byCountry[b].networks.filter(function(n) { return !n.has_probe; }).length;
|
|
return bNo - aNo;
|
|
});
|
|
|
|
var h = '';
|
|
countries.forEach(function(cc) {
|
|
var c = byCountry[cc];
|
|
var noProbe = c.networks.filter(function(n) { return !n.has_probe; });
|
|
var withProbe = c.networks.filter(function(n) { return n.has_probe; });
|
|
var covPct = (withProbe.length / c.networks.length * 100).toFixed(0);
|
|
var badgeClass = covPct >= 70 ? 'badge-green' : covPct >= 30 ? 'badge-orange' : 'badge-red';
|
|
var secId = 'country_' + cc;
|
|
|
|
h += '<div class="country-section">';
|
|
h += '<div class="country-header" onclick="var el=document.getElementById(\'' + secId + '\');el.classList.toggle(\'hidden\');this.querySelector(\'.arrow\').textContent=el.classList.contains(\'hidden\')?\'\u25B6\':\'\u25BC\'">';
|
|
h += '<span class="country-flag">' + flag(cc) + '</span>';
|
|
h += '<span class="country-name">' + escHtml(c.name) + ' (' + cc + ')</span>';
|
|
h += '<span class="country-count">' + noProbe.length + ' without probe / ' + c.networks.length + ' total</span>';
|
|
h += '<span class="' + badgeClass + ' country-badge">' + covPct + '% coverage</span>';
|
|
h += '<span class="arrow" style="color:var(--muted)">\u25B6</span>';
|
|
h += '</div>';
|
|
|
|
h += '<div id="' + secId + '" class="hidden">';
|
|
// Show networks WITHOUT probes first
|
|
if (noProbe.length > 0) {
|
|
h += '<div style="font-size:.7rem;color:var(--red);font-weight:600;margin:.5rem 0 .3rem;padding-left:.5rem">NO PROBE (' + noProbe.length + ')</div>';
|
|
h += '<div class="asn-grid">';
|
|
noProbe.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
|
|
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
|
|
h += '<span class="asn-num">AS' + n.asn + '</span>';
|
|
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
|
|
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
|
|
h += '<span class="no-probe">\u2718 No Probe</span>';
|
|
h += '</div>';
|
|
});
|
|
h += '</div>';
|
|
}
|
|
// Networks WITH probes
|
|
if (withProbe.length > 0) {
|
|
h += '<div style="font-size:.7rem;color:var(--green);font-weight:600;margin:.75rem 0 .3rem;padding-left:.5rem">HAS PROBE (' + withProbe.length + ')</div>';
|
|
h += '<div class="asn-grid">';
|
|
withProbe.sort(function(a,b) { return a.asn - b.asn; }).slice(0, 20).forEach(function(n) {
|
|
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + n.asn + '\',\'_blank\')">';
|
|
h += '<span class="asn-num">AS' + n.asn + '</span>';
|
|
h += '<span class="asn-name">' + escHtml(n.name || '') + '</span>';
|
|
if (n.info_type) h += '<span class="asn-type">' + escHtml(n.info_type) + '</span>';
|
|
h += '<span class="has-probe">\u2714 Probe</span>';
|
|
h += '</div>';
|
|
});
|
|
if (withProbe.length > 20) h += '<div style="font-size:.75rem;color:var(--dim);padding:.3rem .5rem">+ ' + (withProbe.length - 20) + ' more with probes</div>';
|
|
h += '</div>';
|
|
}
|
|
h += '</div>';
|
|
h += '</div>';
|
|
});
|
|
|
|
if (countries.length === 0) {
|
|
h = '<div class="loading">No results found. Try a different search or RIR filter.</div>';
|
|
}
|
|
|
|
$('mainContent').innerHTML = h;
|
|
}
|
|
|
|
function switchRIR(rir, btn) {
|
|
currentRIR = rir;
|
|
document.querySelectorAll('.rir-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
renderStats();
|
|
renderCountries();
|
|
}
|
|
|
|
function filterResults() {
|
|
renderStats();
|
|
renderCountries();
|
|
}
|
|
|
|
function exportPDF() {
|
|
// Generate a printable version
|
|
var w = window.open('', '_blank');
|
|
var filtered = getFiltered().filter(function(n) { return !n.has_probe; });
|
|
var byCountry = {};
|
|
filtered.forEach(function(n) {
|
|
var cc = n.country || 'XX';
|
|
if (!byCountry[cc]) byCountry[cc] = { name: n.country_name || cc, networks: [] };
|
|
byCountry[cc].networks.push(n);
|
|
});
|
|
|
|
var html = '<!DOCTYPE html><html><head><title>Atlas Coverage Report — Lia\'s Paradise</title>';
|
|
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6}h2{color:#7c3aed;margin-top:1.5rem;border-bottom:2px solid #e2e8f0;padding-bottom:.3rem}table{width:100%;border-collapse:collapse;margin:.5rem 0 1rem}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.no-probe{color:#dc2626;font-weight:600}.meta{color:#64748b;font-size:.8rem;margin-bottom:2rem}</style></head><body>';
|
|
html += '<h1>RIPE Atlas Coverage Report</h1>';
|
|
html += '<div class="meta">Generated by Lia\'s Paradise (peercortex.org/lia) on ' + new Date().toISOString().split('T')[0] + '<br>';
|
|
html += 'Filter: ' + (currentRIR === 'all' ? 'All RIRs' : currentRIR.toUpperCase()) + ' | Networks without Atlas Probe: ' + filtered.length + '</div>';
|
|
|
|
Object.keys(byCountry).sort(function(a,b) { return byCountry[b].networks.length - byCountry[a].networks.length; }).forEach(function(cc) {
|
|
var c = byCountry[cc];
|
|
html += '<h2>' + c.name + ' (' + cc + ') — ' + c.networks.length + ' networks</h2>';
|
|
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Type</th><th>Status</th></tr></thead><tbody>';
|
|
c.networks.sort(function(a,b) { return a.asn - b.asn; }).forEach(function(n) {
|
|
html += '<tr><td>AS' + n.asn + '</td><td>' + (n.name || '') + '</td><td>' + (n.info_type || '-') + '</td><td class="no-probe">No Probe</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
});
|
|
|
|
html += '<div class="meta" style="margin-top:3rem">Data from RIPE Atlas & PeeringDB | peercortex.org</div></body></html>';
|
|
w.document.write(html);
|
|
w.document.close();
|
|
w.print();
|
|
}
|
|
|
|
// ============================================================
|
|
// File Upload: Parse ASNs/company names from uploaded files
|
|
// ============================================================
|
|
function handleFileUpload(input) {
|
|
var file = input.files[0];
|
|
if (!file) return;
|
|
var status = $('fileStatus');
|
|
status.textContent = 'Processing ' + file.name + '...';
|
|
status.style.color = 'var(--cyan)';
|
|
|
|
var ext = file.name.split('.').pop().toLowerCase();
|
|
|
|
if (ext === 'csv' || ext === 'txt') {
|
|
file.text().then(function(text) { processFileText(text, file.name); });
|
|
} else if (ext === 'pdf' || ext === 'doc' || ext === 'docx' || ext === 'xls' || ext === 'xlsx') {
|
|
// For binary formats, upload to server for parsing
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
var base64 = e.target.result.split(',')[1];
|
|
fetch('/api/lia/parse-file', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filename: file.name, data: base64 })
|
|
}).then(function(r) { return r.json(); }).then(function(d) {
|
|
if (d.error) {
|
|
status.textContent = 'Error: ' + d.error;
|
|
status.style.color = 'var(--red)';
|
|
return;
|
|
}
|
|
processFileText(d.text || '', file.name);
|
|
}).catch(function(err) {
|
|
// Fallback: try reading as text
|
|
file.text().then(function(text) { processFileText(text, file.name); }).catch(function() {
|
|
status.textContent = 'Cannot parse ' + ext.toUpperCase() + ' files client-side. Use CSV or TXT.';
|
|
status.style.color = 'var(--red)';
|
|
});
|
|
});
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} else {
|
|
status.textContent = 'Unsupported file type: ' + ext;
|
|
status.style.color = 'var(--red)';
|
|
}
|
|
}
|
|
|
|
function processFileText(text, filename) {
|
|
var status = $('fileStatus');
|
|
|
|
// Extract ASNs (AS12345 or just numbers that look like ASNs)
|
|
var asnMatches = text.match(/\bAS?(\d{3,7})\b/gi) || [];
|
|
var asns = new Set();
|
|
asnMatches.forEach(function(m) {
|
|
var num = parseInt(m.replace(/^AS/i, ''));
|
|
if (num >= 100 && num <= 9999999) asns.add(num);
|
|
});
|
|
|
|
// Also try to match company names against our loaded data
|
|
var nameMatches = [];
|
|
if (allData.length > 0) {
|
|
var lines = text.split(/[\n\r,;]+/).map(function(l) { return l.trim().toLowerCase(); }).filter(function(l) { return l.length > 2; });
|
|
lines.forEach(function(line) {
|
|
allData.forEach(function(n) {
|
|
if (n.name && n.name.toLowerCase().indexOf(line) >= 0 && line.length > 4) {
|
|
asns.add(n.asn);
|
|
}
|
|
if (line.indexOf(n.name ? n.name.toLowerCase() : '###') >= 0 && n.name && n.name.length > 4) {
|
|
asns.add(n.asn);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (asns.size === 0) {
|
|
status.textContent = 'No ASNs or matching networks found in ' + filename;
|
|
status.style.color = 'var(--orange)';
|
|
return;
|
|
}
|
|
|
|
// Match against our data
|
|
var results = [];
|
|
var asnArr = Array.from(asns);
|
|
var dataMap = {};
|
|
allData.forEach(function(n) { dataMap[n.asn] = n; });
|
|
|
|
asnArr.forEach(function(asn) {
|
|
var n = dataMap[asn];
|
|
results.push({
|
|
asn: asn,
|
|
name: n ? n.name : '(Unknown)',
|
|
country: n ? n.country : '',
|
|
info_type: n ? n.info_type : '',
|
|
has_probe: n ? n.has_probe : false,
|
|
in_peeringdb: !!n,
|
|
});
|
|
});
|
|
|
|
results.sort(function(a, b) { return (a.has_probe ? 1 : 0) - (b.has_probe ? 1 : 0); });
|
|
|
|
var withProbe = results.filter(function(r) { return r.has_probe; }).length;
|
|
var noProbe = results.length - withProbe;
|
|
|
|
status.innerHTML = '<span style="color:var(--green)">' + withProbe + ' with probe</span> · <span style="color:var(--red)">' + noProbe + ' without probe</span> · ' + results.length + ' total from ' + escHtml(filename);
|
|
|
|
var h = '<div style="display:flex;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">';
|
|
h += '<button class="export-btn" onclick="exportFileResults()" style="font-size:.7rem;padding:.3rem .8rem">Export Results as PDF</button>';
|
|
h += '</div>';
|
|
h += '<div class="asn-grid">';
|
|
results.forEach(function(r) {
|
|
h += '<div class="asn-row" onclick="window.open(\'/?asn=' + r.asn + '\',\'_blank\')">';
|
|
h += '<span class="asn-num">AS' + r.asn + '</span>';
|
|
h += '<span class="asn-name">' + escHtml(r.name) + '</span>';
|
|
if (r.country) h += '<span style="font-size:.65rem;color:var(--dim)">' + flag(r.country) + ' ' + r.country + '</span>';
|
|
if (r.info_type) h += '<span class="asn-type">' + escHtml(r.info_type) + '</span>';
|
|
h += r.has_probe ? '<span class="has-probe">\u2714 Probe</span>' : '<span class="no-probe">\u2718 No Probe</span>';
|
|
if (!r.in_peeringdb) h += '<span style="font-size:.6rem;color:var(--dim)">(not in PeeringDB)</span>';
|
|
h += '</div>';
|
|
});
|
|
h += '</div>';
|
|
|
|
$('fileResults').innerHTML = h;
|
|
$('fileResults').classList.remove('hidden');
|
|
|
|
// Store for export
|
|
window._fileResults = results;
|
|
window._fileName = filename;
|
|
}
|
|
|
|
function exportFileResults() {
|
|
if (!window._fileResults) return;
|
|
var results = window._fileResults;
|
|
var w = window.open('', '_blank');
|
|
var html = '<!DOCTYPE html><html><head><title>Atlas Probe Check — ' + escHtml(window._fileName) + '</title>';
|
|
html += '<style>body{font-family:Arial,sans-serif;margin:2rem;color:#1a1a2e}h1{color:#5b21b6;font-size:1.3rem}table{width:100%;border-collapse:collapse;margin:1rem 0}th,td{text-align:left;padding:.4rem .6rem;border-bottom:1px solid #e2e8f0;font-size:.85rem}th{background:#f8fafc;font-weight:600}.yes{color:#16a34a;font-weight:700}.no{color:#dc2626;font-weight:700}.meta{color:#64748b;font-size:.8rem}</style></head><body>';
|
|
html += '<h1>Atlas Probe Coverage Check</h1>';
|
|
html += '<div class="meta">Source: ' + escHtml(window._fileName) + ' | Generated: ' + new Date().toISOString().split('T')[0] + ' | peercortex.org/lia</div>';
|
|
html += '<table><thead><tr><th>ASN</th><th>Name</th><th>Country</th><th>Type</th><th>Atlas Probe</th></tr></thead><tbody>';
|
|
results.forEach(function(r) {
|
|
html += '<tr><td>AS' + r.asn + '</td><td>' + escHtml(r.name) + '</td><td>' + (r.country || '-') + '</td><td>' + (r.info_type || '-') + '</td>';
|
|
html += '<td class="' + (r.has_probe ? 'yes' : 'no') + '">' + (r.has_probe ? 'YES' : 'NO') + '</td></tr>';
|
|
});
|
|
html += '</tbody></table></body></html>';
|
|
w.document.write(html);
|
|
w.document.close();
|
|
w.print();
|
|
}
|
|
|
|
// Boot
|
|
loadData();
|
|
</script>
|
|
</body>
|
|
</html>
|