51eb3bf7c8
- Wire up 26 vendor providers: Atlassian Statuspage API, Status.io, Instatus, AWS RSS feeds, Apple/Google JSON feeds, M365 Graph API, and synthetic checks - Add 11 new providers: AWS, Cloudflare, SmartPass, School Dismissal Manager, SherpaDesk, Classkick, ClassDojo, Savvas, Study Island, Promethean, RAZ-Kids - Rename Local Infrastructure → Internet (TCP check to 8.8.8.8:53) - Add WAN throughput graph section: dual-link canvas graphs (Crown Castle + Comcast) polling FortiGate REST API every 30s with 30-min rolling history - Add FortiGate health card: uptime, CPU %, memory % from FortiOS API - Add /api/throughput and /api/fortigate-health endpoints - Add README with setup instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
2.6 KiB
JavaScript
98 lines
2.6 KiB
JavaScript
import https from "https";
|
|
|
|
const HOST = process.env.FORTIGATE_HOST;
|
|
const TOKEN = process.env.FORTIGATE_API_TOKEN;
|
|
|
|
const POLL_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
|
|
|
|
let cached = null;
|
|
|
|
function fetchJson(urlStr, headers) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(urlStr);
|
|
const req = https.request(
|
|
{
|
|
hostname: url.hostname,
|
|
port: url.port || 443,
|
|
path: url.pathname + url.search,
|
|
method: "GET",
|
|
headers,
|
|
rejectUnauthorized: false,
|
|
},
|
|
(res) => {
|
|
let body = "";
|
|
res.on("data", (chunk) => (body += chunk));
|
|
res.on("end", () => {
|
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
reject(new Error(`HTTP ${res.statusCode}`));
|
|
} else {
|
|
try { resolve(JSON.parse(body)); }
|
|
catch { reject(new Error("Invalid JSON in response")); }
|
|
}
|
|
});
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function formatUptime(seconds) {
|
|
const d = Math.floor(seconds / 86400);
|
|
const h = Math.floor((seconds % 86400) / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
if (d > 0) return `${d}d ${h}h`;
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
return `${m}m`;
|
|
}
|
|
|
|
async function poll() {
|
|
const headers = { Authorization: `Bearer ${TOKEN}` };
|
|
const base = `https://${HOST}`;
|
|
|
|
const [statusData, resourceData] = await Promise.all([
|
|
fetchJson(`${base}/api/v2/monitor/system/status`, headers),
|
|
fetchJson(`${base}/api/v2/monitor/system/resource/usage`, headers),
|
|
]);
|
|
|
|
const r = statusData.results ?? {};
|
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
const uptimeSec = r.utc_last_reboot
|
|
? nowSec - r.utc_last_reboot
|
|
: null;
|
|
|
|
const cpuPct = resourceData.results?.cpu?.[0]?.current ?? null;
|
|
const memPct = resourceData.results?.mem?.[0]?.current ?? null;
|
|
|
|
cached = {
|
|
hostname: r.hostname ?? "FortiGate",
|
|
model: r.model_number ?? "1800F",
|
|
version: statusData.version ?? "—",
|
|
uptimeSeconds: uptimeSec,
|
|
uptimeLabel: uptimeSec !== null ? formatUptime(uptimeSec) : "—",
|
|
cpuPct,
|
|
memPct,
|
|
lastUpdated: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
export function getHealth() {
|
|
return cached;
|
|
}
|
|
|
|
export function startPolling() {
|
|
if (!HOST || !TOKEN) {
|
|
console.warn("[fg-health] FORTIGATE_HOST or FORTIGATE_API_TOKEN not set — health polling disabled.");
|
|
return;
|
|
}
|
|
poll().catch((err) =>
|
|
console.error("[fg-health] Initial poll failed:", err.message)
|
|
);
|
|
setInterval(
|
|
() => poll().catch((err) =>
|
|
console.error("[fg-health] Poll failed:", err.message)
|
|
),
|
|
POLL_INTERVAL_MS
|
|
);
|
|
}
|