// How often to fetch status (ms) const REFRESH_INTERVAL = 60_000; // How often to fetch WAN throughput (ms) — matches backend poll rate const THROUGHPUT_INTERVAL = 30_000; // Status severity for sorting (higher = more severe = shown first) const SEVERITY = { outage: 3, degraded: 2, unknown: 1, operational: 0 }; // Track first render for entrance animation let initialLoad = true; // Simple HTML escaping for template safety function esc(str) { const el = document.createElement("span"); el.textContent = str; return el.innerHTML; } // Mock data — will be replaced by /api/status once the backend is wired up function getMockData() { return [ { name: "Microsoft 365", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 120_000).toISOString() }, { name: "SpamTitan", status: "operational", message: "Email filtering operational.", lastUpdated: new Date(Date.now() - 300_000).toISOString() }, { name: "PowerSchool", status: "degraded", message: "Slow response times reported. PowerSchool is investigating.", lastUpdated: new Date(Date.now() - 60_000).toISOString() }, { name: "Classlink", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 180_000).toISOString() }, { name: "Apple", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 600_000).toISOString() }, { name: "DRC", status: "outage", message: "INSIGHT portal is currently unavailable. DRC is aware of the issue.", lastUpdated: new Date(Date.now() - 45_000).toISOString() }, { name: "FinalSite", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 240_000).toISOString() }, { name: "Google Workspace", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 90_000).toISOString() }, { name: "Follett", status: "operational", message: "All services running normally.", lastUpdated: new Date(Date.now() - 350_000).toISOString() }, { name: "EdInsight", status: "operational", message: "Data analytics platform operational.", lastUpdated: new Date(Date.now() - 200_000).toISOString() }, { name: "Raptor", status: "operational", message: "Visitor management online.", lastUpdated: new Date(Date.now() - 400_000).toISOString() }, { name: "SchoolMessenger", status: "operational", message: "Messaging services operational.", lastUpdated: new Date(Date.now() - 150_000).toISOString() }, { name: "Fortinet", status: "operational", message: "FortiGuard and FortiCloud services operational.", lastUpdated: new Date(Date.now() - 500_000).toISOString() }, { name: "McGraw Hill", status: "operational", message: "All platform services operational.", lastUpdated: new Date(Date.now() - 270_000).toISOString() }, { name: "Local Infrastructure", status: "operational", message: "Firewall uptime 42d. WAN throughput nominal.", lastUpdated: new Date(Date.now() - 30_000).toISOString() } ]; } // Fetch vendor status from the backend API, falling back to mock data async function fetchStatus() { try { const res = await fetch("/api/status"); if (!res.ok) throw new Error(res.statusText); return await res.json(); } catch { // Backend not available yet — use mock data return getMockData(); } } // Format an ISO timestamp as a relative or short local time string function formatTime(iso) { const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return new Date(iso).toLocaleDateString(); } // Render the summary bar (counts by status) function renderSummary(vendors) { const counts = { operational: 0, degraded: 0, outage: 0, unknown: 0 }; vendors.forEach(v => { counts[v.status] = (counts[v.status] || 0) + 1; }); const bar = document.getElementById("summary-bar"); const items = []; if (counts.outage > 0) { items.push(`
${counts.outage} Outage
`); } if (counts.degraded > 0) { items.push(`
${counts.degraded} Degraded
`); } items.push(`
${counts.operational} Operational
`); if (counts.unknown > 0) { items.push(`
${counts.unknown} Unknown
`); } bar.innerHTML = items.join(""); } // Render vendor cards into the grid, sorted by severity function renderGrid(vendors) { const sorted = [...vendors].sort((a, b) => { const severityDiff = (SEVERITY[b.status] || 0) - (SEVERITY[a.status] || 0); if (severityDiff !== 0) return severityDiff; return a.name.localeCompare(b.name); }); const grid = document.getElementById("vendor-grid"); grid.innerHTML = sorted.map((v, i) => { const animClass = initialLoad ? "animate-in" : ""; const animStyle = initialLoad ? `style="--d: ${i * 0.07}s"` : ""; const nameHtml = v.url ? `${esc(v.name)}` : esc(v.name); return `
${nameHtml}
${esc(v.status)}

${esc(v.message)}

