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>
119 lines
3.3 KiB
JavaScript
119 lines
3.3 KiB
JavaScript
import https from "https";
|
|
|
|
const HOST = process.env.FORTIGATE_HOST;
|
|
const TOKEN = process.env.FORTIGATE_API_TOKEN;
|
|
const WAN1_IF = process.env.FORTIGATE_WAN1_INTERFACE;
|
|
const WAN1_LABEL = process.env.FORTIGATE_WAN1_LABEL ?? WAN1_IF;
|
|
const WAN2_IF = process.env.FORTIGATE_WAN2_INTERFACE;
|
|
const WAN2_LABEL = process.env.FORTIGATE_WAN2_LABEL ?? WAN2_IF;
|
|
|
|
const POLL_INTERVAL_MS = 30_000;
|
|
const HISTORY_POINTS = 60; // 30 minutes at 30s intervals
|
|
|
|
let history = [];
|
|
let prevReading = null;
|
|
|
|
// Use the https module directly so we can disable rejectUnauthorized —
|
|
// FortiGate management interface uses a self-signed cert.
|
|
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 (e) { reject(new Error("Invalid JSON in response")); }
|
|
}
|
|
});
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function getIfBytes(data, ifname) {
|
|
const results = data?.results;
|
|
if (!results) return null;
|
|
// FortiOS may return results as an object keyed by interface name or as an array
|
|
const iface = Array.isArray(results)
|
|
? results.find((i) => i.name === ifname)
|
|
: results[ifname];
|
|
if (!iface) return null;
|
|
return { rx: iface.rx_bytes ?? 0, tx: iface.tx_bytes ?? 0 };
|
|
}
|
|
|
|
async function poll() {
|
|
const data = await fetchJson(
|
|
`https://${HOST}/api/v2/monitor/system/interface`,
|
|
{ Authorization: `Bearer ${TOKEN}` }
|
|
);
|
|
|
|
const now = Date.now();
|
|
const wan1 = getIfBytes(data, WAN1_IF);
|
|
const wan2 = getIfBytes(data, WAN2_IF);
|
|
|
|
if (!wan1 || !wan2) {
|
|
console.warn("[throughput] Interface not found in FortiGate response — check interface names in .env");
|
|
return;
|
|
}
|
|
|
|
if (prevReading) {
|
|
const elapsed = (now - prevReading.timestamp) / 1000; // seconds
|
|
const mbps = (curr, prev) =>
|
|
Math.max(0, ((curr - prev) * 8) / elapsed / 1_000_000);
|
|
|
|
const point = {
|
|
timestamp: now,
|
|
wan1: {
|
|
label: WAN1_LABEL,
|
|
rxMbps: mbps(wan1.rx, prevReading.wan1.rx),
|
|
txMbps: mbps(wan1.tx, prevReading.wan1.tx),
|
|
},
|
|
wan2: {
|
|
label: WAN2_LABEL,
|
|
rxMbps: mbps(wan2.rx, prevReading.wan2.rx),
|
|
txMbps: mbps(wan2.tx, prevReading.wan2.tx),
|
|
},
|
|
};
|
|
|
|
history.push(point);
|
|
if (history.length > HISTORY_POINTS) history.shift();
|
|
}
|
|
|
|
prevReading = { timestamp: now, wan1, wan2 };
|
|
}
|
|
|
|
export function getHistory() {
|
|
return history;
|
|
}
|
|
|
|
export function startPolling() {
|
|
if (!HOST || !TOKEN) {
|
|
console.warn("[throughput] FORTIGATE_HOST or FORTIGATE_API_TOKEN not set — throughput polling disabled.");
|
|
return;
|
|
}
|
|
poll().catch((err) =>
|
|
console.error("[throughput] Initial poll failed:", err.message)
|
|
);
|
|
setInterval(
|
|
() => poll().catch((err) =>
|
|
console.error("[throughput] Poll failed:", err.message)
|
|
),
|
|
POLL_INTERVAL_MS
|
|
);
|
|
}
|