Final Phase C batch: moves the remaining inline route bodies (12 proxy stubs, /changelog, the webhooks/hijacks group, PDF export, ASPA adoption tracker + IPv6 stats endpoints) into server/routes/*.js and server/services/api-proxy.js, verified byte-for-byte via the smoke-test diff. Also drops the now-duplicate proxyToApiServer()/API_SERVER_PORT that were left behind in server.js after api-proxy.js absorbed them. server.js: 6838 -> 578 lines, under the 800-line project limit.
115 lines
4.5 KiB
JavaScript
115 lines
4.5 KiB
JavaScript
const { rpkiAspaMap } = require("../services/rpki");
|
|
const { fetchIpv6AdoptionStats } = require("../ipv6-adoption");
|
|
const {
|
|
aspaAdoptionDailyHistory, getAdoptionTrend, buildAspaAdoptionDashboard,
|
|
} = require("../aspa-adoption/tracker");
|
|
|
|
// GET /aspa-adoption → dashboard HTML
|
|
function handleDashboard(req, res) {
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.writeHead(200);
|
|
return res.end(buildAspaAdoptionDashboard());
|
|
}
|
|
|
|
// Linear regression forecast -- duplicated near-identically in
|
|
// buildAspaAdoptionDashboard (server/aspa-adoption/tracker.js) since both
|
|
// use the same formula over possibly-different trend windows; kept as two
|
|
// call sites rather than merged to avoid changing either's behavior here.
|
|
function forecast(data, daysAhead) {
|
|
if (data.length < 2) return null;
|
|
const n = data.length;
|
|
const xs = data.map((_, i) => i);
|
|
const ys = data.map(d => d.coverage_pct_atlas);
|
|
const xMean = xs.reduce((a,b)=>a+b,0)/n;
|
|
const yMean = ys.reduce((a,b)=>a+b,0)/n;
|
|
const num = xs.reduce((s,x,i)=>s+(x-xMean)*(ys[i]-yMean),0);
|
|
const den = xs.reduce((s,x)=>s+(x-xMean)**2,0);
|
|
if (den===0) return yMean;
|
|
const slope = num/den;
|
|
return Math.max(0, Math.min(100, Math.round((yMean + slope*(n-1+daysAhead))*100)/100));
|
|
}
|
|
|
|
// GET /api/aspa-adoption-stats?period=7d|30d|1y
|
|
function handleStats(req, res, url) {
|
|
const period = ['7d','30d','90d','1y'].includes(url.searchParams.get('period'))
|
|
? url.searchParams.get('period') : '30d';
|
|
const trend = getAdoptionTrend(period);
|
|
const latest = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1];
|
|
const prev = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2];
|
|
|
|
const trend30 = getAdoptionTrend('30d');
|
|
const result = {
|
|
period,
|
|
current: {
|
|
date: latest?.date,
|
|
aspa_objects: latest?.aspa_objects ?? rpkiAspaMap.size,
|
|
roa_count: latest?.roa_count,
|
|
atlas_asns_total: latest?.atlas_asns_total ?? 0,
|
|
atlas_asns_with_aspa: latest?.atlas_asns_with_aspa ?? 0,
|
|
coverage_pct_atlas: latest?.coverage_pct_atlas ?? 0,
|
|
delta_from_previous: prev ? (latest?.aspa_objects ?? 0) - prev.aspa_objects : 0,
|
|
},
|
|
trend: trend.map(s => ({
|
|
date: s.date,
|
|
aspa_objects: s.aspa_objects,
|
|
coverage_pct_atlas: s.coverage_pct_atlas,
|
|
atlas_asns_total: s.atlas_asns_total,
|
|
})),
|
|
forecast: {
|
|
predicted_90d: forecast(trend30, 90),
|
|
confidence: trend30.length >= 10 ? 0.75 : 0.5,
|
|
method: 'linear_regression',
|
|
based_on_samples: trend30.length,
|
|
},
|
|
meta: {
|
|
total_snapshots: aspaAdoptionDailyHistory.length,
|
|
last_updated: latest?.date ?? null,
|
|
denominator: 'atlas_visible_asns',
|
|
source: 'rpki_cloudflare_feed',
|
|
}
|
|
};
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.writeHead(200);
|
|
return res.end(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
// GET /api/aspa-adoption-stats/export?format=csv|json&period=30d
|
|
function handleStatsExport(req, res, url) {
|
|
const fmt = url.searchParams.get('format') === 'csv' ? 'csv' : 'json';
|
|
const period = ['7d','30d','90d','1y'].includes(url.searchParams.get('period'))
|
|
? url.searchParams.get('period') : '30d';
|
|
const data = getAdoptionTrend(period);
|
|
|
|
if (fmt === 'csv') {
|
|
const header = 'date,aspa_objects,roa_count,atlas_asns_total,atlas_asns_with_aspa,coverage_pct_atlas,aspa_delta\n';
|
|
const rows = data.map(s =>
|
|
`${s.date},${s.aspa_objects},${s.roa_count},${s.atlas_asns_total},${s.atlas_asns_with_aspa},${s.coverage_pct_atlas},${s.aspa_delta??0}`
|
|
).join('\n');
|
|
res.setHeader('Content-Type', 'text/csv');
|
|
res.setHeader('Content-Disposition', `attachment; filename="peercortex-aspa-adoption-${period}.csv"`);
|
|
res.writeHead(200);
|
|
return res.end(header + rows);
|
|
}
|
|
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Content-Disposition', `attachment; filename="peercortex-aspa-adoption-${period}.json"`);
|
|
res.writeHead(200);
|
|
return res.end(JSON.stringify({ period, count: data.length, data }, null, 2));
|
|
}
|
|
|
|
// GET /api/ipv6-adoption-stats?refresh=1
|
|
async function handleIpv6Stats(req, res, url) {
|
|
const force = url.searchParams.get('refresh') === '1';
|
|
try {
|
|
const data = await fetchIpv6AdoptionStats(force);
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.writeHead(200);
|
|
return res.end(JSON.stringify(data, null, 2));
|
|
} catch (err) {
|
|
res.writeHead(503);
|
|
return res.end(JSON.stringify({ error: 'IPv6 stats unavailable', message: err.message }));
|
|
}
|
|
}
|
|
|
|
module.exports = { handleDashboard, handleStats, handleStatsExport, handleIpv6Stats };
|