PeerCortex/public/aspa-adoption.html
Rene Fichtmueller 5554c1a53e feat: BGP Hijack Alerting + Webhooks (Feature 1)
- Deterministic Classification: MOAS/HIJACK/LEAK type detection
- Severity scoring: CRITICAL/HIGH/MEDIUM/LOW based on prefix length
- Optional Ollama enrichment (qwen2.5:3b) for CRITICAL only (5s timeout)
- PostgreSQL backend: hijack_events, webhook_subscriptions, webhook_deliveries
- HMAC-SHA256 webhook signing with exponential backoff retry
- Retry scheduler: node-cron job every 5 minutes
- 6 API endpoints: POST/GET/DELETE webhooks, test delivery, list/resolve hijacks
- 22 comprehensive tests (80%+ coverage)
- Zero external API costs (deterministic + local Ollama only)
2026-04-29 07:45:15 +02:00

503 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ASPA Adoption Tracker - PeerCortex</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 14px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.stat-label {
font-size: 12px;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-change {
font-size: 12px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
.change-up {
color: #4ade80;
}
.change-down {
color: #f87171;
}
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
margin-bottom: 30px;
}
.chart-section {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.chart-title {
color: #333;
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.chart-container {
position: relative;
height: 300px;
}
.region-table,
.ixp-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.region-table th,
.ixp-table th {
background: #f5f5f5;
color: #333;
font-weight: 600;
padding: 12px;
text-align: left;
font-size: 12px;
border-bottom: 2px solid #ddd;
}
.region-table td,
.ixp-table td {
padding: 12px;
border-bottom: 1px solid #eee;
font-size: 14px;
}
.region-table tr:hover,
.ixp-table tr:hover {
background: #f9f9f9;
}
.coverage-bar {
background: #e5e7eb;
height: 6px;
border-radius: 3px;
overflow: hidden;
}
.coverage-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 3px;
}
.forecast-box {
background: #f0f9ff;
border-left: 4px solid #3b82f6;
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.forecast-value {
font-size: 20px;
font-weight: bold;
color: #1e40af;
}
.forecast-label {
font-size: 12px;
color: #1e40af;
margin-top: 5px;
}
.confidence-badge {
display: inline-block;
background: #dbeafe;
color: #1e40af;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
margin-left: 8px;
}
footer {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
color: #666;
font-size: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
background: #fee;
color: #c33;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
main {
grid-template-columns: 1fr;
}
h1 {
font-size: 20px;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🌐 ASPA Adoption Tracker</h1>
<p class="subtitle">Global BGP ASPA (Autonomous System Provider Authorization) adoption trends</p>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Current Coverage</div>
<div class="stat-value" id="current-coverage">-</div>
<div class="stat-change">
<span id="change-24h" class="change-up">+0.5%</span> (24h)
</div>
</div>
<div class="stat-card">
<div class="stat-label">Trend</div>
<div class="stat-value" id="trend-indicator"></div>
<div class="stat-change" id="trend-strength">Moderate growth</div>
</div>
<div class="stat-card">
<div class="stat-label">6-Month Forecast</div>
<div class="stat-value" id="forecast-coverage">-</div>
<div class="stat-change">
<span id="forecast-confidence">-</span> confidence
</div>
</div>
<div class="stat-card">
<div class="stat-label">Data Points</div>
<div class="stat-value" id="data-points">-</div>
<div class="stat-change" id="last-update">Updated -</div>
</div>
</div>
</header>
<main>
<div class="chart-section">
<div class="chart-title">📈 Adoption Trend (30 days)</div>
<div class="chart-container">
<canvas id="adoption-chart"></canvas>
</div>
</div>
<div class="chart-section">
<div class="chart-title">🗺️ Regional Coverage</div>
<table class="region-table" id="regions-table">
<thead>
<tr>
<th>Region</th>
<th>Coverage</th>
<th>ASNs</th>
</tr>
</thead>
<tbody id="regions-body">
<tr>
<td colspan="3" style="text-align: center; color: #999">Loading...</td>
</tr>
</tbody>
</table>
</div>
<div class="chart-section">
<div class="chart-title">🏢 IXP Coverage</div>
<table class="ixp-table" id="ixps-table">
<thead>
<tr>
<th>IXP</th>
<th>Coverage</th>
<th>Participants</th>
</tr>
</thead>
<tbody id="ixps-body">
<tr>
<td colspan="3" style="text-align: center; color: #999">Loading...</td>
</tr>
</tbody>
</table>
</div>
<div class="chart-section">
<div class="chart-title">🎯 Top Adopters</div>
<div style="margin-top: 15px" id="top-adopters-list">
<p style="color: #999">Loading...</p>
</div>
</div>
</main>
<footer>
<p>Last updated: <span id="footer-timestamp">-</span></p>
<p style="margin-top: 10px; color: #999">Data sources: RIPE Stat, PeeringDB, CAIDA. Updated daily at 2:00 AM UTC.</p>
</footer>
</div>
<script>
let adoptionChart = null
async function loadData() {
try {
const response = await fetch('/api/aspa-adoption-stats?period=30d')
if (!response.ok) throw new Error('Failed to load data')
const stats = await response.json()
updateStats(stats)
renderAdoptionChart(stats.trend)
loadRegionalData()
loadIXPData()
} catch (error) {
console.error('Error loading data:', error)
document.body.innerHTML += `<div class="error">Failed to load ASPA adoption data</div>`
}
}
function updateStats(stats) {
document.getElementById('current-coverage').textContent = stats.current.coverage.toFixed(1) + '%'
document.getElementById('change-24h').textContent =
(stats.current.change24h >= 0 ? '+' : '') + stats.current.change24h.toFixed(2) + '%'
document.getElementById('change-24h').className = stats.current.change24h >= 0 ? 'change-up' : 'change-down'
const trendSymbols = { up: '↑', down: '↓', stable: '→' }
document.getElementById('trend-indicator').textContent = trendSymbols[stats.current.trend] || '→'
document.getElementById('trend-strength').textContent =
stats.current.trend.charAt(0).toUpperCase() + stats.current.trend.slice(1) + ' trend'
document.getElementById('forecast-coverage').textContent = stats.forecast.predictedCoverage6m.toFixed(1) + '%'
document.getElementById('forecast-confidence').textContent = (stats.forecast.confidence * 100).toFixed(0) + '%'
document.getElementById('data-points').textContent = stats.trend.length
const lastUpdate = new Date()
document.getElementById('last-update').textContent = 'Updated ' + formatTime(lastUpdate)
document.getElementById('footer-timestamp').textContent = lastUpdate.toLocaleString()
renderTopAdopters(stats.topAdopters)
}
function renderAdoptionChart(trendData) {
const ctx = document.getElementById('adoption-chart').getContext('2d')
if (adoptionChart) {
adoptionChart.destroy()
}
adoptionChart = new Chart(ctx, {
type: 'line',
data: {
labels: trendData.map((p) => p.date),
datasets: [
{
label: 'ASPA Coverage (%)',
data: trendData.map((p) => p.coveragePercentage),
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#667eea',
pointBorderColor: '#fff',
pointBorderWidth: 2,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
},
scales: {
y: {
min: 0,
max: 100,
ticks: { callback: (v) => v + '%' },
},
},
},
})
}
async function loadRegionalData() {
try {
const response = await fetch('/api/aspa-adoption-stats/regional')
if (!response.ok) throw new Error('Failed to load regional data')
const data = await response.json()
const tbody = document.getElementById('regions-body')
tbody.innerHTML = data.regions
.map(
(r) => `
<tr>
<td>${r.region}</td>
<td>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="flex: 1;">
<div class="coverage-bar">
<div class="coverage-fill" style="width: ${r.coveragePercentage}%"></div>
</div>
</div>
<span>${r.coveragePercentage.toFixed(1)}%</span>
</div>
</td>
<td>${r.ASNsWithASPA} / ${r.totalASNs}</td>
</tr>
`
)
.join('')
} catch (error) {
console.error('Error loading regional data:', error)
}
}
async function loadIXPData() {
try {
const response = await fetch('/api/aspa-adoption-stats/ixps?top=5')
if (!response.ok) throw new Error('Failed to load IXP data')
const data = await response.json()
const tbody = document.getElementById('ixps-body')
tbody.innerHTML = data.ixps
.map(
(i) => `
<tr>
<td>${i.ixpName}</td>
<td>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="flex: 1;">
<div class="coverage-bar">
<div class="coverage-fill" style="width: ${i.coveragePercentage}%"></div>
</div>
</div>
<span>${i.coveragePercentage.toFixed(1)}%</span>
</div>
</td>
<td>${i.participantsWithASPA} / ${i.participants}</td>
</tr>
`
)
.join('')
} catch (error) {
console.error('Error loading IXP data:', error)
}
}
function renderTopAdopters(adopters) {
const list = document.getElementById('top-adopters-list')
list.innerHTML = adopters
.slice(0, 5)
.map(
(a, i) => `
<div style="padding: 10px 0; border-bottom: 1px solid #eee;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600;">
<span style="color: #667eea; margin-right: 8px;">#${i + 1}</span>
${a.name}
</span>
<span style="color: #666; font-size: 12px;">${a.providers} provider${a.providers !== 1 ? 's' : ''}</span>
</div>
</div>
`
)
.join('')
}
function formatTime(date) {
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true })
}
document.addEventListener('DOMContentLoaded', loadData)
setInterval(loadData, 5 * 60 * 1000)
</script>
</body>
</html>