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:
parent
fc58394555
commit
cdf21b9e8e
@ -201,6 +201,15 @@ a{color:var(--blue);text-decoration:none;transition:color .2s}a:hover{color:var(
|
|||||||
<div id="rpkiContent"></div>
|
<div id="rpkiContent"></div>
|
||||||
</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) -->
|
<!-- ASPA Intelligence (NEW) -->
|
||||||
<div class="card full" id="aspaCard">
|
<div class="card full" id="aspaCard">
|
||||||
<div class="card-title">
|
<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 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">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"><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">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">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>
|
<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;
|
$('rpkiContent').innerHTML = rk;
|
||||||
|
|
||||||
|
// Atlas Probes
|
||||||
|
renderAtlas(d.atlas);
|
||||||
|
|
||||||
// Neighbours
|
// Neighbours
|
||||||
let ne = '<div class="stat-row">';
|
let ne = '<div class="stat-row">';
|
||||||
ne += '<div class="stat"><div class="stat-val">' + nb.total + '</div><div class="stat-label">Total</div></div>';
|
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;
|
$('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() {
|
async function doCompare() {
|
||||||
if (!currentAsn) return;
|
if (!currentAsn) return;
|
||||||
const raw2 = $('compareAsn').value.trim().replace(/[^0-9]/g, '');
|
const raw2 = $('compareAsn').value.trim().replace(/[^0-9]/g, '');
|
||||||
|
|||||||
23
server.js
23
server.js
@ -383,12 +383,13 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
try {
|
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://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/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/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/as-overview/data.json?resource=AS" + asn),
|
||||||
fetchJSON("https://stat.ripe.net/data/rir-stats-country/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] || {};
|
const net = pdbNet?.data?.[0] || {};
|
||||||
@ -398,6 +399,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const overview = overviewData?.data || {};
|
const overview = overviewData?.data || {};
|
||||||
const rirEntries = rirData?.data?.located_resources || rirData?.data?.rir_stats || [];
|
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)
|
// Phase 2: IX + Facilities + RPKI (batched 20 at a time)
|
||||||
const phase2Promises = [];
|
const phase2Promises = [];
|
||||||
if (netId) {
|
if (netId) {
|
||||||
@ -531,6 +537,21 @@ const server = http.createServer(async (req, res) => {
|
|||||||
total: facilities.length,
|
total: facilities.length,
|
||||||
list: facilities,
|
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));
|
res.end(JSON.stringify(result, null, 2));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user