// 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 AuftragsbuchResp, type ChronikResp, type ConnectResp, type ConnectHealth, type DiscoverResp, type DraftsResp, type EigenlebenResp, type GovernorStatus, type GroupsResp, type HermesBrainResp, type Health, type HistoryResp, type IdeenLogResp, type IdeenResp, type Job, type Memory, 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, 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, memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const, memoryGraph: ["memory-graph"] as const, voiceTrace: ["voice-trace"] as const, auftragsbuch: ["auftragsbuch"] as const, ideen: ["ideen"] as const, chronik: ["chronik"] as const, governor: ["governor"] as const, eigenleben: ["eigenleben"] 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, // Live-Graphen im Cockpit (System, Token-Durchsatz) schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch 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) // Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende // Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden. export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) => useQuery({ queryKey: qk.auftragsbuch, queryFn: () => api("/api/auftragsbuch"), refetchInterval: relax(refetchInterval), }) // 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 useEigenleben = (refetchInterval: number = TAKT.traege) => useQuery({ queryKey: qk.eigenleben, queryFn: () => api("/api/eigenleben"), refetchInterval, }) 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 ?? [], }) // Token-Wächter (:8100). Ist er aus, liefert das Backend `reachable:false` statt zu werfen — // deshalb kein Retry-Sturm und kein Fehlerzustand in der Kachel. export const useGovernor = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.governor, queryFn: () => api("/api/governor"), refetchInterval, retry: false, }) export const useMemoryGraph = (enabled = true) => useQuery({ queryKey: qk.memoryGraph, queryFn: () => api("/api/memory/graph"), enabled, }) 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 }) // 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"), refetchInterval, enabled, select: (d) => d.jobs ?? [], }) export const useTokenStats = (refetchInterval: number = TAKT.graph) => useQuery({ queryKey: qk.tokenStats, queryFn: () => api("/api/system/token-stats"), refetchInterval }) export const useAgentStatus = (refetchInterval: number = TAKT.normal) => useQuery({ queryKey: qk.agentStatus, queryFn: () => api("/api/agent/status"), 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, }) export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) => useQuery({ queryKey: qk.memory(opts?.q, opts?.category), queryFn: () => { const p = new URLSearchParams() if (opts?.q) p.set("q", opts.q) if (opts?.category) p.set("category", opts.category) return api(`/api/memory?${p}`) }, select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d), }) /** 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 }