// --------------------------------------------------------------------------- // Health checks for all dashboard services. // Reads the service URLs from script.js (single source of truth), pings each // one in parallel and writes health.json. Scheduled every minute by cron. // // Status classification: // online - got any HTTP response below 500 (401/404 still means "up") // error - server responded with 5xx // offline - timeout / connection refused / DNS failure // TLS certificate validity is ignored (reachability check only). // --------------------------------------------------------------------------- const http = require("http"); const https = require("https"); const fs = require("fs"); const path = require("path"); const { URL } = require("url"); const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/health.json"; const TIMEOUT = parseInt(process.env.HEALTH_TIMEOUT || "5000", 10); // Reachability only — don't fail on self-signed certificates. const httpAgent = new http.Agent({ rejectUnauthorized: false }); const httpsAgent = new https.Agent({ rejectUnauthorized: false }); function serviceUrls() { const candidates = [ process.env.SERVICES_JS, "/usr/share/nginx/html/script.js", path.join(__dirname, "..", "script.js"), ].filter(Boolean); for (const p of candidates) { try { const src = fs.readFileSync(p, "utf8"); const urls = [...src.matchAll(/url: *"(https?:\/\/[^"]+)"/g)].map((m) => m[1]); if (urls.length) return [...new Set(urls)]; } catch { // try next candidate } } return []; } function checkUrl(url) { return new Promise((resolve) => { const started = Date.now(); let settled = false; const done = (result) => { if (settled) return; settled = true; resolve(result); }; let req; try { const u = new URL(url); const client = u.protocol === "https:" ? https : http; req = client.request( u, { method: "GET", agent: u.protocol === "https:" ? httpsAgent : httpAgent, timeout: TIMEOUT, }, (res) => { const code = res.statusCode; res.destroy(); // response headers are all we need done({ status: code >= 500 ? "error" : "online", code, latencyMs: Date.now() - started, error: null, }); } ); } catch (e) { return done({ status: "offline", code: null, latencyMs: null, error: e.message }); } req.on("timeout", () => { req.destroy(); done({ status: "offline", code: null, latencyMs: null, error: "Timeout" }); }); req.on("error", (e) => { done({ status: "offline", code: null, latencyMs: null, error: e.message }); }); req.end(); }); } async function main() { const urls = serviceUrls(); if (urls.length === 0) { console.error("No service URLs found — is script.js available?"); process.exit(1); } const results = await Promise.all(urls.map(async (u) => [u, await checkUrl(u)])); const services = Object.fromEntries(results); const payload = { updatedAt: new Date().toISOString(), services }; fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2)); const count = (s) => results.filter(([, r]) => r.status === s).length; console.log(`Health: ${urls.length} services — ${count("online")} online, ${count("error")} error, ${count("offline")} offline`); } main().catch((err) => { console.error(err); process.exit(1); });