Refactor: TanStack-Query-Daten-Layer + useDialog (Phase 3b)
- @tanstack/react-query (v5) als zentraler Daten-Layer; QueryClientProvider in main.tsx (retry 1, kein refetchOnWindowFocus, staleTime 5s). - lib/queries.ts: Domänen-Hooks (useHealth/useSystemStatus/useServices/ useModels/useRouting/useJobs/useTokenStats/useAgentStatus/useUpdates/ useDiscover/useConnect/useMemory) + zentrale Query-Keys (qk) + invalidate. Gleicher Key = eine Anfrage über alle Views (Dedup), einheitliches Polling. - lib/useDialog.tsx: ein Hook für Alert/Confirm/Prompt statt 5x dupliziertem Dialog-State + showAlert/showConfirm. - api.ts: TokenStats + ModelsResp typisiert. - Migriert auf Hooks/useDialog: App, SystemView, AgentView, ConnectView, MemoryView (manuelles useEffect+setInterval entfernt; Mutationen invalidieren gezielt die Query-Keys). Verifiziert: tsc grün, Build grün, alle migrierten Views ohne Konsolenfehler, Live-Daten (CPU/RAM/Dienste) rendern korrekt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -243,3 +243,17 @@ export interface Health {
|
||||
engine_reachable: boolean
|
||||
gateway_reachable: boolean
|
||||
}
|
||||
|
||||
export interface TokenStats {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
saved_usd: number
|
||||
saved_eur: number
|
||||
pricing?: Record<string, { in: number; out: number }>
|
||||
}
|
||||
|
||||
export interface ModelsResp {
|
||||
models: ModelInfo[]
|
||||
running?: string[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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 DiscoverResp,
|
||||
type Health,
|
||||
type Job,
|
||||
type Memory,
|
||||
type ModelsResp,
|
||||
type RoutingResp,
|
||||
type ServicesResp,
|
||||
type SystemStatus,
|
||||
type TokenStats,
|
||||
type UpdatesResp,
|
||||
} 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,
|
||||
routing: ["routing"] as const,
|
||||
jobs: ["jobs"] as const,
|
||||
tokenStats: ["token-stats"] as const,
|
||||
agentStatus: ["agent-status"] as const,
|
||||
updates: ["updates"] as const,
|
||||
discover: ["discover"] as const,
|
||||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||||
}
|
||||
|
||||
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 useRouting = (refetchInterval = 4_000) =>
|
||||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||||
|
||||
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 })
|
||||
|
||||
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") })
|
||||
|
||||
export const useConnect = (params?: string) =>
|
||||
useQuery({
|
||||
queryKey: qk.connect(params),
|
||||
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
||||
})
|
||||
|
||||
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 }
|
||||
@@ -0,0 +1,55 @@
|
||||
// Ein Hook für Alert/Confirm/Prompt-Dialoge — ersetzt die zuvor in jeder View
|
||||
// duplizierte showAlert/showConfirm-Logik + den lokalen Dialog-State.
|
||||
//
|
||||
// Nutzung:
|
||||
// const { showAlert, showConfirm, showPrompt, dialogElement } = useDialog()
|
||||
// ...
|
||||
// showConfirm("Titel", "Wirklich?", () => doIt())
|
||||
// return (<>{dialogElement}...</>)
|
||||
|
||||
import { useCallback, useState } from "react"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
interface DialogState {
|
||||
type: "alert" | "confirm" | "prompt"
|
||||
title: string
|
||||
message: string
|
||||
defaultValue?: string
|
||||
onConfirm: (val?: string) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const [dialog, setDialog] = useState<DialogState | null>(null)
|
||||
const close = useCallback(() => setDialog(null), [])
|
||||
|
||||
const showAlert = useCallback((title: string, message: string, onConfirm?: () => void) => {
|
||||
setDialog({
|
||||
type: "alert", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showConfirm = useCallback(
|
||||
(title: string, message: string, onConfirm: () => void, onCancel?: () => void) => {
|
||||
setDialog({
|
||||
type: "confirm", title, message,
|
||||
onConfirm: () => { setDialog(null); onConfirm() },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showPrompt = useCallback(
|
||||
(title: string, message: string, defaultValue: string,
|
||||
onConfirm: (val?: string) => void, onCancel?: () => void) => {
|
||||
setDialog({
|
||||
type: "prompt", title, message, defaultValue,
|
||||
onConfirm: (val) => { setDialog(null); onConfirm(val) },
|
||||
onCancel: () => { setDialog(null); onCancel?.() },
|
||||
})
|
||||
}, [])
|
||||
|
||||
const dialogElement = dialog ? <CustomDialog {...dialog} /> : null
|
||||
|
||||
return { showAlert, showConfirm, showPrompt, close, dialogElement }
|
||||
}
|
||||
Reference in New Issue
Block a user