feat: add RIPE Atlas probe integration to dashboard

- Query RIPE Atlas API for probes in the looked-up ASN
- Display probe count, connected/disconnected status, anchors
- Expandable probe detail table with links to atlas.ripe.net
- Connection ratio progress bar
- "Host a probe?" prompt for networks without Atlas presence
This commit is contained in:
Rene Fichtmueller 2026-03-26 11:14:41 +13:00
parent fc58394555
commit cdf21b9e8e
2 changed files with 83 additions and 1 deletions

View File

@ -201,6 +201,15 @@ a{color:var(--blue);text-decoration:none;transition:color .2s}a:hover{color:var(
<div id="rpkiContent"></div>
</div>
<!-- Atlas Probes -->
<div class="card full" id="atlasCard">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>
RIPE Atlas Probes
</div>
<div id="atlasContent"></div>
</div>
<!-- ASPA Intelligence (NEW) -->
<div class="card full" id="aspaCard">
<div class="card-title">
@ -269,6 +278,7 @@ a{color:var(--blue);text-decoration:none;transition:color .2s}a:hover{color:var(
<div class="card full"><div class="card-title">Network Overview</div><div class="skeleton h2"></div><div class="skeleton h3"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card"><div class="card-title">Announced Prefixes</div><div class="skeleton h2"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card"><div class="card-title">RPKI Compliance</div><div class="skeleton h2"></div><div class="skeleton wide"></div></div>
<div class="card full"><div class="card-title">RIPE Atlas Probes</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">ASPA Status</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">bgproutes.io</div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
<div class="card full"><div class="card-title">AS Neighbours</div><div class="skeleton wide"></div><div class="skeleton wide"></div><div class="skeleton med"></div></div>
@ -604,6 +614,9 @@ function renderDashboard(d) {
}
$('rpkiContent').innerHTML = rk;
// Atlas Probes
renderAtlas(d.atlas);
// Neighbours
let ne = '<div class="stat-row">';
ne += '<div class="stat"><div class="stat-val">' + nb.total + '</div><div class="stat-label">Total</div></div>';
@ -665,6 +678,54 @@ function renderDashboard(d) {
$('facContent').innerHTML = fh;
}
function renderAtlas(atlas) {
if (!atlas) {
$('atlasContent').innerHTML = '<div style="font-size:.85rem;color:var(--muted)">No RIPE Atlas data available.</div>';
return;
}
var h = '';
// Summary badges
h += '<div class="stat-row">';
h += '<div class="stat"><div class="stat-val blue">' + atlas.total_probes + '</div><div class="stat-label">Total Probes</div></div>';
h += '<div class="stat"><div class="stat-val green">' + atlas.connected + '</div><div class="stat-label">Connected</div></div>';
h += '<div class="stat"><div class="stat-val red">' + atlas.disconnected + '</div><div class="stat-label">Disconnected</div></div>';
h += '<div class="stat"><div class="stat-val orange">' + atlas.anchors + '</div><div class="stat-label">Anchors</div></div>';
h += '</div>';
if (atlas.total_probes > 0) {
// Connection ratio bar
var connPct = pct(atlas.connected, atlas.total_probes);
h += '<div style="font-size:.7rem;color:var(--muted)">Connected ' + connPct + '% / Disconnected ' + (100 - connPct) + '%</div>';
h += '<div class="progress-multi"><div style="width:' + connPct + '%;background:var(--green)"></div><div style="width:' + (100 - connPct) + '%;background:var(--red)"></div></div>';
}
if (atlas.probes && atlas.probes.length > 0) {
h += '<div class="expand-toggle" onclick="toggleExpand(this)">Show probe details (' + atlas.probes.length + (atlas.total_probes > atlas.probes.length ? ' of ' + atlas.total_probes : '') + ')</div>';
h += '<div class="expand-body"><div class="scroll-wrap"><table class="tbl"><thead><tr><th>ID</th><th>Status</th><th>Anchor</th><th>Country</th><th>Prefix</th><th>Description</th></tr></thead><tbody>';
atlas.probes.forEach(function(p) {
var statusClass = p.status === 'Connected' ? 'badge-green' : 'badge-red';
var anchorBadge = p.is_anchor ? '<span class="badge badge-orange">Anchor</span>' : '-';
var prefix = p.prefix_v4 || p.prefix_v6 || '-';
h += '<tr>';
h += '<td><a href="https://atlas.ripe.net/probes/' + p.id + '/" target="_blank" style="color:var(--blue)">' + p.id + '</a></td>';
h += '<td><span class="badge ' + statusClass + '">' + escHtml(p.status) + '</span></td>';
h += '<td>' + anchorBadge + '</td>';
h += '<td>' + countryFlag(p.country) + ' ' + escHtml(p.country) + '</td>';
h += '<td style="font-family:monospace;font-size:.75rem">' + escHtml(prefix) + '</td>';
h += '<td style="font-size:.75rem">' + escHtml(p.description) + '</td>';
h += '</tr>';
});
h += '</tbody></table></div></div>';
} else if (atlas.total_probes === 0) {
h += '<div style="margin-top:.75rem;font-size:.85rem;color:var(--muted)">No RIPE Atlas probes found for this network. <a href="https://atlas.ripe.net/get-involved/become-a-host/" target="_blank" style="color:var(--blue)">Host a probe?</a></div>';
}
$('atlasContent').innerHTML = h;
}
async function doCompare() {
if (!currentAsn) return;
const raw2 = $('compareAsn').value.trim().replace(/[^0-9]/g, '');

View File

@ -383,12 +383,13 @@ const server = http.createServer(async (req, res) => {
const start = Date.now();
try {
const [pdbNet, prefixData, neighbourData, overviewData, rirData] = await Promise.all([
const [pdbNet, prefixData, neighbourData, overviewData, rirData, atlasProbeData] = await Promise.all([
fetchJSON("https://www.peeringdb.com/api/net?asn=" + asn),
fetchJSON("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + asn),
fetchJSON("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + asn),
fetchJSON("https://stat.ripe.net/data/as-overview/data.json?resource=AS" + asn),
fetchJSON("https://stat.ripe.net/data/rir-stats-country/data.json?resource=AS" + asn),
fetchJSON("https://atlas.ripe.net/api/v2/probes/?asn_v4=" + asn + "&page_size=500"),
]);
const net = pdbNet?.data?.[0] || {};
@ -398,6 +399,11 @@ const server = http.createServer(async (req, res) => {
const overview = overviewData?.data || {};
const rirEntries = rirData?.data?.located_resources || rirData?.data?.rir_stats || [];
// Atlas probes
const atlasProbes = atlasProbeData?.results || [];
const atlasConnected = atlasProbes.filter(p => p.status_name === "Connected");
const atlasAnchors = atlasProbes.filter(p => p.is_anchor === true);
// Phase 2: IX + Facilities + RPKI (batched 20 at a time)
const phase2Promises = [];
if (netId) {
@ -531,6 +537,21 @@ const server = http.createServer(async (req, res) => {
total: facilities.length,
list: facilities,
},
atlas: {
total_probes: atlasProbes.length,
connected: atlasConnected.length,
disconnected: atlasProbes.length - atlasConnected.length,
anchors: atlasAnchors.length,
probes: atlasProbes.slice(0, 100).map(p => ({
id: p.id,
status: p.status_name || p.status || "Unknown",
is_anchor: p.is_anchor || false,
country: p.country_code || "",
prefix_v4: p.prefix_v4 || "",
prefix_v6: p.prefix_v6 || "",
description: p.description || "",
})),
},
};
res.end(JSON.stringify(result, null, 2));