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');