diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 054794b..e0a2e3c 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,3 +23,4 @@ {"d":"2026-04-09","t":"FIX","m":"doLookup: add 15s AbortController on initial fetch — skeleton no longer spins indefinitely on slow/failed lookups"} {"d":"2026-04-09","t":"FIX","m":"aspath/rpki-history/looking-glass/communities: fetchJSONWithRetry with 15-20s timeouts replaced by fetchJSON 5-6s — was causing 40-72s hangs"} {"d":"2026-04-09","t":"FIX","m":"loadCommunities/loadIrrAudit/loadRpkiHistory/loadAspath/loadHijackMonitor: add AbortController 8-10s — cards no longer spin forever"} +{"d":"2026-04-09","t":"FIX","m":"renderResilienceScore + renderRouteLeak: functions were called but never defined — caused JS crash 'is not defined' breaking entire doLookup render"} diff --git a/public/index.html b/public/index.html index df88332..478921f 100644 --- a/public/index.html +++ b/public/index.html @@ -4224,6 +4224,66 @@ function renderContacts(d) { card.classList.remove('hidden'); } +// ── Resilience Score ─────────────────────────────────────────── +function renderResilienceScore(rs) { + const card = document.getElementById('resilienceCard'); + const el = document.getElementById('resilienceContent'); + if (!card || !el || !rs) return; + card.style.display = ''; + const score = rs.score || 0; + const color = score >= 7 ? 'var(--green)' : score >= 4 ? 'var(--orange)' : 'var(--red)'; + const bd = rs.breakdown || {}; + const labels = { transit_diversity: 'Transit Diversity', peering_breadth: 'Peering Breadth', ixp_presence: 'IXP Presence', path_redundancy: 'Path Redundancy' }; + let h = '
'; + h += '' + score.toFixed(1) + ''; + h += '/10
'; + h += '
'; + Object.keys(bd).forEach(function(k) { + const item = bd[k]; + const pct = Math.round((item.raw || 0) * 10); + const c = pct >= 70 ? 'var(--green)' : pct >= 40 ? 'var(--orange)' : 'var(--red)'; + h += '
'; + h += '' + (labels[k] || k) + ''; + h += '
'; + h += '' + (item.raw || 0) + '/10'; + h += '
'; + }); + h += '
'; + if (rs._provenance) { + const prov = rs._provenance; + const badge = document.getElementById('resilienceProvBadge'); + if (badge) badge.innerHTML = '' + escHtml(prov.confidence || '') + ' · ' + escHtml(prov.validation || '') + ''; + } + el.innerHTML = h; +} + +// ── Route Leak Detection ─────────────────────────────────────── +function renderRouteLeak(rl) { + const card = document.getElementById('routeLeakCard'); + const el = document.getElementById('routeLeakContent'); + if (!card || !el || !rl) return; + card.style.display = ''; + const detected = rl.detected; + const color = detected ? 'var(--red)' : 'var(--green)'; + let h = '
'; + h += '' + (detected ? '⚠ LEAK DETECTED' : '✓ No Leaks Detected') + '
'; + if (rl.patterns && rl.patterns.length) { + h += '
Patterns:
'; + rl.patterns.forEach(function(p) { + h += '
' + escHtml(String(p)) + '
'; + }); + } + h += '
'; + h += 'Tier-1 upstreams: ' + (rl.tier1_upstream_count || 0) + ' · Tier-1 downstreams: ' + (rl.tier1_downstream_count || 0); + h += '
'; + if (rl._provenance) { + const prov = rl._provenance; + const badge = document.getElementById('routeLeakProvBadge'); + if (badge) badge.innerHTML = '' + escHtml(prov.confidence || '') + ' · ' + escHtml(prov.validation || '') + ''; + } + el.innerHTML = h; +} + // ── Data Sources Timing ──────────────────────────────────────── function renderSourceTiming(d) { const card = document.getElementById('sourceTimingCard');