1"use client";23import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";4import type { Job, ManagerState, Telemetry } from "./types";56interface LiveEvent {7 seq: number;8 ts: number;9 type: string;10 data: Record<string, unknown>;11}1213interface LiveState {14 connected: boolean;15 manager: ManagerState | null;16 metrics: Telemetry | null;17 metricsHistory: Telemetry[];18 jobs: Record<string, Job>;19 modelEvents: Record<string, Record<string, unknown>>;20 alerts: { ts: number; level: string; message: string }[];21 requests: Record<string, unknown>[];22 version: number; // bumps on model/manager/settings changes so pages can refetch23}2425const Ctx = createContext<LiveState>({26 connected: false,27 manager: null,28 metrics: null,29 metricsHistory: [],30 jobs: {},31 modelEvents: {},32 alerts: [],33 requests: [],34 version: 0,35});3637export function LiveProvider({ children, enabled }: { children: React.ReactNode; enabled: boolean }) {38 const [state, setState] = useState<LiveState>({39 connected: false, manager: null, metrics: null, metricsHistory: [], jobs: {}, modelEvents: {}, alerts: [], requests: [], version: 0,40 });41 const esRef = useRef<EventSource | null>(null);4243 useEffect(() => {44 if (!enabled) return;45 let stopped = false;46 let retry = 1000;47 const connect = () => {48 if (stopped) return;49 const es = new EventSource("/api/events");50 esRef.current = es;51 es.onopen = () => {52 retry = 1000;53 setState((s) => ({ ...s, connected: true }));54 };55 es.onerror = () => {56 es.close();57 setState((s) => ({ ...s, connected: false }));58 if (!stopped) setTimeout(connect, (retry = Math.min(retry * 1.6, 15000)));59 };60 const handle = (ev: MessageEvent) => {61 let e: LiveEvent;62 try {63 e = JSON.parse(ev.data);64 } catch {65 return;66 }67 setState((s) => {68 const n = { ...s };69 switch (e.type) {70 case "snapshot": {71 const d = e.data as { manager: ManagerState; metrics: Telemetry | null; jobs: Job[] };72 n.manager = d.manager;73 if (d.metrics) n.metrics = d.metrics;74 n.jobs = Object.fromEntries((d.jobs || []).map((j) => [j.id, j]));75 n.version++;76 break;77 }78 case "manager":79 n.manager = e.data as unknown as ManagerState;80 n.version++;81 break;82 case "metrics": {83 const t = e.data as unknown as Telemetry;84 n.metrics = t;85 n.metricsHistory = [...s.metricsHistory.slice(-119), t];86 break;87 }88 case "job": {89 const j = e.data as unknown as Job;90 n.jobs = { ...s.jobs, [j.id]: j };91 if (j.status === "completed" && (j.kind === "download" || j.kind === "scan" || j.kind === "benchmark")) n.version++;92 break;93 }94 case "model":95 n.modelEvents = { ...s.modelEvents, [String(e.data.model_id)]: e.data };96 n.version++;97 break;98 case "models":99 case "settings":100 case "server":101 n.version++;102 break;103 case "alert":104 n.alerts = [...s.alerts.slice(-9), { ts: e.ts, level: String(e.data.level), message: String(e.data.message) }];105 break;106 case "request":107 n.requests = [e.data, ...s.requests.slice(0, 49)];108 break;109 }110 return n;111 });112 };113 for (const t of ["snapshot", "manager", "metrics", "job", "model", "models", "settings", "server", "alert", "request"]) {114 es.addEventListener(t, handle as EventListener);115 }116 };117 connect();118 return () => {119 stopped = true;120 esRef.current?.close();121 };122 }, [enabled]);123124 const value = useMemo(() => state, [state]);125 return <Ctx.Provider value={value}>{children}</Ctx.Provider>;126}127128export function useLive() {129 return useContext(Ctx);130}131