9e432dbd2d
Teil 1: VoiceLatencyCard auf dem Dashboard (GET /api/voice/metrics, C2) — zeigt STT/Vision/Chat-TTFB/TTS mit p50/p95/last + count. Teil 2: UI-editierbare Routing-Policy. Neuer routing_policy.py (hot-reload JSON unter MODELS_DIR/mc2-routing.json, Env=Defaults, atomarer Write, Validierung). router_logic, gateway_proxy und gateway.routing_summary lesen jetzt live via load_policy(); routing_summary ist lane-bewusst (chat/coding statt altem auto). Neue Endpoints GET/PUT /api/routing/policy. Teil 3: LaneEditor.tsx als ZONE im Cockpit (chat/coding-Aliase + Schwellen + fast_no_think, Speichern/Default-je-Feld); Gateway-Node zeigt die Lanes. Verifiziert: npm run build (tsc strict) clean, FastAPI TestClient (GET/PUT, Validierung, Persistenz, Hot-reload durch die API), venv-Smoke (Routing). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
5.5 KiB
TypeScript
147 lines
5.5 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 {
|
|
api,
|
|
type AgentStatus,
|
|
type ConnectResp,
|
|
type ConnectHealth,
|
|
type DiscoverResp,
|
|
type DraftsResp,
|
|
type GroupsResp,
|
|
type HermesBrainResp,
|
|
type Health,
|
|
type Job,
|
|
type Memory,
|
|
type MemoryGraph,
|
|
type ModelsResp,
|
|
type RoutingResp,
|
|
type RoutingPolicyMeta,
|
|
type ServicesResp,
|
|
type SystemStatus,
|
|
type TokenStats,
|
|
type UpdatesResp,
|
|
type VoiceMetrics,
|
|
} 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,
|
|
voiceMetrics: ["voice-metrics"] 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,
|
|
}
|
|
|
|
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: 10_000 })
|
|
|
|
export const useSystemStatus = (refetchInterval = 5_000) =>
|
|
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
|
|
|
export const useServices = (refetchInterval = 3_000) =>
|
|
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
|
|
|
export const useModels = (refetchInterval = 4_000) =>
|
|
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval })
|
|
|
|
export const useGroups = (refetchInterval = 8_000) =>
|
|
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
|
|
|
export const useRouting = (refetchInterval = 4_000) =>
|
|
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
|
|
|
export const useVoiceMetrics = (refetchInterval = 5_000) =>
|
|
useQuery({ queryKey: qk.voiceMetrics, queryFn: () => api<VoiceMetrics>("/api/voice/metrics"), refetchInterval })
|
|
|
|
export const useRoutingPolicy = () =>
|
|
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
|
|
|
export const useJobs = (refetchInterval = 2_000) =>
|
|
useQuery({
|
|
queryKey: qk.jobs,
|
|
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
|
refetchInterval,
|
|
select: (d) => d.jobs ?? [],
|
|
})
|
|
|
|
export const useTokenStats = (refetchInterval = 3_000) =>
|
|
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
|
|
|
export const useAgentStatus = (refetchInterval = 5_000) =>
|
|
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 = 60_000) =>
|
|
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
|
|
|
export const useUpdates = (refetchInterval?: number) =>
|
|
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 }
|