Updated ${esc(formatTime(v.lastUpdated))}
`; }).join(""); if (initialLoad) initialLoad = false; } // Orchestrate a full render pass function render(vendors) { renderSummary(vendors); renderGrid(vendors); document.getElementById("last-refreshed").textContent = `Refreshed ${new Date().toLocaleTimeString()}`; } // Update the live clock display function updateClock() { document.getElementById("clock").textContent = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } // Fetch and render async function refresh() { const vendors = await fetchStatus(); render(vendors); } // ===== WAN THROUGHPUT GRAPH ===== async function fetchThroughput() { try { const res = await fetch("/api/throughput"); if (!res.ok) throw new Error(res.statusText); return await res.json(); } catch { return []; } } // Round up to the nearest "nice" scale value for the Y axis function niceMax(val) { const steps = [10, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000]; return steps.find((s) => s >= val) ?? Math.ceil(val * 1.2); } function fmtMbps(mbps) { if (mbps >= 1000) return `${(mbps / 1000).toFixed(1)} Gbps`; if (mbps >= 100) return `${Math.round(mbps)} Mbps`; if (mbps >= 10) return `${mbps.toFixed(1)} Mbps`; return `${mbps.toFixed(2)} Mbps`; } function drawGraph(canvas, points, wanKey) { const dpr = window.devicePixelRatio || 1; const W = canvas.offsetWidth; const H = canvas.offsetHeight; canvas.width = W * dpr; canvas.height = H * dpr; const ctx = canvas.getContext("2d"); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H); if (points.length < 2) { ctx.fillStyle = "#484f58"; ctx.font = "11px Consolas, monospace"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("Waiting for data…", W / 2, H / 2); return; } const PAD = { top: 6, right: 6, bottom: 4, left: 42 }; const plotW = W - PAD.left - PAD.right; const plotH = H - PAD.top - PAD.bottom; const allVals = points.flatMap((p) => [p[wanKey].rxMbps, p[wanKey].txMbps]); const maxVal = niceMax(Math.max(...allVals, 1)); // Grid lines and Y-axis labels ctx.font = "9px Consolas, monospace"; ctx.textAlign = "right"; ctx.textBaseline = "middle"; [0, 0.5, 1].forEach((frac) => { const y = PAD.top + plotH * (1 - frac); ctx.strokeStyle = "#21262d"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(PAD.left, y); ctx.lineTo(PAD.left + plotW, y); ctx.stroke(); if (frac > 0) { ctx.fillStyle = "#484f58"; ctx.fillText(fmtMbps(maxVal * frac), PAD.left - 5, y); } }); // Draw a line with a fill underneath function drawLine(getValue, strokeColor, fillColor) { const n = points.length; const pts = points.map((p, i) => ({ x: PAD.left + (i / (n - 1)) * plotW, y: PAD.top + plotH * (1 - Math.max(0, getValue(p)) / maxVal), })); // Fill ctx.beginPath(); ctx.moveTo(pts[0].x, PAD.top + plotH); pts.forEach((pt) => ctx.lineTo(pt.x, pt.y)); ctx.lineTo(pts[n - 1].x, PAD.top + plotH); ctx.closePath(); ctx.fillStyle = fillColor; ctx.fill(); // Stroke ctx.beginPath(); pts.forEach((pt, i) => (i === 0 ? ctx.moveTo(pt.x, pt.y) : ctx.lineTo(pt.x, pt.y))); ctx.strokeStyle = strokeColor; ctx.lineWidth = 1.5; ctx.lineJoin = "round"; ctx.stroke(); } drawLine((p) => p[wanKey].rxMbps, "#58a6ff", "rgba(88, 166, 255, 0.10)"); drawLine((p) => p[wanKey].txMbps, "#3fb950", "rgba(63, 185, 80, 0.10)"); } function renderThroughput(points) { if (points.length === 0) return; const latest = points[points.length - 1]; // Update labels from the data (picks up names from .env via backend) document.getElementById("wan1-name").textContent = latest.wan1.label; document.getElementById("wan2-name").textContent = latest.wan2.label; document.getElementById("wan1-rx").textContent = `${fmtMbps(latest.wan1.rxMbps)} ↓`; document.getElementById("wan1-tx").textContent = `${fmtMbps(latest.wan1.txMbps)} ↑`; document.getElementById("wan2-rx").textContent = `${fmtMbps(latest.wan2.rxMbps)} ↓`; document.getElementById("wan2-tx").textContent = `${fmtMbps(latest.wan2.txMbps)} ↑`; drawGraph(document.getElementById("wan1-canvas"), points, "wan1"); drawGraph(document.getElementById("wan2-canvas"), points, "wan2"); } async function refreshThroughput() { const points = await fetchThroughput(); renderThroughput(points); } // ===== FORTIGATE HEALTH CARD ===== async function fetchFgHealth() { try { const res = await fetch("/api/fortigate-health"); if (!res.ok) return null; return await res.json(); } catch { return null; } } function renderFgHealth(health) { if (!health) return; document.getElementById("fg-model").textContent = `FortiGate ${health.model}`; document.getElementById("fg-hostname").textContent = health.hostname; document.getElementById("fg-version").textContent = health.version; document.getElementById("fg-uptime").textContent = health.uptimeLabel; const cpuEl = document.getElementById("fg-cpu"); cpuEl.textContent = health.cpuPct !== null ? `${health.cpuPct}%` : "—"; cpuEl.className = "fg-metric-value" + (health.cpuPct >= 90 ? " crit" : health.cpuPct >= 75 ? " warn" : ""); const memEl = document.getElementById("fg-mem"); memEl.textContent = health.memPct !== null ? `${health.memPct}%` : "—"; memEl.className = "fg-metric-value" + (health.memPct >= 90 ? " crit" : health.memPct >= 75 ? " warn" : ""); const dot = document.getElementById("fg-status-dot"); const stressed = health.cpuPct >= 90 || health.memPct >= 90; dot.className = `status-indicator ${stressed ? "degraded" : "operational"}`; } async function refreshFgHealth() { const health = await fetchFgHealth(); renderFgHealth(health); } // Initialize updateClock(); setInterval(updateClock, 1000); refresh(); setInterval(refresh, REFRESH_INTERVAL); refreshThroughput(); setInterval(refreshThroughput, THROUGHPUT_INTERVAL); refreshFgHealth(); setInterval(refreshFgHealth, 2 * 60 * 1000);