// 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(`
${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); } // Initialize updateClock(); setInterval(updateClock, 1000); refresh(); setInterval(refresh, REFRESH_INTERVAL);