// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching, // Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling. // Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden. import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query" import { sseVerbunden } from "./events" import { api, type AgentStatus, type AktivitaetResp, type ChronikResp, type ConnectResp, type ConnectHealth, type DiscoverResp, type DraftsResp, type GroupsResp, type HermesBrainResp, type Health, type HistoryResp, type IdeenLogResp, type IdeenResp, type Job, type MemoryGraph, type ModelsResp, type Reminder, type RoutingResp, type RoutingPolicyMeta, type ServicesResp, type SystemStatus, type TokenStats, type UpdatesResp, type VoiceTraceResp, type WissenResp, type ZeitmaschineResp, } from "./api" // Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate). export const qk = { health: ["health"] as const, systemStatus: ["system-status"] as const, services: ["services"] as const, models: ["models"] as const, groups: ["groups"] as const, routing: ["routing"] as const, routingPolicy: ["routing-policy"] as const, jobs: ["jobs"] as const, tokenStats: ["token-stats"] as const, agentStatus: ["agent-status"] as const, agentAktivitaet: ["agent-aktivitaet"] as const, hermesBrain: ["hermes-brain"] as const, updates: ["updates"] as const, discover: ["discover"] as const, drafts: (target?: string) => ["drafts", target ?? ""] as const, connect: (params?: string) => ["connect", params ?? ""] as const, connectHealth: ["connect-health"] as const, memoryGraph: ["memory-graph"] as const, voiceTrace: ["voice-trace"] as const, ideen: ["ideen"] as const, chronik: ["chronik"] as const, wissen: ["wissen"] as const, zeitmaschine: ["zeitmaschine"] as const, reminders: ["reminders"] as const, } // ── Polling-Takte ──────────────────────────────────────────────────────────── // EINE Stelle für alle Intervalle. Regel: Die Live-Graphen (System/Token) takten // schnell, alles andere gemächlich — Mutationen invalidieren ohnehin gezielt, // Polling ist nur das Sicherheitsnetz. Komponenten übergeben KEINE eigenen // Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt). const TAKT = { graph: 3_000, // System + Token: seit P4 nur noch Rueckfall/Randdaten (siehe unten) schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace normal: 10_000, // Dienste, Routing, Agent-Status, Health gemuetlich: 30_000, // Chronik, Ideen traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen } as const // UMBAU v3 P3a: Läuft der SSE-Eventstrom (/api/events, siehe lib/events.ts), sind die // Poller der ereignishaften Quellen nur noch SICHERHEITSNETZ — der Strom invalidiert // bei Änderung sofort. Verbunden = Takt ×5; Strom weg = alter Takt (EventSource // reconnectet selbst). Live-Graphen/Status bleiben bewusst beim Polling. const relax = (ms: number) => () => (sseVerbunden() ? ms * 5 : ms) // UMBAU v3 P4: Die MESSWERTE (CPU, RAM, GPU, Temperatur, Token-Zähler, Betriebszeit) // kommen jetzt gepusht — Ereignis `metrik`, jede Sekunde. Der Poller dieser beiden // Endpunkte war die halbe Grundlast der Startseite (Befund B-12). // // Er wird trotzdem NICHT abgeschaltet, sondern gedrosselt. Grund: Beide Antworten tragen // mehr als die Messwerte — `/api/system/status` liefert die Versions-Hashes (Sidebar-Fuß), // `/api/system/token-stats` die Gesamtsumme und die Cloud-Ersparnis, für die das Backend // die Tarife auflöst (Preis-Logik ist dort die einzige Wahrheit, sie hier nachzubauen wäre // eine zweite). Beides ändert sich langsam — einmal pro Minute reicht dafür völlig. // // 3 s → 60 s ist eine Zwanzigstel-Last; abschalten hätte diese Felder einfrieren lassen. const langsamWennStrom = (ms: number) => () => (sseVerbunden() ? 60_000 : ms) // Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht. export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) => useQuery({ queryKey: qk.ideen, queryFn: () => api("/api/ideen"), refetchInterval: relax(refetchInterval), }) // Ideen-Log (Live Worker Log): pollt nur solange Idee triage/running UND offen export const useIdeenLog = (id: string, enabled: boolean = false) => useQuery({ queryKey: ["ideen-log", id], queryFn: () => api(`/api/ideen/${id}/log`), enabled, refetchInterval: 4000, // alle ~4s staleTime: 0, }) // Limit gehört in den Key: zwei Aufrufer mit verschiedenen Limits würden sich sonst // denselben Cache-Eintrag gegenseitig überschreiben. SSE invalidiert das Präfix ["chronik"]. export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlich) => useQuery({ queryKey: [...qk.chronik, limit], queryFn: () => api(`/api/chronik?limit=${limit}`), refetchInterval: relax(refetchInterval), select: (d) => d.items ?? [], }) export const useWissen = () => useQuery({ queryKey: qk.wissen, queryFn: () => api("/api/wissen") }) export const useZeitmaschine = (refetchInterval: number = TAKT.traege, enabled = true) => useQuery({ queryKey: qk.zeitmaschine, queryFn: () => api("/api/zeitmaschine"), refetchInterval, enabled, }) export const useReminders = (refetchInterval: number = TAKT.traege) => useQuery({ queryKey: qk.reminders, queryFn: () => api<{ items: Reminder[] }>("/api/reminders"), refetchInterval: relax(refetchInterval), select: (d) => d.items ?? [], }) // Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown). export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell) => useQuery({ queryKey: qk.voiceTrace, queryFn: () => api(`/api/voice/trace?limit=${limit}`), refetchInterval, select: (d) => d.turns ?? [], }) export const useMemoryGraph = (enabled = true) => useQuery({ queryKey: qk.memoryGraph, queryFn: () => api("/api/wissen/graph"), enabled, staleTime: 60 * 1000, }) export const useHealth = () => useQuery({ queryKey: qk.health, queryFn: () => api("/api/health"), refetchInterval: TAKT.normal }) export const useSystemStatus = (refetchInterval: number = TAKT.graph) => useQuery({ queryKey: qk.systemStatus, queryFn: () => api("/api/system/status"), refetchInterval: langsamWennStrom(refetchInterval) }) // Metrik-Verlauf (Cockpit-Zeitachse): 1h/24h aus dem Backend-Ringpuffer. Nur aktiv, // wenn eine Karte gerade diesen Bereich zeigt (enabled) — Live bleibt beim 3s-Store. export const useMetricHistory = (minutes: number, enabled = true) => useQuery({ queryKey: ["metrics-history", minutes], queryFn: () => api(`/api/system/history?minutes=${minutes}`), enabled, refetchInterval: 60_000, }) export const useServices = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.services, queryFn: () => api("/api/system/services"), refetchInterval }) // Der SSE-Strom invalidiert "models" bei Laden/Entladen → Poller ist nur Sicherheitsnetz. export const useModels = (refetchInterval: number = TAKT.schnell) => useQuery({ queryKey: qk.models, queryFn: () => api("/api/models"), refetchInterval: relax(refetchInterval) }) export const useGroups = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.groups, queryFn: () => api("/api/groups"), refetchInterval }) export const useRouting = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.routing, queryFn: () => api("/api/routing"), refetchInterval }) export const useRoutingPolicy = () => useQuery({ queryKey: qk.routingPolicy, queryFn: () => api("/api/routing/policy") }) // Jobs (Downloads) takten etwas schneller, damit Fortschritts-Balken flüssig wirken. // `enabled=false` pausiert den Beobachter (z. B. geschlossene System-Schublade) — // der Cache bleibt geteilt, es pollt nur niemand umsonst. export const useJobs = (refetchInterval = 3_000, enabled = true) => useQuery({ queryKey: qk.jobs, queryFn: () => api<{ jobs: Job[] }>("/api/jobs"), // Seit P4 meldet der Fingerabdruck-Sammler Zustand UND Fortschritt der Jobs — // der Poller ist damit auch hier nur noch Sicherheitsnetz. refetchInterval: relax(refetchInterval), enabled, select: (d) => d.jobs ?? [], }) export const useTokenStats = (refetchInterval: number = TAKT.graph) => useQuery({ queryKey: qk.tokenStats, queryFn: () => api("/api/system/token-stats"), refetchInterval: langsamWennStrom(refetchInterval) }) export const useAgentStatus = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.agentStatus, queryFn: () => api("/api/agent/status"), refetchInterval }) // Werkzeug-Verlauf des Agenten. Liest das Ende von Hermes' Log — billig, aber es // aendert sich nur, wenn Lucy wirklich arbeitet; der gemuetliche Takt reicht. export const useWerkzeugverlauf = (refetchInterval: number = TAKT.gemuetlich) => useQuery({ queryKey: qk.agentAktivitaet, queryFn: () => api("/api/agent/aktivitaet?limit=80"), refetchInterval: relax(refetchInterval), }) // Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage). export const useHermesBrain = (refetchInterval: number = TAKT.traege) => useQuery({ queryKey: qk.hermesBrain, queryFn: () => api("/api/agent/brain"), refetchInterval }) // Updates-Check ist teuer (apt/git auf der Box) — träge pollen reicht völlig. export const useUpdates = (refetchInterval: number = TAKT.traege) => useQuery({ queryKey: qk.updates, queryFn: () => api("/api/maintenance/updates"), refetchInterval }) export const useDiscover = () => useQuery({ queryKey: qk.discover, queryFn: () => api("/api/discover") }) // Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad). export const useDrafts = (target?: string) => useQuery({ queryKey: qk.drafts(target), queryFn: () => api(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`), enabled: !!target, }) export const useConnect = (params?: string) => useQuery({ queryKey: qk.connect(params), queryFn: () => api(params ? `/api/connect?${params}` : "/api/connect"), }) // Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert. export const useConnectHealth = () => useQuery({ queryKey: qk.connectHealth, queryFn: () => api("/api/connect/health"), refetchInterval: 15000, }) /** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */ export function invalidate(qc: QueryClient, ...keys: (readonly unknown[])[]) { for (const key of keys) qc.invalidateQueries({ queryKey: key }) } export { useQueryClient }