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 };