Files
mission-control-v2/frontend/src/lib/queries.ts
T
Hitonabi 71192b3db0 Zeitachse (24h-Historie), Verbinden-Autofill, Modelltausch abgesichert
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>
2026-07-16 01:08:03 +02:00

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 }