// ── SchematicIQ Ops Dashboard — dashboard-data.js ── // Reads event blobs and returns summarized metrics // Endpoint: GET /api/dashboard-data let blobsAvailable = true; function headers() { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "GET, OPTIONS", "Content-Type": "application/json", "Cache-Control": "no-store" }; } function percentile(arr, pct) { if (!arr.length) return 0; const sorted = [...arr].sort((a, b) => a - b); return sorted[Math.min(sorted.length - 1, Math.floor((pct / 100) * sorted.length))]; } function summarize(items) { const now = Date.now(); const last24 = items.filter(x => now - new Date(x.timestamp).getTime() <= 86400000); const successes = last24.filter(x => x.eventType === "success"); const failures = last24.filter(x => x.eventType === "failure"); const fallbacks = last24.filter(x => x.eventType === "fallback"); const tierClicks = last24.filter(x => x.eventType === "tier_click"); const total = successes.length + failures.length + fallbacks.length; const latencies = last24 .filter(x => x.responseTimeMs > 0) .map(x => Number(x.responseTimeMs)) .sort((a, b) => a - b); // Per-hour breakdown (last 24h) const perHourMap = {}; for (let i = 23; i >= 0; i--) { const d = new Date(now - i * 3600000); const key = d.getHours().toString().padStart(2, "0") + ":00"; perHourMap[key] = { hour: key, requests: 0, failures: 0 }; } last24.forEach(item => { const d = new Date(item.timestamp); const key = d.getHours().toString().padStart(2, "0") + ":00"; if (!perHourMap[key]) return; if (["success","failure","fallback"].includes(item.eventType)) perHourMap[key].requests++; if (item.eventType === "failure") perHourMap[key].failures++; }); // By vertical const vertMap = {}; last24.forEach(e => { if (e.vertical) vertMap[e.vertical] = (vertMap[e.vertical] || 0) + 1; }); const byVertical = Object.entries(vertMap) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value); // By tier const byTier = { Good: 0, Better: 0, Best: 0 }; tierClicks.forEach(e => { if (byTier[e.tierSelected] !== undefined) byTier[e.tierSelected]++; }); // Open incidents (recent failures) const openIncidents = failures.slice(0, 5).map((item, idx) => ({ id: item.id || `incident-${idx}`, severity: item.errorType === "outage" ? "P1" : (item.errorType === "api" || item.errorType === "timeout" ? "P2" : "P3"), title: item.message || "Generation failure", vertical: item.vertical || "Unknown", ageMinutes: Math.max(1, Math.round((now - new Date(item.timestamp).getTime()) / 60000)), owner: "Ops Queue", status: "Investigating", })); return { totalRequests: total, successCount: successes.length, failureCount: failures.length, fallbackCount: fallbacks.length, successRate: total > 0 ? Math.round(successes.length / total * 100) : 100, fallbackRate: total > 0 ? Math.round(fallbacks.length / total * 100) : 0, avgLatencyMs: latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0, p50LatencyMs: percentile(latencies, 50), p95LatencyMs: percentile(latencies, 95), p99LatencyMs: percentile(latencies, 99), byVertical, byTier, perHour: Object.values(perHourMap), openIncidents, }; } exports.handler = async function(event) { const h = headers(); if (event.httpMethod === "OPTIONS") return { statusCode: 200, headers: h, body: "" }; if (event.httpMethod !== "GET") return { statusCode: 405, headers: h, body: JSON.stringify({ error: "GET only" }) }; let items = []; // Try Netlify Blobs if (blobsAvailable) { try { const { getStore } = require("@netlify/blobs"); const store = getStore("ops-events"); const { blobs } = await store.list(); const reads = await Promise.all( blobs.slice(0, 500).map(b => store.get(b.key, { type: "json" }).catch(() => null)) ); items = reads.filter(Boolean); } catch(err) { console.warn("Blobs unavailable:", err.message); blobsAvailable = false; } } const summary = summarize(items); return { statusCode: 200, headers: h, body: JSON.stringify({ events: items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, 200), summary, meta: { totalStored: items.length, blobsAvailable, generatedAt: new Date().toISOString(), isDemoData: items.length === 0, } }) }; };