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:
+380
File diff suppressed because one or more lines are too long
-380
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-L9oobut2.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CSv49ImW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
+27
@@ -8,6 +8,7 @@
|
||||
"name": "mission-control-2-frontend",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.101.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
@@ -1810,6 +1811,32 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.1.tgz",
|
||||
"integrity": "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.101.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.1.tgz",
|
||||
"integrity": "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.101.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.101.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
@@ -18,9 +19,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/node": "^22.10.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.3",
|
||||
|
||||
+3
-16
@@ -11,30 +11,17 @@ import { AgentView } from "@/views/AgentView"
|
||||
import { GuideView } from "@/views/GuideView"
|
||||
import { Placeholder } from "@/views/Placeholder"
|
||||
import { SystemDrawer } from "@/components/SystemDrawer"
|
||||
import { api, type Health, type SystemStatus } from "@/lib/api"
|
||||
import { useHealth, useSystemStatus } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<ViewId>("dashboard")
|
||||
const [health, setHealth] = useState<Health | null>(null)
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true")
|
||||
const [sysStatus, setSysStatus] = useState<SystemStatus | null>(null)
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance")
|
||||
|
||||
useEffect(() => {
|
||||
const load = () => api<Health>("/api/health").then(setHealth).catch(() => setHealth(null))
|
||||
load()
|
||||
const t = setInterval(load, 10000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const loadSys = () => api<SystemStatus>("/api/system/status").then(setSysStatus).catch(() => {})
|
||||
loadSys()
|
||||
const t = setInterval(loadSys, 20000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
const { data: health } = useHealth()
|
||||
const { data: sysStatus } = useSystemStatus(20_000)
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.add("dark")
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
+15
-1
@@ -1,10 +1,24 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
|
||||
// Zentraler Daten-Layer: Caching, Dedup, Retry, Polling pro Query-Hook.
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 5_000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react"
|
||||
import { useState, useRef, useCallback, useMemo } from "react"
|
||||
import { Bot, ExternalLink, Activity, Cpu, Wrench, Shield, Check, X } from "lucide-react"
|
||||
import { api, type AgentStatus } from "@/lib/api"
|
||||
import { api } from "@/lib/api"
|
||||
import { useAgentStatus, useModels, useQueryClient, qk } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: boolean; detail?: string; icon: any; onClick?: () => void }) {
|
||||
@@ -49,26 +50,21 @@ function Tile({ label, ok, detail, icon: Icon, onClick }: { label: string; ok: b
|
||||
}
|
||||
|
||||
export function AgentView() {
|
||||
const [s, setS] = useState<AgentStatus | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
const { data: s, error: sErr } = useAgentStatus(5_000)
|
||||
const { data: modelsData } = useModels()
|
||||
const { showAlert, dialogElement } = useDialog()
|
||||
const qc = useQueryClient()
|
||||
const error = sErr ? String(sErr) : ""
|
||||
|
||||
const availableModels = useMemo(() => {
|
||||
const names = (modelsData?.models ?? []).map(
|
||||
(m) => m.name.split("/").pop()?.replace(".gguf", "") || m.name)
|
||||
return ["auto", "fast", "heavy", ...names]
|
||||
}, [modelsData])
|
||||
|
||||
// Graph UI state
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [showBrainSelect, setShowBrainSelect] = useState(false)
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm?: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({ type: "alert", title, message, onConfirm })
|
||||
}
|
||||
|
||||
// Canvas pixel tracking for pixel-perfect connection graph without non-uniform scaling
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 360 })
|
||||
@@ -99,19 +95,6 @@ export function AgentView() {
|
||||
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
api<AgentStatus>("/api/agent/status").then(setS).catch((e) => setError(String(e)))
|
||||
}
|
||||
|
||||
function loadModels() {
|
||||
api<{ models: { name: string }[] }>("/api/models")
|
||||
.then((res) => {
|
||||
const names = res.models.map(m => m.name.split("/").pop()?.replace(".gguf", "") || m.name)
|
||||
setAvailableModels(["auto", "fast", "heavy", ...names])
|
||||
})
|
||||
.catch((e) => console.error("Error loading models", e))
|
||||
}
|
||||
|
||||
async function changeBrainModel(model: string) {
|
||||
try {
|
||||
await api("/api/agent/brain", {
|
||||
@@ -119,20 +102,13 @@ export function AgentView() {
|
||||
body: JSON.stringify({ model })
|
||||
})
|
||||
showAlert("Erfolgreich", `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`)
|
||||
loadData()
|
||||
qc.invalidateQueries({ queryKey: qk.agentStatus })
|
||||
setShowBrainSelect(false)
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Wechseln des Gehirns: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
loadModels()
|
||||
const t = setInterval(loadData, 5000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Styles inside AgentView for dash flow animations */}
|
||||
@@ -471,15 +447,7 @@ export function AgentView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={() => dialog.onConfirm && dialog.onConfirm()}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { Check, Copy, Terminal, Info, Globe, FolderOpen } from "lucide-react"
|
||||
import { api, type ConnectResp } from "@/lib/api"
|
||||
import { useConnect } from "@/lib/queries"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function ConnectView() {
|
||||
const [host, setHost] = useState(localStorage.getItem("mc_host") || "192.168.178.151")
|
||||
const [mcpPath, setMcpPath] = useState(localStorage.getItem("mc_mcp_path") || "")
|
||||
const [data, setData] = useState<ConnectResp | null>(null)
|
||||
const [active, setActive] = useState("cline")
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams()
|
||||
params.set("host", host)
|
||||
if (mcpPath) params.set("mcp_path", mcpPath)
|
||||
api<ConnectResp>(`/api/connect?${params}`)
|
||||
.then(setData)
|
||||
.catch((e) => setError(String(e)))
|
||||
}, [host, mcpPath])
|
||||
const params = new URLSearchParams({ host })
|
||||
if (mcpPath) params.set("mcp_path", mcpPath)
|
||||
const { data, error: dataErr } = useConnect(params.toString())
|
||||
const error = dataErr ? String(dataErr) : ""
|
||||
|
||||
function saveHost(v: string) {
|
||||
setHost(v)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { Trash2, Sparkles, User, Scroll, Shield, Tag, Clock, Search, Plus, BookOpen } from "lucide-react"
|
||||
import { api, type DedupeResult, type Memory } from "@/lib/api"
|
||||
import { api, type DedupeResult } from "@/lib/api"
|
||||
import { useMemory, useQueryClient } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
const CATEGORIES = ["user", "instruction", "stable", "versioned", "ephemeral"]
|
||||
@@ -26,59 +27,18 @@ const BORDER_CLASSES: Record<string, string> = {
|
||||
}
|
||||
|
||||
export function MemoryView() {
|
||||
const [items, setItems] = useState<Memory[]>([])
|
||||
const [filter, setFilter] = useState("")
|
||||
const [q, setQ] = useState("")
|
||||
const [content, setContent] = useState("")
|
||||
const [category, setCategory] = useState("stable")
|
||||
const [error, setError] = useState("")
|
||||
const [deduping, setDeduping] = useState(false)
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
const qc = useQueryClient()
|
||||
const { showAlert, showConfirm, dialogElement } = useDialog()
|
||||
const { data: items = [], error: itemsErr } = useMemory({ q, category: filter })
|
||||
const error = itemsErr ? String(itemsErr) : ""
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showConfirm(title: string, message: string, onConfirm: () => void) {
|
||||
setDialog({
|
||||
type: "confirm",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
onConfirm()
|
||||
},
|
||||
onCancel: () => setDialog(null)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function load() {
|
||||
const params = new URLSearchParams()
|
||||
if (q) params.set("q", q)
|
||||
if (filter) params.set("category", filter)
|
||||
api<Memory[]>(`/api/memory?${params}`)
|
||||
.then(setItems)
|
||||
.catch((e) => setError(String(e)))
|
||||
}
|
||||
|
||||
useEffect(load, [q, filter])
|
||||
const reloadMemory = () => qc.invalidateQueries({ queryKey: ["memory"] })
|
||||
|
||||
async function add() {
|
||||
if (!content.trim()) return
|
||||
@@ -87,12 +47,12 @@ export function MemoryView() {
|
||||
body: JSON.stringify({ content, category, source: "ui" })
|
||||
})
|
||||
setContent("")
|
||||
load()
|
||||
reloadMemory()
|
||||
}
|
||||
|
||||
async function del(id: string) {
|
||||
await api(`/api/memory/${id}`, { method: "DELETE" })
|
||||
load()
|
||||
reloadMemory()
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
@@ -115,7 +75,7 @@ export function MemoryView() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ apply: true })
|
||||
})
|
||||
load()
|
||||
reloadMemory()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Fehler beim Löschen: ${e.message}`)
|
||||
}
|
||||
@@ -285,15 +245,7 @@ export function MemoryView() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react"
|
||||
import { api, type ServicesResp, type SystemStatus } from "@/lib/api"
|
||||
import { api } from "@/lib/api"
|
||||
import { useSystemStatus, useServices } from "@/lib/queries"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { gb } from "@/lib/format"
|
||||
import { CustomDialog } from "@/components/CustomDialog"
|
||||
|
||||
|
||||
function DiagnosticBar({ label, percent, detail, icon: Icon }: { label: string; percent: number; detail?: string; icon: any }) {
|
||||
@@ -36,44 +37,13 @@ function DiagnosticBar({ label, percent, detail, icon: Icon }: { label: string;
|
||||
}
|
||||
|
||||
export function SystemView() {
|
||||
const [s, setS] = useState<SystemStatus | null>(null)
|
||||
const [svc, setSvc] = useState<ServicesResp | null>(null)
|
||||
const [error, setError] = useState("")
|
||||
const { data: s, error: sErr } = useSystemStatus(3_000)
|
||||
const { data: svc } = useServices(3_000)
|
||||
const { showAlert, dialogElement } = useDialog()
|
||||
const error = sErr ? String(sErr) : ""
|
||||
const [backupMsg, setBackupMsg] = useState("")
|
||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
title: string
|
||||
message: string
|
||||
onConfirm: () => void
|
||||
onCancel?: () => void
|
||||
} | null>(null)
|
||||
|
||||
function showAlert(title: string, message: string, onConfirm?: () => void) {
|
||||
setDialog({
|
||||
type: "alert",
|
||||
title,
|
||||
message,
|
||||
onConfirm: () => {
|
||||
setDialog(null)
|
||||
if (onConfirm) onConfirm()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function load() {
|
||||
api<SystemStatus>("/api/system/status").then(setS).catch((e) => setError(String(e)))
|
||||
api<ServicesResp>("/api/system/services").then(setSvc).catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
const t = setInterval(load, 3000)
|
||||
return () => clearInterval(t)
|
||||
}, [])
|
||||
|
||||
async function doBackup() {
|
||||
setBackupMsg("Backup snapshotted...")
|
||||
try {
|
||||
@@ -254,15 +224,7 @@ export function SystemView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
title={dialog.title}
|
||||
message={dialog.message}
|
||||
onConfirm={dialog.onConfirm}
|
||||
onCancel={dialog.onCancel}
|
||||
/>
|
||||
)}
|
||||
{dialogElement}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user