diff --git a/public/index.html b/public/index.html
index ffe36a4..f3fe104 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2553,6 +2553,58 @@ function renderHealthReport(d) {
h += '';
+ // === DATA ACCURACY SECTION ===
+ h += '
';
+ h += '
';
+ h += '
Score Breakdown — Why ' + score + '/100? ';
+
+ // Score calculation table
+ h += '
';
+ h += 'Check Weight Earned Reason ';
+
+ var weightMap = { bogon: 15, irr: 10, rpki_completeness: 15, abuse_contact: 5, blocklist: 15, manrs: 5, rdns: 5, visibility: 10, rpsl: 5, ix_route_server: 5, resource_cert: 10 };
+ var totalW = 0, totalE = 0;
+ checkOrder.forEach(function(key) {
+ var v = validations[key];
+ if (!v) return;
+ var w = weightMap[key] || 0;
+ if (w === 0) return; // communities, geolocation not scored
+ var info = checkLabels[key] || { label: key };
+ var earned = 0;
+ var reason = '';
+ if (v.status === 'info') {
+ reason = 'Excluded — unable to verify (API requires authentication) ';
+ // info checks excluded from scoring
+ } else {
+ if (v.status === 'pass') { earned = w; reason = 'Full points '; }
+ else if (v.status === 'warning') { earned = Math.round(w * 0.5); reason = '' + escHtml(v.note || v.message || 'Partial compliance') + ' '; }
+ else { earned = 0; reason = '' + escHtml(v.note || v.message || 'Check failed') + ' '; }
+ totalW += w;
+ totalE += earned;
+ }
+ var statusIcon = v.status === 'pass' ? '✅' : v.status === 'warning' ? '⚠️' : v.status === 'fail' ? '❌' : 'ℹ️';
+ h += '' + statusIcon + ' ' + info.label + ' ';
+ h += '' + (v.status === 'info' ? '—' : w) + ' ';
+ h += '' + (v.status === 'info' ? '—' : earned) + ' ';
+ h += '' + reason + ' ';
+ });
+
+ var calcScore = totalW > 0 ? Math.round((totalE / totalW) * 100) : 0;
+ h += 'Total ';
+ h += '' + totalW + ' ';
+ h += '' + totalE + ' ';
+ h += '' + calcScore + '/100 = ' + totalE + '/' + totalW + ' weighted points ';
+ h += '
';
+
+ // Data source note
+ h += '
';
+ h += 'Data Sources: PeeringDB (profile, IX, facilities), RIPE Stat (prefixes, neighbours, visibility, RPKI), ';
+ h += 'RIPE Atlas (probes), Cloudflare RPKI (ROA + ASPA), MANRS Observatory, RIPE DB (IRR objects). ';
+ h += 'Scoring: Each check has a weight reflecting its importance to routing security. ';
+ h += '"Pass" = full weight, "Warning" = 50%, "Fail" = 0%, "Info" = excluded (unable to verify). ';
+ h += 'Score = earned / total_weight × 100. Checks marked "info" (e.g., MANRS when API is unavailable) are excluded from the denominator to avoid unfair penalties.';
+ h += '
';
+
// Expandable details
h += 'Show detailed validation results
';
h += '';
diff --git a/server.js b/server.js
index b654d29..4bb60ca 100644
--- a/server.js
+++ b/server.js
@@ -940,8 +940,14 @@ const server = http.createServer(async (req, res) => {
return res.end();
}
- const url = new URL(req.url, "http://localhost");
- const reqPath = url.pathname;
+ let url, reqPath;
+ try {
+ url = new URL(req.url, "http://localhost");
+ reqPath = url.pathname;
+ } catch (_) {
+ res.writeHead(400);
+ return res.end("Bad Request");
+ }
// Serve static files
if (reqPath === "/" || reqPath === "/index.html") {