71192b3db0
Paket 1 - Metrik-Historie mit Zeitachse (User: 'WANN war Last?'): - services/metrics_history.py: 10s-Sampler (CPU/RAM/GPU/Disk + Token-Totale) in 24h-Ringpuffer (aelteres faellt automatisch raus, nichts waechst unbegrenzt), persistiert alle 5 min -> uebersteht Deploys; GET /api/system/history?minutes mit Downsampling auf ~300 Punkte; Lifespan-Task in app.py - Cockpit-Karten System-Status + Token-Durchsatz: Bereichs-Schalter Live/1h/24h, sichtbare Zeitachse (HH:MM:SS bei kurzen, HH:MM bei langen Fenstern), Tooltip zeigt Uhrzeit; Token-Raten in 1h/24h aus Totale-Deltas Paket 2 - Verbinden selbsterklaerend: - Box-IP vorbefuellt aus window.location.hostname (Dev-Fallback bleibt) - MCP-Scriptpfad-Feld aus der Kopfzeile in die 'Gedaechtnis anbinden'-Karte verschoben, mit Erklaerung WANN man es braucht Paket 3 - Modelltausch-Loecher gestopft (Review 16.07.): - install: Registrierung OHNE Alias-Umzug; Rolle wird erst NACH erfolgreichem Download uebernommen (on_done) - hermes dabei durch den warm-bewussten set_agent_brain-Flow statt rohem Alias-Move (Lucy waere sonst bis Download- Ende tot gewesen); Re-Install erhaelt bestehende Aliase - set_role_alias: lebenswichtige Aliase (hermes/embed) des Ziel-Modells ueberleben jeden Rollen-Klick (Qwen3.6 haelt live hermes+fast!) - Rollen-Endpoint verweigert Rollen-Wechsel am Halter geschuetzter Aliase mit klarer Anleitung; Werkbank sperrt die Rollen-Chips sichtbar Verifiziert: py_compile + Sampler-Smoke (2 Punkte, Persist ok) + Alias-Logik- Unittest (3 Faelle) + Browser (Zeitachse tickt, Schalter, Leerzustand, Verbinden). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
// 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 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,
|
||
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<AuftragsbuchResp>("/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<IdeenResp>("/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<IdeenLogResp>(`/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<ChronikResp>(`/api/chronik?limit=${limit}`),
|
||
refetchInterval: relax(refetchInterval),
|
||
select: (d) => d.items ?? [],
|
||
})
|
||
|
||
export const useWissen = () =>
|
||
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
||
|
||
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
||
useQuery({
|
||
queryKey: qk.eigenleben,
|
||
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
||
refetchInterval,
|
||
})
|
||
|
||
export const useZeitmaschine = (refetchInterval: number = TAKT.traege, enabled = true) =>
|
||
useQuery({
|
||
queryKey: qk.zeitmaschine,
|
||
queryFn: () => api<ZeitmaschineResp>("/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<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
||
refetchInterval,
|
||
select: (d) => d.turns ?? [],
|
||
})
|
||
|
||
export const useMemoryGraph = (enabled = true) =>
|
||
useQuery({
|
||
queryKey: qk.memoryGraph,
|
||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||
enabled,
|
||
})
|
||
|
||
export const useHealth = () =>
|
||
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: TAKT.normal })
|
||
|
||
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
|
||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/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<HistoryResp>(`/api/system/history?minutes=${minutes}`),
|
||
enabled,
|
||
refetchInterval: 60_000,
|
||
})
|
||
|
||
export const useServices = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/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<ModelsResp>("/api/models"), refetchInterval: relax(refetchInterval) })
|
||
|
||
export const useGroups = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
||
|
||
export const useRouting = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||
|
||
export const useRoutingPolicy = () =>
|
||
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/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<TokenStats>("/api/system/token-stats"), refetchInterval })
|
||
|
||
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/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<HermesBrainResp>("/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<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
||
|
||
export const useDiscover = () =>
|
||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/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<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||
enabled: !!target,
|
||
})
|
||
|
||
export const useConnect = (params?: string) =>
|
||
useQuery({
|
||
queryKey: qk.connect(params),
|
||
queryFn: () => api<ConnectResp>(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<ConnectHealth>("/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<Memory[]>(`/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 }
|