// How often to fetch status (ms) const REFRESH_INTERVAL = 60_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(`