diff --git a/CLAUDE.md b/CLAUDE.md index 1ade06e..456c9ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,11 +18,11 @@ A web-based status feed aggregator for a K-12 school district IT department. Pro | Google Workspace | Productivity suite | Google Workspace Status Dashboard JSON feed | | Follett | Library management | Synthetic check — district Destiny instance (northhills.follettdestiny.com) | | EdInsight | Data analytics (Harris Education Solutions) | Synthetic check — no public status page found | -| Raptor | Visitor management | Status.io API (status.raptortech.com) | +| Raptor | Visitor management | Status.io API (status.raptortech.com); incidents in "Monitoring" state (state ≥ 300) are suppressed from the message | | SchoolMessenger | Communication platform | Atlassian Statuspage API (PowerSchool status page, SchoolMessenger components filtered) | | McGraw Hill | Curriculum / assessment | Synthetic check — ConnectED portal (status.mcgrawhill.com is JS-rendered) | | Fortinet | Network security | Atlassian Statuspage API (FortiGate Cloud — status.fortigate.forticloud.com) | -| SherpaDesk | Helpdesk / ticketing | Synthetic check — app portal (no public status API) | +| SherpaDesk | Helpdesk / ticketing | Synthetic check — district portal (nhsd.sherpadesk.com); HEAD not supported, uses GET | | Study Island | Instructional practice | Atlassian Statuspage API (Edmentum — status.edmentum.com, Study Island component filtered) | | Classkick | Classroom assessment | Synthetic check — app portal (StatusCast API requires auth token) | | ClassDojo | Classroom communication | Synthetic check — app portal (no machine-readable status feed) | @@ -32,7 +32,7 @@ A web-based status feed aggregator for a K-12 school district IT department. Pro | SmartPass | Hall pass management | Instatus JSON API (smartpass.instatus.com) | | School Dismissal Manager | Dismissal management | Synthetic check — admin portal (status page redirects to StatusGator) | | Promethean | Interactive displays | Synthetic check — prometheanworld.com (panels used in standalone mode; no cloud features) | -| RAZ-Kids | Reading platform | Synthetic check — Learning A-Z login portal (browser UA required; behind Cloudflare bot detection) | +| RAZ-Kids | Reading platform | Synthetic check — Learning A-Z login portal; always returns `unknown` due to Cloudflare managed challenge blocking server-side fetches | | Internet | Connectivity | TCP check to 8.8.8.8:53 | Note: Exchange Online is intentionally excluded — it is a component of M365 Service Health and would be redundant. @@ -41,55 +41,44 @@ New vendors should be added incrementally, not speculatively. ## FortiGate Dashboard Features -In addition to the vendor status cards, the dashboard includes two FortiGate-specific panels that sit above the vendor grid: - -### WAN Throughput Graph -- Two side-by-side canvas graphs, one per WAN link (Crown Castle on port25, Comcast on port8) -- Polls `GET /api/v2/monitor/system/interface` on the FortiGate every 30 seconds -- Computes Mbps from cumulative byte counter deltas -- Stores a 30-minute rolling history (60 points at 30s intervals) -- Frontend fetches `/api/throughput` and renders using HTML5 Canvas - -### FortiGate Health Card -- Shows hostname, firmware version, uptime, CPU %, and memory % -- Polls `GET /api/v2/monitor/system/status` and `GET /api/v2/monitor/system/resource/usage` -- Updates every 2 minutes -- CPU/memory values turn amber at ≥75% and red at ≥90% -- Frontend fetches `/api/fortigate-health` - -Both panels use the built-in Node.js `https` module with `rejectUnauthorized: false` to handle the FortiGate's self-signed management certificate. +The WAN throughput graphs and FortiGate health card were implemented but are currently disabled due to a FortiGate API access issue. The code has been removed from the frontend and backend pending resolution. The `.env` variables below are placeholders for when this is revisited. ## Credentials / Environment `backend/.env` is gitignored and contains: - `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` — Microsoft 365 Graph API -- `FORTIGATE_HOST`, `FORTIGATE_API_TOKEN` — FortiGate REST API -- `FORTIGATE_WAN1_INTERFACE`, `FORTIGATE_WAN1_LABEL` — Crown Castle WAN (port25) -- `FORTIGATE_WAN2_INTERFACE`, `FORTIGATE_WAN2_LABEL` — Comcast WAN (port8) +- `FORTIGATE_HOST`, `FORTIGATE_API_TOKEN` — FortiGate REST API (not currently used) +- `FORTIGATE_WAN1_INTERFACE`, `FORTIGATE_WAN1_LABEL` — Crown Castle WAN (not currently used) +- `FORTIGATE_WAN2_INTERFACE`, `FORTIGATE_WAN2_LABEL` — Comcast WAN (not currently used) + +## Source Control + +- **Remote**: https://git.canadabot.net/canadabot/infrastructure-monitoring-dashboard (remote name: `gitea`) +- Credentials stored in `C:\users\kleins\.gitea_credentials` ## Hosting -- **Web server**: Caddy -- **URL**: https://status.nhsd.net:8443 (port 8443 to avoid conflict with existing Caddy instance on this machine) -- **Access**: Local network only (DNS A record points to the host machine) -- **TLS**: Caddy internal TLS (self-signed). IT staff only — browser cert warnings are acceptable. -- **Important**: There is a separate, pre-existing Caddy instance already running on this machine (unrelated to this project). This project runs its own dedicated Caddy instance using `config/Caddyfile`. Do not confuse the two — always start/stop the dashboard Caddy explicitly with that Caddyfile. +- **Web server**: Shared Caddy instance also used by the staff lifecycle portal (`C:\staff-lifecycle-portal\caddy\Caddyfile`) +- **URL**: https://status.nhsd.net (standard HTTPS, no custom port) +- **Access**: All local network devices (no IP restriction on the status block) +- **TLS**: Caddy internal TLS (self-signed). Browser cert warnings are acceptable; distribute `caddy/data/caddy/pki/authorities/local/root.crt` via Group Policy to eliminate them. +- **DNS**: A record `status.nhsd.net → 10.1.20.214` on nhsd-dc-04p.nhsd.net +- **Caddy reload**: `caddy reload --config "C:\staff-lifecycle-portal\caddy\Caddyfile"` ## Architecture - **Frontend**: HTML/CSS/JS dashboard — lightweight, no heavy framework. Designed to work on a wall-mounted monitor or quick browser check. - **Backend**: Node.js service that polls vendor status on a schedule and caches results. -- **Web server**: Caddy reverse-proxies to the backend API and serves the static frontend. -- **Services**: NSSM runs both Caddy and the Node backend as Windows services. +- **Web server**: Caddy reverse-proxies `/api/*` to the Node backend on port 3000 and serves the static frontend directly. +- **Services**: Node backend runs as an NSSM Windows service named `StatusDashboard` (auto-start). Caddy is managed by the staff-lifecycle-portal project. - **Data flow**: Backend polls vendors → caches to local store → frontend fetches from backend API → auto-refreshes on interval. +- **Logs**: `C:\infrastructure-monitoring-dashboard\logs\backend.log` ## API Endpoints | Endpoint | Description | Poll interval | |---|---|---| | `GET /api/status` | All vendor status cards | 2 minutes | -| `GET /api/throughput` | WAN throughput history (60 points) | 30 seconds | -| `GET /api/fortigate-health` | FortiGate system health | 2 minutes | | `GET /api/health` | Backend liveness check | On demand | ## Directory Structure @@ -100,10 +89,9 @@ infrastructure-monitoring-dashboard/ ├── README.md ├── .gitignore ├── bin/ -│ ├── caddy/ # Drop caddy.exe here (git-ignored) │ └── nssm/ # Drop nssm.exe here (git-ignored) ├── config/ -│ └── Caddyfile # Caddy server configuration +│ └── Caddyfile # Unused — Caddy config lives in the lifecycle portal project ├── frontend/ │ ├── index.html │ ├── css/style.css @@ -111,9 +99,8 @@ infrastructure-monitoring-dashboard/ ├── backend/ │ ├── package.json │ ├── server.js -│ ├── fortigate-throughput.js # WAN throughput poller -│ ├── fortigate-health.js # FortiGate system health poller │ └── providers/ # One module per vendor +├── logs/ # backend.log (git-ignored) └── scripts/ # NSSM service install/uninstall helpers ``` diff --git a/backend/fortigate-health.js b/backend/fortigate-health.js deleted file mode 100644 index 8d30c87..0000000 --- a/backend/fortigate-health.js +++ /dev/null @@ -1,97 +0,0 @@ -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 - ); -} diff --git a/backend/fortigate-throughput.js b/backend/fortigate-throughput.js deleted file mode 100644 index f1aa306..0000000 --- a/backend/fortigate-throughput.js +++ /dev/null @@ -1,118 +0,0 @@ -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 - ); -} diff --git a/backend/providers/classdojo.js b/backend/providers/classdojo.js index 06b4ac7..e042b0c 100644 --- a/backend/providers/classdojo.js +++ b/backend/providers/classdojo.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `App portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `App portal responding (HTTP ${res.status}).` + : `Unexpected response from app portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/classkick.js b/backend/providers/classkick.js index b3b84f5..13d23c0 100644 --- a/backend/providers/classkick.js +++ b/backend/providers/classkick.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `App portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `App portal responding (HTTP ${res.status}).` + : `Unexpected response from app portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/drc.js b/backend/providers/drc.js index 9dfde5b..e2d86cc 100644 --- a/backend/providers/drc.js +++ b/backend/providers/drc.js @@ -1,17 +1,20 @@ export const name = "DRC"; -export const url = "https://wbte.drcedirect.com/PA/portals/pa"; +export const url = "https://status.drcedirect.com/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"; +// Note: /PA/portals/pa returns 500 — /PA/ is the correct probe path. +const PROBE_URL = "https://wbte.drcedirect.com/PA/"; export async function checkStatus() { const res = await fetch(PROBE_URL, { method: "HEAD" }); return { name, - status: "operational", - message: `PA INSIGHT portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `PA INSIGHT portal responding (HTTP ${res.status}).` + : `Unexpected response from PA INSIGHT portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/edinsight.js b/backend/providers/edinsight.js index 6d118f8..5c1b536 100644 --- a/backend/providers/edinsight.js +++ b/backend/providers/edinsight.js @@ -11,8 +11,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `Portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `Portal responding (HTTP ${res.status}).` + : `Unexpected response from portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/follett.js b/backend/providers/follett.js index a520e7e..6541fde 100644 --- a/backend/providers/follett.js +++ b/backend/providers/follett.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `Destiny portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `Destiny portal responding (HTTP ${res.status}).` + : `Unexpected response from Destiny portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/mcgraw-hill.js b/backend/providers/mcgraw-hill.js index ea069d9..e494f6f 100644 --- a/backend/providers/mcgraw-hill.js +++ b/backend/providers/mcgraw-hill.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `ConnectED portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `ConnectED portal responding (HTTP ${res.status}).` + : `Unexpected response from ConnectED portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/microsoft365.js b/backend/providers/microsoft365.js index aec36bb..f098e11 100644 --- a/backend/providers/microsoft365.js +++ b/backend/providers/microsoft365.js @@ -1,5 +1,5 @@ export const name = "Microsoft 365"; -export const url = "https://admin.microsoft.com/Adminportal/Home#/servicehealth"; +export const url = "https://status.cloud.microsoft/m365/referrer=serviceStatusRedirect"; const GRAPH_HEALTH_URL = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/healthOverviews"; diff --git a/backend/providers/promethean.js b/backend/providers/promethean.js index a556fa4..6596d7a 100644 --- a/backend/providers/promethean.js +++ b/backend/providers/promethean.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `Site responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `Site responding (HTTP ${res.status}).` + : `Unexpected response from site (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/raptor.js b/backend/providers/raptor.js index 52b921b..b671f07 100644 --- a/backend/providers/raptor.js +++ b/backend/providers/raptor.js @@ -26,10 +26,19 @@ export async function checkStatus() { const status = mapStatusCode(overall.status_code); const incidents = result.incidents ?? []; - let message; - if (incidents.length > 0) { - message = incidents + // Only surface incidents still being actively investigated or identified. + // State 300 = Monitoring (fix deployed), 400 = Resolved — exclude both. + const activeIncidents = incidents.filter((i) => { + const messages = i.messages ?? []; + if (messages.length === 0) return true; + const latestState = messages[messages.length - 1].state; + return latestState < 300; + }); + + let message; + if (activeIncidents.length > 0) { + message = activeIncidents .map((i) => { const components = (i.containers_affected ?? []) .map((c) => c.name) diff --git a/backend/providers/raz-kids.js b/backend/providers/raz-kids.js index 2ea1f93..a61943a 100644 --- a/backend/providers/raz-kids.js +++ b/backend/providers/raz-kids.js @@ -15,6 +15,16 @@ const HEADERS = { export async function checkStatus() { const res = await fetch(PROBE_URL, { method: "GET", headers: HEADERS }); + // Cloudflare managed challenge — JS required, not a real error + if (res.headers.get("cf-mitigated") === "challenge") { + return { + name, + status: "unknown", + message: "Cloudflare challenge blocked synthetic check — status cannot be determined.", + lastUpdated: new Date().toISOString(), + }; + } + return { name, status: res.ok ? "operational" : "degraded", diff --git a/backend/providers/school-dismissal-manager.js b/backend/providers/school-dismissal-manager.js index 90f43b4..cdb84f4 100644 --- a/backend/providers/school-dismissal-manager.js +++ b/backend/providers/school-dismissal-manager.js @@ -10,8 +10,10 @@ export async function checkStatus() { return { name, - status: "operational", - message: `Admin portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `Admin portal responding (HTTP ${res.status}).` + : `Unexpected response from admin portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/sherpadesk.js b/backend/providers/sherpadesk.js index d6ce7ea..4b63223 100644 --- a/backend/providers/sherpadesk.js +++ b/backend/providers/sherpadesk.js @@ -1,17 +1,19 @@ export const name = "SherpaDesk"; -export const url = "https://app.sherpadesk.com/new/login/"; +export const url = "https://nhsd.sherpadesk.com/"; -// 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/"; +// No usable public status API. Synthetic check against the district portal. +// HEAD returns 404 on this host — use GET. +const PROBE_URL = "https://nhsd.sherpadesk.com/"; export async function checkStatus() { - const res = await fetch(PROBE_URL, { method: "HEAD" }); + const res = await fetch(PROBE_URL, { method: "GET" }); return { name, - status: "operational", - message: `App portal responding (HTTP ${res.status}).`, + status: res.ok ? "operational" : "degraded", + message: res.ok + ? `App portal responding (HTTP ${res.status}).` + : `Unexpected response from app portal (HTTP ${res.status}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/providers/spamtitan.js b/backend/providers/spamtitan.js index 58aa113..ce39668 100644 --- a/backend/providers/spamtitan.js +++ b/backend/providers/spamtitan.js @@ -37,11 +37,14 @@ function probe(host) { export async function checkStatus() { const statusCode = await probe(HOST); + const ok = statusCode >= 200 && statusCode < 400; return { name, - status: "operational", - message: `Mail portal responding (HTTP ${statusCode}).`, + status: ok ? "operational" : "degraded", + message: ok + ? `Mail portal responding (HTTP ${statusCode}).` + : `Unexpected response from mail portal (HTTP ${statusCode}).`, lastUpdated: new Date().toISOString(), }; } diff --git a/backend/server.js b/backend/server.js index 60ca9ab..f888032 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,8 +2,6 @@ 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; @@ -52,23 +50,11 @@ 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); diff --git a/frontend/index.html b/frontend/index.html index f44a3e9..196fd29 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -21,58 +21,6 @@
-