"use client"; import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; import type { Job, ManagerState, Telemetry } from "./types"; interface LiveEvent { seq: number; ts: number; type: string; data: Record; } interface LiveState { connected: boolean; manager: ManagerState | null; metrics: Telemetry | null; metricsHistory: Telemetry[]; jobs: Record; modelEvents: Record>; alerts: { ts: number; level: string; message: string }[]; requests: Record[]; version: number; // bumps on model/manager/settings changes so pages can refetch } const Ctx = createContext({ connected: false, manager: null, metrics: null, metricsHistory: [], jobs: {}, modelEvents: {}, alerts: [], requests: [], version: 0, }); export function LiveProvider({ children, enabled }: { children: React.ReactNode; enabled: boolean }) { const [state, setState] = useState({ connected: false, manager: null, metrics: null, metricsHistory: [], jobs: {}, modelEvents: {}, alerts: [], requests: [], version: 0, }); const esRef = useRef(null); useEffect(() => { if (!enabled) return; let stopped = false; let retry = 1000; const connect = () => { if (stopped) return; const es = new EventSource("/api/events"); esRef.current = es; es.onopen = () => { retry = 1000; setState((s) => ({ ...s, connected: true })); }; es.onerror = () => { es.close(); setState((s) => ({ ...s, connected: false })); if (!stopped) setTimeout(connect, (retry = Math.min(retry * 1.6, 15000))); }; const handle = (ev: MessageEvent) => { let e: LiveEvent; try { e = JSON.parse(ev.data); } catch { return; } setState((s) => { const n = { ...s }; switch (e.type) { case "snapshot": { const d = e.data as { manager: ManagerState; metrics: Telemetry | null; jobs: Job[] }; n.manager = d.manager; if (d.metrics) n.metrics = d.metrics; n.jobs = Object.fromEntries((d.jobs || []).map((j) => [j.id, j])); n.version++; break; } case "manager": n.manager = e.data as unknown as ManagerState; n.version++; break; case "metrics": { const t = e.data as unknown as Telemetry; n.metrics = t; n.metricsHistory = [...s.metricsHistory.slice(-119), t]; break; } case "job": { const j = e.data as unknown as Job; n.jobs = { ...s.jobs, [j.id]: j }; if (j.status === "completed" && (j.kind === "download" || j.kind === "scan" || j.kind === "benchmark")) n.version++; break; } case "model": n.modelEvents = { ...s.modelEvents, [String(e.data.model_id)]: e.data }; n.version++; break; case "models": case "settings": case "server": n.version++; break; case "alert": n.alerts = [...s.alerts.slice(-9), { ts: e.ts, level: String(e.data.level), message: String(e.data.message) }]; break; case "request": n.requests = [e.data, ...s.requests.slice(0, 49)]; break; } return n; }); }; for (const t of ["snapshot", "manager", "metrics", "job", "model", "models", "settings", "server", "alert", "request"]) { es.addEventListener(t, handle as EventListener); } }; connect(); return () => { stopped = true; esRef.current?.close(); }; }, [enabled]); const value = useMemo(() => state, [state]); return {children}; } export function useLive() { return useContext(Ctx); }