Implement all vendor integrations, WAN graphs, and FortiGate health panel

- 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>
This commit is contained in:
Klein
2026-02-20 13:46:13 -05:00
parent 7d8cde8f92
commit 51eb3bf7c8
39 changed files with 1776 additions and 59 deletions
+97
View File
@@ -0,0 +1,97 @@
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
);
}
+118
View File
@@ -0,0 +1,118 @@
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
);
}
+13
View File
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2"
}
},
@@ -174,6 +175,18 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+1
View File
@@ -8,6 +8,7 @@
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2"
}
}
+77
View File
@@ -0,0 +1,77 @@
export const name = "Amazon AWS";
export const url = "https://health.aws.amazon.com/health/status";
// Core services in both Pittsburgh-adjacent regions + global services.
// Expand this list if you discover specific services your vendors rely on.
const FEEDS = [
{ url: "https://status.aws.amazon.com/rss/ec2-us-east-1.rss", label: "EC2 (N. Virginia)" },
{ url: "https://status.aws.amazon.com/rss/ec2-us-east-2.rss", label: "EC2 (Ohio)" },
{ url: "https://status.aws.amazon.com/rss/s3-us-east-1.rss", label: "S3 (N. Virginia)" },
{ url: "https://status.aws.amazon.com/rss/cloudfront.rss", label: "CloudFront" },
{ url: "https://status.aws.amazon.com/rss/route53.rss", label: "Route 53" },
];
const SEVERITY_ORDER = ["operational", "degraded", "outage"];
function worstStatus(a, b) {
return SEVERITY_ORDER.indexOf(a) >= SEVERITY_ORDER.indexOf(b) ? a : b;
}
// Extract the text of the first <title> inside the first <item>.
// Returns null if there are no items (clean feed = all clear).
function parseFirstItemTitle(xml) {
const itemMatch = xml.match(/<item[\s>][\s\S]*?<\/item>/i);
if (!itemMatch) return null;
const titleMatch = itemMatch[0].match(/<title>([\s\S]*?)<\/title>/i);
return titleMatch ? titleMatch[1].trim() : null;
}
function titleToStatus(title) {
if (!title) return "operational"; // no items = clean feed
const t = title.toLowerCase();
if (t.includes("operating normally") || t.includes("informational")) return "operational";
if (t.includes("performance issues") || t.includes("degraded")) return "degraded";
if (t.includes("service disruption") || t.includes("disruption")) return "outage";
return "degraded"; // unknown incident title — assume degraded
}
async function checkFeed({ url, label }) {
const res = await fetch(url);
if (!res.ok) throw new Error(`${label}: HTTP ${res.status}`);
const xml = await res.text();
const title = parseFirstItemTitle(xml);
const status = titleToStatus(title);
return { label, status, title };
}
export async function checkStatus() {
const results = await Promise.allSettled(FEEDS.map(checkFeed));
let overall = "operational";
const issues = [];
for (const result of results) {
if (result.status === "rejected") {
overall = worstStatus(overall, "degraded");
issues.push(`Check failed: ${result.reason?.message ?? "unknown error"}`);
continue;
}
const { label, status, title } = result.value;
overall = worstStatus(overall, status);
if (status !== "operational") {
issues.push(`${label}: ${title ?? "unknown issue"}`);
}
}
const message =
issues.length > 0 ? issues.join(" | ") : "All monitored services operating normally.";
return {
name,
status: overall,
message,
lastUpdated: new Date().toISOString(),
};
}
+50 -2
View File
@@ -1,10 +1,58 @@
export const name = "Apple";
export const url = "https://www.apple.com/support/systemstatus/";
const STATUS_URL =
"https://www.apple.com/support/systemstatus/data/system_status_en_US.js";
// eventStatus values that mean the event is no longer active
const RESOLVED_STATUSES = new Set(["resolved", "completed"]);
// statusType → dashboard status (Performance issues = degraded, outages = outage)
function mapStatusType(statusType) {
const t = (statusType ?? "").toLowerCase();
if (t === "outage") return "outage";
return "degraded"; // Performance, Maintenance, or anything else
}
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Apple status request failed (${res.status})`);
}
const data = await res.json();
const services = data.services ?? [];
// Collect services with active (unresolved) events
const affected = [];
let overall = "operational";
for (const svc of services) {
const activeEvents = (svc.events ?? []).filter(
(e) => !RESOLVED_STATUSES.has((e.eventStatus ?? "").toLowerCase())
);
if (activeEvents.length === 0) continue;
for (const event of activeEvents) {
const mapped = mapStatusType(event.statusType);
if (mapped === "outage" || overall === "operational") {
overall = mapped;
}
affected.push(`${svc.serviceName}: ${event.message ?? event.statusType}`);
}
}
const message =
affected.length > 0
? affected.join(" | ")
: "All services operating normally.";
return {
name,
status: "operational",
message: "All services running normally.",
status: overall,
message,
lastUpdated: new Date().toISOString(),
};
}
+17
View File
@@ -0,0 +1,17 @@
export const name = "ClassDojo";
export const url = "https://status.classdojo.com/";
// No public JSON status API — status page is a manually-updated static HTML file.
// Synthetic check against the main app; link to status page for incident details.
const PROBE_URL = "https://www.classdojo.com/";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: `App portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+17
View File
@@ -0,0 +1,17 @@
export const name = "Classkick";
export const url = "https://classkick.statuscast.com/";
// No public JSON status API — StatusCast requires a Bearer token.
// Synthetic check against the app; link to StatusCast page for incident details.
const PROBE_URL = "https://app.classkick.com/";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: `App portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+32 -2
View File
@@ -1,10 +1,40 @@
export const name = "Classlink";
export const url = "https://status.classlink.com/";
const STATUS_URL = "https://status.classlink.com/api/v2/summary.json";
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Classlink status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status: "operational",
message: "All services running normally.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+42
View File
@@ -0,0 +1,42 @@
export const name = "Cloudflare";
export const url = "https://www.cloudflarestatus.com/";
const STATUS_URL = "https://www.cloudflarestatus.com/api/v2/summary.json";
// Atlassian Statuspage indicator → dashboard status
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Cloudflare status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
// Build message from active incidents, fall back to Statuspage description
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+9 -2
View File
@@ -1,10 +1,17 @@
export const name = "DRC";
export const url = "https://wbte.drcedirect.com/PA/portals/pa";
// status.drcedirect.com is a JS-rendered Angular app with no accessible API.
// Synthetic check against the PA INSIGHT portal instead.
const PROBE_URL = "https://wbte.drcedirect.com/PA/portals/pa";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "outage",
message: "INSIGHT portal is currently unavailable. DRC is aware of the issue.",
status: "operational",
message: `PA INSIGHT portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+9 -1
View File
@@ -1,10 +1,18 @@
export const name = "EdInsight";
export const url = "https://myedinsight.com";
// TODO: No public status page found for Harris Education Solutions. Synthetic
// check only — investigate whether Harris offers a status API or webhook feed.
const PROBE_URL = "https://myedinsight.com";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: "Data analytics platform operational.",
message: `Portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+32 -2
View File
@@ -1,10 +1,40 @@
export const name = "FinalSite";
export const url = "https://status.finalsite.com/";
const STATUS_URL = "https://status.finalsite.com/api/v2/summary.json";
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`FinalSite status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status: "operational",
message: "All services running normally.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+8 -1
View File
@@ -1,10 +1,17 @@
export const name = "Follett";
export const url = "https://northhills.follettdestiny.com";
// status.follettsoftware.com has no public API or RSS feed — JS-rendered only.
// Synthetic check against the district's Destiny instance instead.
const PROBE_URL = "https://northhills.follettdestiny.com";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: "All services running normally.",
message: `Destiny portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+36 -2
View File
@@ -1,10 +1,44 @@
// Monitors FortiGate Cloud specifically — the most relevant FortiCloud service
// for a school district. Other FortiCloud products have separate status pages
// at status.forticlient.forticloud.com, status.fortiedge.forticloud.com, etc.
export const name = "Fortinet";
export const url = "https://status.fortigate.forticloud.com/";
const STATUS_URL =
"https://status.fortigate.forticloud.com/api/v2/summary.json";
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Fortinet status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status: "operational",
message: "FortiGuard and FortiCloud services operational.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+58 -2
View File
@@ -1,10 +1,66 @@
export const name = "Google Workspace";
export const url = "https://workspace.google.com/status/";
const INCIDENTS_URL =
"https://www.google.com/appsstatus/dashboard/incidents.json";
// status_impact → dashboard status
const STATUS_MAP = {
SERVICE_OUTAGE: "outage",
SERVICE_DISRUPTION: "degraded",
};
const IMPACT_LABEL = {
SERVICE_OUTAGE: "outage",
SERVICE_DISRUPTION: "service disruption",
};
const SEVERITY_ORDER = ["operational", "degraded", "outage"];
function worstStatus(a, b) {
return SEVERITY_ORDER.indexOf(a) >= SEVERITY_ORDER.indexOf(b) ? a : b;
}
export async function checkStatus() {
const res = await fetch(INCIDENTS_URL);
if (!res.ok) {
throw new Error(`Google Workspace status request failed (${res.status})`);
}
const incidents = await res.json();
// Active incidents have no end timestamp
const active = incidents.filter((i) => !i.end);
if (active.length === 0) {
return {
name,
status: "operational",
message: "All services operating normally.",
lastUpdated: new Date().toISOString(),
};
}
let overall = "operational";
const descriptions = [];
for (const incident of active) {
const mapped = STATUS_MAP[incident.status_impact] ?? "degraded";
overall = worstStatus(overall, mapped);
const services = (incident.affected_products ?? [])
.map((p) => p.title)
.join(", ");
const label = services || incident.service_name || "Unknown service";
const impact = IMPACT_LABEL[incident.status_impact] ?? "incident";
descriptions.push(`${label}: ${impact}`);
}
return {
name,
status: "operational",
message: "All services running normally.",
status: overall,
message: descriptions.join(" | "),
lastUpdated: new Date().toISOString(),
};
}
+22
View File
@@ -13,6 +13,17 @@ import * as schoolmessenger from "./schoolmessenger.js";
import * as fortinet from "./fortinet.js";
import * as mcgrawHill from "./mcgraw-hill.js";
import * as localInfrastructure from "./local-infrastructure.js";
import * as amazonAws from "./amazon-aws.js";
import * as cloudflare from "./cloudflare.js";
import * as sherpadesk from "./sherpadesk.js";
import * as studyIsland from "./study-island.js";
import * as classkick from "./classkick.js";
import * as classdojo from "./classdojo.js";
import * as savvas from "./savvas.js";
import * as schoolDismissalManager from "./school-dismissal-manager.js";
import * as smartpass from "./smartpass.js";
import * as promethean from "./promethean.js";
import * as razKids from "./raz-kids.js";
export const providers = [
microsoft365,
@@ -30,4 +41,15 @@ export const providers = [
fortinet,
mcgrawHill,
localInfrastructure,
amazonAws,
cloudflare,
sherpadesk,
studyIsland,
classkick,
classdojo,
savvas,
schoolDismissalManager,
smartpass,
promethean,
razKids,
];
+42 -3
View File
@@ -1,10 +1,49 @@
export const name = "Local Infrastructure";
import { createConnection } from "net";
export const name = "Internet";
export const url = null;
// TCP connect to Google DNS port 53 — more reliable than ICMP ping in
// managed Windows environments where outbound ICMP may be blocked.
const CHECK_HOST = "8.8.8.8";
const CHECK_PORT = 53;
const TIMEOUT_MS = 5000;
function tcpCheck(host, port) {
return new Promise((resolve) => {
const start = Date.now();
const socket = createConnection({ host, port }, () => {
const latencyMs = Date.now() - start;
socket.destroy();
resolve({ reachable: true, latencyMs });
});
socket.setTimeout(TIMEOUT_MS);
socket.on("timeout", () => { socket.destroy(); resolve({ reachable: false, latencyMs: null }); });
socket.on("error", () => { resolve({ reachable: false, latencyMs: null }); });
});
}
export async function checkStatus() {
const { reachable, latencyMs } = await tcpCheck(CHECK_HOST, CHECK_PORT);
if (!reachable) {
return {
name,
status: "outage",
message: `No response from ${CHECK_HOST}:${CHECK_PORT} — internet may be down.`,
lastUpdated: new Date().toISOString(),
};
}
const status = latencyMs >= 100 ? "degraded" : "operational";
const message = latencyMs >= 100
? `High latency to ${CHECK_HOST}: ${latencyMs}ms.`
: `Internet reachable — ${CHECK_HOST} responded in ${latencyMs}ms.`;
return {
name,
status: "operational",
message: "Firewall uptime 42d. WAN throughput nominal.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+8 -1
View File
@@ -1,10 +1,17 @@
export const name = "McGraw Hill";
export const url = "https://connected.mcgraw-hill.com/connected/permLinkLogin.do";
// status.mcgrawhill.com is a JS-rendered page with no accessible API.
// Synthetic check against the ConnectED portal instead.
const PROBE_URL = "https://connected.mcgraw-hill.com/connected/permLinkLogin.do";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: "All platform services operational.",
message: `ConnectED portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+120 -2
View File
@@ -1,10 +1,128 @@
export const name = "Microsoft 365";
export const url = "https://admin.microsoft.com/Adminportal/Home#/servicehealth";
const GRAPH_HEALTH_URL =
"https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews";
// Token cache
let cachedToken = null;
// Graph serviceStatus → dashboard status
const STATUS_MAP = {
serviceOperational: "operational",
serviceRestored: "operational",
postIncidentReviewPublished: "operational",
verifyingService: "operational",
falsePositive: "operational",
serviceDegradation: "degraded",
investigating: "degraded",
restoringService: "degraded",
extendedRecovery: "degraded",
investigationSuspended: "degraded",
serviceInterruption: "outage",
};
// Graph serviceStatus → human-readable label for the message
const STATUS_LABEL = {
serviceDegradation: "service degradation",
investigating: "investigating",
restoringService: "restoring service",
extendedRecovery: "extended recovery",
investigationSuspended: "investigation suspended",
serviceInterruption: "service interruption",
};
const SEVERITY_ORDER = ["operational", "degraded", "outage", "unknown"];
function mapStatus(graphStatus) {
return STATUS_MAP[graphStatus] || "unknown";
}
function worstStatus(a, b) {
return SEVERITY_ORDER.indexOf(a) >= SEVERITY_ORDER.indexOf(b) ? a : b;
}
async function getToken() {
const { AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET } =
process.env;
if (!AZURE_TENANT_ID || !AZURE_CLIENT_ID || !AZURE_CLIENT_SECRET) {
throw new Error(
"Missing Azure credentials (AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET)"
);
}
// Return cached token if still valid (with 5-minute buffer)
if (cachedToken && Date.now() < cachedToken.expiresAt - 5 * 60 * 1000) {
return cachedToken.accessToken;
}
const tokenUrl = `https://login.microsoftonline.com/${AZURE_TENANT_ID}/oauth2/v2.0/token`;
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: AZURE_CLIENT_ID,
client_secret: AZURE_CLIENT_SECRET,
scope: "https://graph.microsoft.com/.default",
});
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token request failed (${res.status}): ${text}`);
}
const data = await res.json();
cachedToken = {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return cachedToken.accessToken;
}
export async function checkStatus() {
const token = await getToken();
const res = await fetch(GRAPH_HEALTH_URL, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Graph API request failed (${res.status}): ${text}`);
}
const data = await res.json();
const services = data.value || [];
let overall = "operational";
const issues = [];
for (const svc of services) {
const mapped = mapStatus(svc.status);
overall = worstStatus(overall, mapped);
if (mapped !== "operational") {
const label = STATUS_LABEL[svc.status] ?? svc.status;
issues.push(`${svc.service}: ${label}`);
}
}
const message =
issues.length > 0
? issues.join(", ")
: "All services running normally.";
return {
name,
status: "operational",
message: "All services running normally.",
status: overall,
message,
lastUpdated: new Date().toISOString(),
};
}
+32 -2
View File
@@ -1,10 +1,40 @@
export const name = "PowerSchool";
export const url = "https://status.powerschool.com/";
const STATUS_URL = "https://status.powerschool.com/api/v2/summary.json";
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`PowerSchool status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status: "degraded",
message: "Slow response times reported. PowerSchool is investigating.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+17
View File
@@ -0,0 +1,17 @@
export const name = "Promethean";
export const url = "https://www.prometheanworld.com/";
// No cloud features in use — panels run in standalone mode. No public status
// page exists. Synthetic check confirms basic web reachability only.
const PROBE_URL = "https://www.prometheanworld.com/";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: `Site responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+42 -2
View File
@@ -1,10 +1,50 @@
export const name = "Raptor";
export const url = "https://status.raptortech.com/";
const API_URL =
"https://status.raptortech.com/1.0/status/6501d5abfab4ce052d52e315";
// Status.io status_code → dashboard status
function mapStatusCode(code) {
if (code === 100) return "operational";
if (code === 200) return "degraded"; // planned maintenance
if (code >= 300 && code < 500) return "degraded"; // degraded / partial disruption
if (code >= 500) return "outage"; // service disruption / security event
return "unknown";
}
export async function checkStatus() {
const res = await fetch(API_URL);
if (!res.ok) {
throw new Error(`Raptor status request failed (${res.status})`);
}
const data = await res.json();
const result = data.result ?? {};
const overall = result.status_overall ?? {};
const status = mapStatusCode(overall.status_code);
const incidents = result.incidents ?? [];
let message;
if (incidents.length > 0) {
message = incidents
.map((i) => {
const components = (i.containers_affected ?? [])
.map((c) => c.name)
.join(", ");
return components ? `${i.name} (${components})` : i.name;
})
.join(" | ");
} else {
message = overall.status ?? "All services operational.";
}
return {
name,
status: "operational",
message: "Visitor management online.",
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+26
View File
@@ -0,0 +1,26 @@
export const name = "RAZ-Kids";
export const url = "https://www.raz-kids.com/";
// No public status page. Synthetic check against the teacher login portal.
// Learning A-Z properties are behind Cloudflare bot detection — requires a
// browser User-Agent to avoid 403 challenges.
const PROBE_URL =
"https://accounts.learninga-z.com/ng/member/login?siteAbbr=rk";
const HEADERS = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
};
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "GET", headers: HEADERS });
return {
name,
status: res.ok ? "operational" : "degraded",
message: res.ok
? `Login portal responding (HTTP ${res.status}).`
: `Unexpected response from login portal (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+40
View File
@@ -0,0 +1,40 @@
export const name = "Savvas K-12";
export const url = "https://status.savvas.com/";
const STATUS_URL = "https://status.savvas.com/api/v2/summary.json";
const STATUS_MAP = {
none: "operational",
minor: "degraded",
major: "degraded",
critical: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Savvas status request failed (${res.status})`);
}
const data = await res.json();
const indicator = data.status?.indicator ?? "unknown";
const status = STATUS_MAP[indicator] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = data.status?.description ?? "Status unavailable.";
}
return {
name,
status,
message,
lastUpdated: new Date().toISOString(),
};
}
@@ -0,0 +1,17 @@
export const name = "School Dismissal Manager";
export const url = "https://www.schooldismissalmanager.com/SchoolAdmin/";
// No owned status page — systemstatus.schooldismissalmanager.com redirects
// to StatusGator. Synthetic check against the school admin portal instead.
const PROBE_URL = "https://www.schooldismissalmanager.com/SchoolAdmin/";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: `Admin portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+54 -2
View File
@@ -1,10 +1,62 @@
export const name = "SchoolMessenger";
export const url = "https://status.powerschool.com/";
// SchoolMessenger was acquired by PowerSchool and lives on their status page
// as a group of components. We filter specifically for those rather than
// duplicating the overall PowerSchool check.
const COMPONENTS_URL =
"https://status.powerschool.com/api/v2/components.json";
// Atlassian component status → dashboard status
const STATUS_MAP = {
operational: "operational",
under_maintenance: "degraded",
degraded_performance: "degraded",
partial_outage: "degraded",
major_outage: "outage",
};
const SEVERITY_ORDER = ["operational", "degraded", "outage"];
function worstStatus(a, b) {
return SEVERITY_ORDER.indexOf(a) >= SEVERITY_ORDER.indexOf(b) ? a : b;
}
export async function checkStatus() {
const res = await fetch(COMPONENTS_URL);
if (!res.ok) {
throw new Error(`SchoolMessenger status request failed (${res.status})`);
}
const data = await res.json();
const components = (data.components ?? []).filter((c) =>
c.name.startsWith("SchoolMessenger")
);
if (components.length === 0) {
throw new Error("No SchoolMessenger components found on status page");
}
let overall = "operational";
const issues = [];
for (const c of components) {
const mapped = STATUS_MAP[c.status] ?? "unknown";
overall = worstStatus(overall, mapped);
if (mapped !== "operational") {
issues.push(`${c.name}: ${c.status.replace(/_/g, " ")}`);
}
}
const message =
issues.length > 0 ? issues.join(" | ") : "All services operational.";
return {
name,
status: "operational",
message: "Messaging services operational.",
status: overall,
message,
lastUpdated: new Date().toISOString(),
};
}
+17
View File
@@ -0,0 +1,17 @@
export const name = "SherpaDesk";
export const url = "https://app.sherpadesk.com/new/login/";
// No usable public status API — status.sherpadesk.com is a Pingdom uptime
// report page with an invalid cert. Synthetic check against the app portal.
const PROBE_URL = "https://app.sherpadesk.com/new/login/";
export async function checkStatus() {
const res = await fetch(PROBE_URL, { method: "HEAD" });
return {
name,
status: "operational",
message: `App portal responding (HTTP ${res.status}).`,
lastUpdated: new Date().toISOString(),
};
}
+36
View File
@@ -0,0 +1,36 @@
export const name = "SmartPass";
export const url = "https://smartpass.instatus.com";
const STATUS_URL = "https://smartpass.instatus.com/summary.json";
// Instatus page.status → dashboard status
const STATUS_MAP = {
UP: "operational",
HASISSUES: "degraded",
UNDERMAINTENANCE: "degraded",
DOWN: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`SmartPass status request failed (${res.status})`);
}
const data = await res.json();
const pageStatus = data.page?.status ?? "UNKNOWN";
const status = STATUS_MAP[pageStatus] ?? "unknown";
const message =
status === "operational"
? "All services operating normally."
: `Service status: ${pageStatus}`;
return {
name,
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+38 -1
View File
@@ -1,10 +1,47 @@
import https from "https";
export const name = "SpamTitan";
export const url = "https://mailportal.nhsd.net";
const HOST = "mailportal.nhsd.net";
const TIMEOUT_MS = 8000;
// TODO: TitanHQ has extensive API docs — investigate using the SpamTitan API
// for richer status data (queue depth, filtering stats, etc.) rather than
// a simple synthetic check.
// Synthetic check — hit the appliance directly and see if it responds.
// Uses https.request so we can skip self-signed cert verification.
function probe(host) {
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: host,
path: "/",
method: "HEAD",
rejectUnauthorized: false, // local appliance may have self-signed cert
timeout: TIMEOUT_MS,
},
(res) => resolve(res.statusCode)
);
req.on("timeout", () => {
req.destroy();
reject(new Error("Connection timed out"));
});
req.on("error", reject);
req.end();
});
}
export async function checkStatus() {
const statusCode = await probe(HOST);
return {
name,
status: "operational",
message: "Email filtering operational.",
message: `Mail portal responding (HTTP ${statusCode}).`,
lastUpdated: new Date().toISOString(),
};
}
+51
View File
@@ -0,0 +1,51 @@
export const name = "Study Island";
export const url = "https://status.edmentum.com/";
// Study Island is a component on Edmentum's parent status page.
const STATUS_URL = "https://status.edmentum.com/api/v2/summary.json";
const STATUS_MAP = {
operational: "operational",
degraded_performance: "degraded",
partial_outage: "degraded",
major_outage: "outage",
};
export async function checkStatus() {
const res = await fetch(STATUS_URL);
if (!res.ok) {
throw new Error(`Edmentum status request failed (${res.status})`);
}
const data = await res.json();
const component = (data.components ?? []).find(
(c) => c.name === "Study Island"
);
if (!component) {
throw new Error("Study Island component not found in Edmentum status feed.");
}
const status = STATUS_MAP[component.status] ?? "unknown";
const incidents = data.incidents ?? [];
const activeIncidents = incidents.filter((i) => i.status !== "resolved");
let message;
if (activeIncidents.length > 0) {
message = activeIncidents.map((i) => i.name).join("; ");
} else {
message = component.status === "operational"
? "All Systems Operational"
: component.status.replace(/_/g, " ");
}
return {
name,
status,
message,
lastUpdated: new Date().toISOString(),
};
}
+18
View File
@@ -1,6 +1,9 @@
import "dotenv/config";
import express from "express";
import cors from "cors";
import { providers } from "./providers/index.js";
import { startPolling as startThroughputPolling, getHistory as getThroughputHistory } from "./fortigate-throughput.js";
import { startPolling as startHealthPolling, getHealth as getFgHealth } from "./fortigate-health.js";
const app = express();
const PORT = 3000;
@@ -22,6 +25,9 @@ function safeCheck(provider) {
status: "unknown",
message: `Check failed: ${err.message}`,
lastUpdated: new Date().toISOString(),
})).then((result) => ({
url: provider.url ?? null,
...result,
}));
}
@@ -46,11 +52,23 @@ app.get("/api/status", (_req, res) => {
res.json(cachedStatuses);
});
app.get("/api/throughput", (_req, res) => {
res.json(getThroughputHistory());
});
app.get("/api/fortigate-health", (_req, res) => {
const health = getFgHealth();
if (!health) return res.status(503).json({ error: "Health data not yet available." });
res.json(health);
});
app.get("/api/health", (_req, res) => {
res.json({ ok: true, providers: providers.length });
});
// Poll immediately, then on interval
startThroughputPolling();
startHealthPolling();
await pollAll();
setInterval(pollAll, POLL_INTERVAL);