refactor: extract lia.js and health.js routes, fix /api/health closure bug
Bug fix (agreed pre-existing bug #3 of 3): the .catch() branch of localDb.getLocalDbStats() referenced status/roaAge, both only declared inside the sibling .then() callback's closure -- out of scope in .catch(), so any real DB-stats failure threw ReferenceError with no further catch, hanging the response until client timeout instead of degrading gracefully. Fixed: roaAge computed once before the promise chain (same pattern as the existing aspaAge); the .catch() branch gets its own locally-scoped `status = "degraded"` (a DB-stats fetch failure can't confirm "ok", matching the honest-degradation pattern already used throughout this codebase). Also deduplicated the identical delta_last computation that was repeated verbatim in both branches into one aspaAdoptionDelta() helper. Verified via node --check, eslint no-undef (2 known bugs remain: compare, webhooks), and smoke-test harness (28/28 match). server.js: 3556 -> 3346 lines.
This commit is contained in:
parent
3399392f1f
commit
b82c61e03c
177
server.js
177
server.js
@ -220,6 +220,8 @@ function proxyToApiServer(req, res, targetUrl) {
|
||||
const staticRoutes = require("./server/routes/static");
|
||||
const searchRoutes = require("./server/routes/search");
|
||||
const feedbackRoutes = require("./server/routes/feedback");
|
||||
const liaRoutes = require("./server/routes/lia");
|
||||
const healthRoute = require("./server/routes/health");
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
@ -282,197 +284,34 @@ const server = http.createServer(async (req, res) => {
|
||||
return staticRoutes.handleFavicon(req, res);
|
||||
}
|
||||
|
||||
// Lia'''s Atlas Paradise - Easter egg page
|
||||
// Lia's Atlas Paradise - Easter egg page
|
||||
if (reqPath === "/lia" || reqPath === "/lia/") {
|
||||
return staticRoutes.handleLia(req, res);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Lia's Atlas Paradise: Atlas probe coverage endpoint
|
||||
// ============================================================
|
||||
if (reqPath === "/api/atlas/coverage") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
if (!atlasState.cache) {
|
||||
res.writeHead(503);
|
||||
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
|
||||
}
|
||||
return res.end(JSON.stringify(atlasState.cache, null, 2));
|
||||
return liaRoutes.handleAtlasCoverage(req, res);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Lia's Paradise: File parsing endpoint (for binary uploads)
|
||||
// ============================================================
|
||||
if (reqPath === "/api/lia/parse-file" && req.method === "POST") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
let body = "";
|
||||
req.on("data", function(chunk) { body += chunk; });
|
||||
req.on("end", function() {
|
||||
try {
|
||||
var parsed = JSON.parse(body);
|
||||
var filename = parsed.filename || "";
|
||||
var ext = filename.split(".").pop().toLowerCase();
|
||||
// For text-based formats, decode base64 and extract text
|
||||
if (ext === "csv" || ext === "txt") {
|
||||
var text = Buffer.from(parsed.data, "base64").toString("utf8");
|
||||
return res.end(JSON.stringify({ text: text }));
|
||||
}
|
||||
// For binary formats (PDF, XLS, DOC), we can't parse server-side without
|
||||
// heavy dependencies. Return helpful error.
|
||||
return res.end(JSON.stringify({
|
||||
error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.",
|
||||
suggestion: "Export your spreadsheet as CSV first, then upload the CSV file."
|
||||
}));
|
||||
} catch(e) {
|
||||
return res.end(JSON.stringify({ error: "Parse error: " + e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
return liaRoutes.handleParseFile(req, res);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Lia's Paradise: Combined PeeringDB + Atlas coverage data
|
||||
// ============================================================
|
||||
if (reqPath === "/api/lia/coverage") {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
if (!atlasState.cache) {
|
||||
res.writeHead(503);
|
||||
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
|
||||
}
|
||||
|
||||
// Cache this expensive response for 30 min
|
||||
var liaCacheKey = "lia_coverage";
|
||||
var liaCached = cacheGet(liaCacheKey);
|
||||
if (liaCached) return res.end(liaCached);
|
||||
|
||||
// Fetch PeeringDB network list (all networks with status "ok")
|
||||
// Use pre-cached org→country map (loaded at startup, 16MB response cached in memory)
|
||||
fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) {
|
||||
if (!pdbData || !pdbData.data) {
|
||||
return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" }));
|
||||
}
|
||||
|
||||
var probeAsns = new Set(atlasState.cache.asns_with_probes || []);
|
||||
|
||||
var enriched = pdbData.data.map(function(n) {
|
||||
var org = pdbOrgState.map.get(n.org_id) || {};
|
||||
var cc = org.country || "";
|
||||
return {
|
||||
asn: n.asn,
|
||||
name: n.name || "",
|
||||
org_name: org.name || "",
|
||||
country: cc,
|
||||
country_name: cc,
|
||||
info_type: n.info_type || "",
|
||||
has_probe: probeAsns.has(n.asn),
|
||||
};
|
||||
}).filter(function(n) { return n.asn > 0 && n.country; });
|
||||
|
||||
var result = JSON.stringify({
|
||||
networks: enriched,
|
||||
total: enriched.length,
|
||||
with_probes: enriched.filter(function(n) { return n.has_probe; }).length,
|
||||
without_probes: enriched.filter(function(n) { return !n.has_probe; }).length,
|
||||
atlas_unique_asns: probeAsns.size,
|
||||
org_countries_loaded: pdbOrgState.map.size,
|
||||
fetched_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
cacheSet(liaCacheKey, result, 30 * 60 * 1000);
|
||||
res.end(result);
|
||||
}).catch(function(e) {
|
||||
res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message }));
|
||||
});
|
||||
return;
|
||||
return liaRoutes.handleLiaCoverage(req, res);
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
|
||||
// Health endpoint — extended with cache status, ASPA metrics, and local DB stats
|
||||
if (reqPath === "/api/health") {
|
||||
const mem = process.memoryUsage();
|
||||
const rpkiAspaLastFetch = getRpkiAspaLastFetch();
|
||||
const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1;
|
||||
const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses;
|
||||
|
||||
// Query local DB stats (async, but return partial if needed)
|
||||
localDb.getLocalDbStats().then(function(dbStats) {
|
||||
// Determine health status based on local DB data availability
|
||||
const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally
|
||||
const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally
|
||||
const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded";
|
||||
|
||||
const healthResponse = {
|
||||
status,
|
||||
service: "PeerCortex",
|
||||
version: "0.6.9",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
||||
bgproutes_configured: !!BGPROUTES_API_KEY,
|
||||
caches: {
|
||||
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
||||
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
||||
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
||||
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
||||
ripe_stat: { entries: ripeStatCache.size },
|
||||
response_cache: { entries: responseCache.size },
|
||||
},
|
||||
local_db: dbStats ? {
|
||||
bgp_routes: dbStats.bgp_routes,
|
||||
rpki_roas: dbStats.rpki_roas,
|
||||
threat_intel: dbStats.threat_intel,
|
||||
rdap_cache_entries: dbStats.rdap_cache_entries,
|
||||
source: "PostgreSQL (local)",
|
||||
healthy: hasLocalBgp && hasLocalRpki,
|
||||
} : null,
|
||||
aspa_adoption: {
|
||||
total_objects: rpkiAspaMap.size,
|
||||
roa_count: roaStore.count,
|
||||
history_samples: aspaAdoptionDailyHistory.length,
|
||||
delta_last: aspaAdoptionDailyHistory.length >= 2
|
||||
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
return res.end(JSON.stringify(healthResponse, null, 2));
|
||||
}).catch(function(e) {
|
||||
console.error('[/api/health] Local DB stats error:', e.message);
|
||||
// Return health without local DB stats on error
|
||||
return res.end(
|
||||
JSON.stringify({
|
||||
status,
|
||||
service: "PeerCortex",
|
||||
version: "0.6.9",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
||||
bgproutes_configured: !!BGPROUTES_API_KEY,
|
||||
caches: {
|
||||
roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready },
|
||||
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
||||
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
||||
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
||||
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
||||
ripe_stat: { entries: ripeStatCache.size },
|
||||
response_cache: { entries: responseCache.size },
|
||||
},
|
||||
local_db: { error: "Could not fetch local DB stats", message: e.message },
|
||||
aspa_adoption: {
|
||||
total_objects: rpkiAspaMap.size,
|
||||
roa_count: roaStore.count,
|
||||
history_samples: aspaAdoptionDailyHistory.length,
|
||||
delta_last: aspaAdoptionDailyHistory.length >= 2
|
||||
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
|
||||
: 0,
|
||||
},
|
||||
}, null, 2)
|
||||
);
|
||||
});
|
||||
return;
|
||||
return healthRoute.handleHealth(req, res);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ASPA Deep Verification endpoint: /api/aspa/verify?asn=X
|
||||
// ============================================================
|
||||
|
||||
104
server/routes/health.js
Normal file
104
server/routes/health.js
Normal file
@ -0,0 +1,104 @@
|
||||
const localDb = require("../../local-db-client");
|
||||
const { getRpkiAspaLastFetch, rpkiAspaMap } = require("../services/rpki");
|
||||
const { pdbSourceCache } = require("../caches/pdb-source-cache");
|
||||
const { roaStore } = require("../caches/roa-store");
|
||||
const { ripeStatCache } = require("../services/ripe-stat");
|
||||
const { responseCache } = require("../caches/response-caches");
|
||||
const { aspaAdoptionDailyHistory } = require("../aspa-adoption/tracker");
|
||||
|
||||
const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || "";
|
||||
|
||||
function aspaAdoptionDelta() {
|
||||
return aspaAdoptionDailyHistory.length >= 2
|
||||
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
|
||||
: 0;
|
||||
}
|
||||
|
||||
// Health endpoint — extended with cache status, ASPA metrics, and local DB stats
|
||||
function handleHealth(req, res) {
|
||||
const mem = process.memoryUsage();
|
||||
const rpkiAspaLastFetch = getRpkiAspaLastFetch();
|
||||
const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1;
|
||||
// Bug fix 2026-07-17: previously only declared inside the .then() callback
|
||||
// below, so the .catch() branch's reference to it threw ReferenceError
|
||||
// (never caught -- the response just hung until client timeout).
|
||||
const roaAge = roaStore.lastBuild ? Math.floor((Date.now() - roaStore.lastBuild) / 60000) : -1;
|
||||
const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses;
|
||||
|
||||
// Query local DB stats (async, but return partial if needed)
|
||||
localDb.getLocalDbStats().then(function(dbStats) {
|
||||
// Determine health status based on local DB data availability
|
||||
const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally
|
||||
const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally
|
||||
const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded";
|
||||
|
||||
const healthResponse = {
|
||||
status,
|
||||
service: "PeerCortex",
|
||||
version: "0.6.9",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
||||
bgproutes_configured: !!BGPROUTES_API_KEY,
|
||||
caches: {
|
||||
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
||||
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
||||
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
||||
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
||||
ripe_stat: { entries: ripeStatCache.size },
|
||||
response_cache: { entries: responseCache.size },
|
||||
},
|
||||
local_db: dbStats ? {
|
||||
bgp_routes: dbStats.bgp_routes,
|
||||
rpki_roas: dbStats.rpki_roas,
|
||||
threat_intel: dbStats.threat_intel,
|
||||
rdap_cache_entries: dbStats.rdap_cache_entries,
|
||||
source: "PostgreSQL (local)",
|
||||
healthy: hasLocalBgp && hasLocalRpki,
|
||||
} : null,
|
||||
aspa_adoption: {
|
||||
total_objects: rpkiAspaMap.size,
|
||||
roa_count: roaStore.count,
|
||||
history_samples: aspaAdoptionDailyHistory.length,
|
||||
delta_last: aspaAdoptionDelta(),
|
||||
},
|
||||
};
|
||||
return res.end(JSON.stringify(healthResponse, null, 2));
|
||||
}).catch(function(e) {
|
||||
console.error('[/api/health] Local DB stats error:', e.message);
|
||||
// Return health without local DB stats on error. Local DB stats failed
|
||||
// to fetch, so "ok" can't be confirmed -- report degraded, matching the
|
||||
// honest-degradation pattern used throughout this codebase.
|
||||
const status = "degraded";
|
||||
return res.end(
|
||||
JSON.stringify({
|
||||
status,
|
||||
service: "PeerCortex",
|
||||
version: "0.6.9",
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime_seconds: Math.floor(process.uptime()),
|
||||
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
||||
bgproutes_configured: !!BGPROUTES_API_KEY,
|
||||
caches: {
|
||||
roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready },
|
||||
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
||||
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
||||
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
||||
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
||||
ripe_stat: { entries: ripeStatCache.size },
|
||||
response_cache: { entries: responseCache.size },
|
||||
},
|
||||
local_db: { error: "Could not fetch local DB stats", message: e.message },
|
||||
aspa_adoption: {
|
||||
total_objects: rpkiAspaMap.size,
|
||||
roa_count: roaStore.count,
|
||||
history_samples: aspaAdoptionDailyHistory.length,
|
||||
delta_last: aspaAdoptionDelta(),
|
||||
},
|
||||
}, null, 2)
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
module.exports = { handleHealth };
|
||||
98
server/routes/lia.js
Normal file
98
server/routes/lia.js
Normal file
@ -0,0 +1,98 @@
|
||||
const { atlasState } = require("../atlas-probes");
|
||||
const { pdbOrgState } = require("../pdb-org-countries");
|
||||
const { fetchPeeringDB } = require("../services/peeringdb");
|
||||
const { cacheGet, cacheSet } = require("../caches/response-caches");
|
||||
|
||||
// Lia's Atlas Paradise: Atlas probe coverage endpoint
|
||||
function handleAtlasCoverage(req, res) {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
if (!atlasState.cache) {
|
||||
res.writeHead(503);
|
||||
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
|
||||
}
|
||||
return res.end(JSON.stringify(atlasState.cache, null, 2));
|
||||
}
|
||||
|
||||
// Lia's Paradise: File parsing endpoint (for binary uploads)
|
||||
function handleParseFile(req, res) {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
let body = "";
|
||||
req.on("data", function(chunk) { body += chunk; });
|
||||
req.on("end", function() {
|
||||
try {
|
||||
var parsed = JSON.parse(body);
|
||||
var filename = parsed.filename || "";
|
||||
var ext = filename.split(".").pop().toLowerCase();
|
||||
// For text-based formats, decode base64 and extract text
|
||||
if (ext === "csv" || ext === "txt") {
|
||||
var text = Buffer.from(parsed.data, "base64").toString("utf8");
|
||||
return res.end(JSON.stringify({ text: text }));
|
||||
}
|
||||
// For binary formats (PDF, XLS, DOC), we can't parse server-side without
|
||||
// heavy dependencies. Return helpful error.
|
||||
return res.end(JSON.stringify({
|
||||
error: "Binary file parsing (" + ext.toUpperCase() + ") requires client-side extraction. Please use CSV or TXT format, or copy-paste the content.",
|
||||
suggestion: "Export your spreadsheet as CSV first, then upload the CSV file."
|
||||
}));
|
||||
} catch(e) {
|
||||
return res.end(JSON.stringify({ error: "Parse error: " + e.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Lia's Paradise: Combined PeeringDB + Atlas coverage data
|
||||
function handleLiaCoverage(req, res) {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
if (!atlasState.cache) {
|
||||
res.writeHead(503);
|
||||
return res.end(JSON.stringify({ error: "Atlas probe data is still loading. Please try again in a minute." }));
|
||||
}
|
||||
|
||||
// Cache this expensive response for 30 min
|
||||
var liaCacheKey = "lia_coverage";
|
||||
var liaCached = cacheGet(liaCacheKey);
|
||||
if (liaCached) return res.end(liaCached);
|
||||
|
||||
// Fetch PeeringDB network list (all networks with status "ok")
|
||||
// Use pre-cached org→country map (loaded at startup, 16MB response cached in memory)
|
||||
fetchPeeringDB("/net?status=ok&depth=0").then(function(pdbData) {
|
||||
if (!pdbData || !pdbData.data) {
|
||||
return res.end(JSON.stringify({ error: "Could not fetch PeeringDB networks" }));
|
||||
}
|
||||
|
||||
var probeAsns = new Set(atlasState.cache.asns_with_probes || []);
|
||||
|
||||
var enriched = pdbData.data.map(function(n) {
|
||||
var org = pdbOrgState.map.get(n.org_id) || {};
|
||||
var cc = org.country || "";
|
||||
return {
|
||||
asn: n.asn,
|
||||
name: n.name || "",
|
||||
org_name: org.name || "",
|
||||
country: cc,
|
||||
country_name: cc,
|
||||
info_type: n.info_type || "",
|
||||
has_probe: probeAsns.has(n.asn),
|
||||
};
|
||||
}).filter(function(n) { return n.asn > 0 && n.country; });
|
||||
|
||||
var result = JSON.stringify({
|
||||
networks: enriched,
|
||||
total: enriched.length,
|
||||
with_probes: enriched.filter(function(n) { return n.has_probe; }).length,
|
||||
without_probes: enriched.filter(function(n) { return !n.has_probe; }).length,
|
||||
atlas_unique_asns: probeAsns.size,
|
||||
org_countries_loaded: pdbOrgState.map.size,
|
||||
fetched_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
cacheSet(liaCacheKey, result, 30 * 60 * 1000);
|
||||
res.end(result);
|
||||
}).catch(function(e) {
|
||||
res.end(JSON.stringify({ error: "PeeringDB fetch failed: " + e.message }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
module.exports = { handleAtlasCoverage, handleParseFile, handleLiaCoverage };
|
||||
Loading…
x
Reference in New Issue
Block a user