Refactor: Diagnose-Tab in Zentrale gemerged + Zentrale neu strukturiert
Diagnose war groesstenteils Dublette (Metriken/Temps schon in Zentrale; Logs/Restart/
Updates im Pflege-Drawer). Tab entfernt, zwei einzigartige Teile umverteilt.
- Zentrale neu in 3 Zonen: Live-Telemetrie (System-Status + Token-Durchsatz nebeneinander
statt gestapelt), Stack-Status (Aktive Modelle / Rollen / Dienste), Betrieb & Wissen
(Updates / Hermes / Gedaechtnis).
- Neue ServicesCard (Dienste-Health aus Diagnose) auf der Zentrale.
- TokenStatsCard ('Effizienz & Ersparnis') in TokenPerformanceCard gemerged (Input/Output
+ gespart) -> eine Karte weniger, keine Dublette.
- Backup/Snapshot in den System-Wartung-Drawer verschoben.
- nav.ts/App.tsx: 'system'-View entfernt (6 statt 7 Tabs); SystemView.tsx geloescht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-1
File diff suppressed because one or more lines are too long
+468
File diff suppressed because one or more lines are too long
-473
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Dn30RTcO.js"></script>
|
<script type="module" crossorigin src="/assets/index-CauIIHdl.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-C4fQuXTp.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-wQ2PPr04.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NAV, type ViewId } from "@/nav"
|
|||||||
import { CommandPalette } from "@/components/CommandPalette"
|
import { CommandPalette } from "@/components/CommandPalette"
|
||||||
import { DashboardView } from "@/views/DashboardView"
|
import { DashboardView } from "@/views/DashboardView"
|
||||||
import { ModelsView } from "@/views/ModelsView"
|
import { ModelsView } from "@/views/ModelsView"
|
||||||
import { SystemView } from "@/views/SystemView"
|
|
||||||
import { ConnectView } from "@/views/ConnectView"
|
import { ConnectView } from "@/views/ConnectView"
|
||||||
import { MemoryView } from "@/views/MemoryView"
|
import { MemoryView } from "@/views/MemoryView"
|
||||||
import { AgentView } from "@/views/AgentView"
|
import { AgentView } from "@/views/AgentView"
|
||||||
@@ -176,12 +175,11 @@ export default function App() {
|
|||||||
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin">
|
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin">
|
||||||
{view === "dashboard" && <DashboardView />}
|
{view === "dashboard" && <DashboardView />}
|
||||||
{view === "models" && <ModelsView />}
|
{view === "models" && <ModelsView />}
|
||||||
{view === "system" && <SystemView />}
|
|
||||||
{view === "connect" && <ConnectView />}
|
{view === "connect" && <ConnectView />}
|
||||||
{view === "memory" && <MemoryView />}
|
{view === "memory" && <MemoryView />}
|
||||||
{view === "agent" && <AgentView />}
|
{view === "agent" && <AgentView />}
|
||||||
{view === "guide" && <GuideView />}
|
{view === "guide" && <GuideView />}
|
||||||
{!["dashboard", "models", "system", "connect", "memory", "agent", "guide"].includes(view) && (
|
{!["dashboard", "models", "connect", "memory", "agent", "guide"].includes(view) && (
|
||||||
<Placeholder title={active.label} hint={active.hint} />
|
<Placeholder title={active.label} hint={active.hint} />
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useRef } from "react"
|
import { useEffect, useState, useRef } from "react"
|
||||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle } from "lucide-react"
|
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle, Save } from "lucide-react"
|
||||||
import { api, type Job, type UpdatesResp } from "@/lib/api"
|
import { api, type Job, type UpdatesResp } from "@/lib/api"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { CustomDialog } from "./CustomDialog"
|
import { CustomDialog } from "./CustomDialog"
|
||||||
@@ -35,6 +35,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
||||||
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
|
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
|
||||||
const [checkingUpdates, setCheckingUpdates] = useState(false)
|
const [checkingUpdates, setCheckingUpdates] = useState(false)
|
||||||
|
const [backupMsg, setBackupMsg] = useState("")
|
||||||
|
const [backupRunning, setBackupRunning] = useState(false)
|
||||||
|
|
||||||
// Custom Dialog State
|
// Custom Dialog State
|
||||||
const [dialog, setDialog] = useState<{
|
const [dialog, setDialog] = useState<{
|
||||||
@@ -222,6 +224,19 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doBackup() {
|
||||||
|
setBackupRunning(true)
|
||||||
|
setBackupMsg("Snapshot wird erzeugt...")
|
||||||
|
try {
|
||||||
|
const r = await api<{ ok: boolean; snapshot: string; files: string[] }>("/api/system/backup", { method: "POST" })
|
||||||
|
setBackupMsg(r.ok ? `Snapshot erzeugt: ${r.snapshot} (${r.files.length} Dateien)` : "Keine Änderungen zu sichern.")
|
||||||
|
} catch (e: any) {
|
||||||
|
setBackupMsg(`Fehler: ${e.message}`)
|
||||||
|
} finally {
|
||||||
|
setBackupRunning(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function restartService(serviceId: string) {
|
async function restartService(serviceId: string) {
|
||||||
setRestartingServices(prev => ({ ...prev, [serviceId]: true }))
|
setRestartingServices(prev => ({ ...prev, [serviceId]: true }))
|
||||||
try {
|
try {
|
||||||
@@ -384,6 +399,28 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Backup / Snapshot */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">System-Backup & Snapshot</h3>
|
||||||
|
<div className="p-4 rounded-xl border border-border/60 bg-background/20 space-y-3">
|
||||||
|
<p className="text-[10px] text-muted-foreground leading-normal">
|
||||||
|
Git-Snapshot der aktuellen Konfigurationen und des System-Zustands erzeugen.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={doBackup}
|
||||||
|
disabled={backupRunning}
|
||||||
|
className="w-full h-9 flex items-center justify-center gap-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer disabled:opacity-50 shadow-md shadow-primary/10"
|
||||||
|
>
|
||||||
|
<Save className={cn("h-4 w-4", backupRunning && "animate-pulse")} /> Snapshot erstellen
|
||||||
|
</button>
|
||||||
|
{backupMsg && (
|
||||||
|
<div className="text-[10px] font-mono text-primary bg-primary/5 p-2 rounded-lg border border-primary/20 break-all leading-normal">
|
||||||
|
{backupMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modell Upgrades */}
|
{/* Modell Upgrades */}
|
||||||
{updates?.model_list && updates.model_list.length > 0 && (
|
{updates?.model_list && updates.model_list.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { ExternalLink, RefreshCw, Server } from "lucide-react"
|
||||||
|
import { api } from "@/lib/api"
|
||||||
|
import { useServices } from "@/lib/queries"
|
||||||
|
import { useDialog } from "@/lib/useDialog"
|
||||||
|
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function ServicesCard() {
|
||||||
|
const { data: svc } = useServices(3_000)
|
||||||
|
const { showAlert, dialogElement } = useDialog()
|
||||||
|
const [restarting, setRestarting] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
async function restart(id: string) {
|
||||||
|
setRestarting((p) => ({ ...p, [id]: true }))
|
||||||
|
try {
|
||||||
|
const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", {
|
||||||
|
method: "POST", body: JSON.stringify({ service: id }),
|
||||||
|
})
|
||||||
|
if (!r.ok) showAlert("Fehler", `Neustart fehlgeschlagen: ${r.err || "Unbekannt"}`)
|
||||||
|
} catch (e: any) {
|
||||||
|
showAlert("Fehler", `Fehler: ${e.message}`)
|
||||||
|
} finally {
|
||||||
|
setRestarting((p) => ({ ...p, [id]: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server className="h-4.5 w-4.5 text-primary" />
|
||||||
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Dienste</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "logs" } }))}
|
||||||
|
className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground transition-colors hover:text-primary cursor-pointer"
|
||||||
|
>
|
||||||
|
Logs / Pflege
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{svc ? (
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{svc.services.map((x) => (
|
||||||
|
<div key={x.name} className="group flex items-center justify-between gap-2 rounded-xl border border-border/30 bg-background/20 p-2.5">
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<span className={cn("h-2.5 w-2.5 shrink-0 rounded-full ring-2 ring-black/40", x.ok ? "bg-emerald-500" : "bg-amber-500")} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="truncate text-xs font-bold text-foreground">{x.name}</div>
|
||||||
|
<div className="truncate font-mono text-[9px] text-muted-foreground/60">{x.url}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => restart(x.name)} disabled={restarting[x.name]}
|
||||||
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg border border-border/40 text-muted-foreground opacity-0 transition-all hover:bg-primary/5 hover:text-primary group-hover:opacity-100"
|
||||||
|
title="Dienst neu starten"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("h-3.5 w-3.5", restarting[x.name] && "animate-spin")} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex flex-wrap gap-3 border-t border-border/20 pt-2.5 text-[10px] font-semibold text-muted-foreground">
|
||||||
|
<a href={resolveExternalUrl(svc.links.engine_ui)} target="_blank" rel="noopener" className="flex items-center gap-1 rounded px-1.5 py-0.5 text-primary transition-colors hover:bg-primary/10">
|
||||||
|
<ExternalLink className="h-3 w-3" /> Engine
|
||||||
|
</a>
|
||||||
|
<a href={resolveExternalUrl(svc.links.gateway)} target="_blank" rel="noopener" className="flex items-center gap-1 rounded px-1.5 py-0.5 text-primary transition-colors hover:bg-primary/10">
|
||||||
|
<ExternalLink className="h-3 w-3" /> Gateway
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-24 items-center justify-center text-xs text-muted-foreground">Lade Dienste…</div>
|
||||||
|
)}
|
||||||
|
{dialogElement}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ export function SystemStatusCard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:col-span-2 flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
|
<div className="flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<Cpu className="h-4.5 w-4.5 text-primary" />
|
<Cpu className="h-4.5 w-4.5 text-primary" />
|
||||||
|
|||||||
@@ -55,8 +55,13 @@ export function TokenPerformanceCard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ts && (
|
{ts && (
|
||||||
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
<div className="mt-0.5 space-y-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||||
{ts.total_tokens.toLocaleString("de-DE")} Tokens gesamt · {ts.saved_eur.toLocaleString("de-DE", { minimumFractionDigits: 2 })} € gespart
|
<div>
|
||||||
|
{ts.total_tokens.toLocaleString("de-DE")} Tokens gesamt · <span className="text-emerald-400/90">{ts.saved_eur.toLocaleString("de-DE", { minimumFractionDigits: 2 })} € gespart</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground/55">
|
||||||
|
Input {ts.prompt_tokens.toLocaleString("de-DE")} · Output {ts.completion_tokens.toLocaleString("de-DE")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Coins } from "lucide-react"
|
|
||||||
import { useTokenStats } from "@/lib/queries"
|
|
||||||
|
|
||||||
export function TokenStatsCard() {
|
|
||||||
const { data: tokenStats } = useTokenStats(3_000)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2 mb-4">
|
|
||||||
<Coins className="h-4.5 w-4.5 text-primary animate-pulse" />
|
|
||||||
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Effizienz & Ersparnis</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tokenStats ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-2.5">
|
|
||||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40 text-left">
|
|
||||||
<div className="text-[8px] uppercase tracking-wider font-semibold text-muted-foreground/60">Geld gespart</div>
|
|
||||||
<div className="text-base font-bold text-emerald-400 mt-0.5 tracking-tight font-space">
|
|
||||||
{tokenStats.saved_eur.toLocaleString("de-DE", { minimumFractionDigits: 2 })} €
|
|
||||||
</div>
|
|
||||||
<div className="text-[8px] text-muted-foreground/80 mt-0.5 font-mono">
|
|
||||||
({tokenStats.saved_usd.toLocaleString("en-US", { minimumFractionDigits: 2 })} $)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-background/20 rounded-xl border border-border/40 text-left">
|
|
||||||
<div className="text-[8px] uppercase tracking-wider font-semibold text-muted-foreground/60">Gesamt-Tokens</div>
|
|
||||||
<div className="text-base font-bold text-primary mt-0.5 tracking-tight font-space">
|
|
||||||
{tokenStats.total_tokens.toLocaleString("de-DE")}
|
|
||||||
</div>
|
|
||||||
<div className="text-[8px] text-muted-foreground/80 mt-0.5 font-mono">
|
|
||||||
(Lokale Inferenz)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 border-t border-border/20 pt-3 text-[10px] text-muted-foreground">
|
|
||||||
<div className="flex justify-between items-center font-mono">
|
|
||||||
<span>Input (Prompts):</span>
|
|
||||||
<span className="font-semibold text-foreground">{tokenStats.prompt_tokens.toLocaleString("de-DE")} tkn</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center font-mono">
|
|
||||||
<span>Output (Antworten):</span>
|
|
||||||
<span className="font-semibold text-foreground">{tokenStats.completion_tokens.toLocaleString("de-DE")} tkn</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-28 flex items-center justify-center text-xs text-muted-foreground">Lade Statistiken…</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 text-[9px] text-muted-foreground/70 border-t border-border/30 pt-2.5 leading-normal">
|
|
||||||
Berechnet ggü. Cloud-APIs
|
|
||||||
{tokenStats?.pricing?.heavy
|
|
||||||
? ` (Ø ${(tokenStats.pricing.heavy.in ?? 0).toFixed(2).replace(".", ",")} $ / ${(tokenStats.pricing.heavy.out ?? 0).toFixed(2).replace(".", ",")} $ pro 1M tkn).`
|
|
||||||
: "."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+1
-3
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Boxes,
|
Boxes,
|
||||||
Cpu,
|
|
||||||
Brain,
|
Brain,
|
||||||
Plug,
|
Plug,
|
||||||
Bot,
|
Bot,
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
|
||||||
export type ViewId = "dashboard" | "models" | "system" | "memory" | "connect" | "agent" | "guide"
|
export type ViewId = "dashboard" | "models" | "memory" | "connect" | "agent" | "guide"
|
||||||
|
|
||||||
export interface NavItem {
|
export interface NavItem {
|
||||||
id: ViewId
|
id: ViewId
|
||||||
@@ -22,7 +21,6 @@ export interface NavItem {
|
|||||||
export const NAV: NavItem[] = [
|
export const NAV: NavItem[] = [
|
||||||
{ id: "dashboard", label: "Zentrale", hint: "System- & Stack-Status", icon: LayoutDashboard },
|
{ id: "dashboard", label: "Zentrale", hint: "System- & Stack-Status", icon: LayoutDashboard },
|
||||||
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
|
{ id: "models", label: "Modell-Manager", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
|
||||||
{ id: "system", label: "Diagnose", hint: "Metriken, Dienste, Logs", icon: Cpu },
|
|
||||||
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
|
||||||
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
|
||||||
{ id: "agent", label: "Hermes", hint: "Agent-Status & AnythingLLM öffnen", icon: Bot },
|
{ id: "agent", label: "Hermes", hint: "Agent-Status & AnythingLLM öffnen", icon: Bot },
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import { UpdatesCard } from "@/components/dashboard/UpdatesCard"
|
|||||||
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
|
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
|
||||||
import { RolesCard } from "@/components/dashboard/RolesCard"
|
import { RolesCard } from "@/components/dashboard/RolesCard"
|
||||||
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
|
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
|
||||||
import { TokenStatsCard } from "@/components/dashboard/TokenStatsCard"
|
|
||||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||||
|
import { ServicesCard } from "@/components/dashboard/ServicesCard"
|
||||||
|
|
||||||
|
function ZoneLabel({ children }: { children: React.ReactNode }) {
|
||||||
|
return <p className="mb-2 ml-0.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">{children}</p>
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardView() {
|
export function DashboardView() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-7">
|
||||||
{/* Title */}
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
|
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
|
||||||
Zentrale
|
Zentrale
|
||||||
@@ -18,25 +21,34 @@ export function DashboardView() {
|
|||||||
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
|
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live: aktuell geladene/arbeitende Modelle */}
|
{/* Zone 1 — Live-Telemetrie: beide Charts nebeneinander */}
|
||||||
<ActiveModelsCard />
|
<section>
|
||||||
|
<ZoneLabel>Live-Telemetrie</ZoneLabel>
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
<SystemStatusCard />
|
||||||
|
<TokenPerformanceCard />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Top Grid: System stats & Updates */}
|
{/* Zone 2 — Stack-Status */}
|
||||||
<div className="grid gap-6 md:grid-cols-3">
|
<section>
|
||||||
<SystemStatusCard />
|
<ZoneLabel>Stack-Status</ZoneLabel>
|
||||||
<UpdatesCard />
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
</div>
|
<ActiveModelsCard />
|
||||||
|
<RolesCard />
|
||||||
|
<ServicesCard />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Live-Durchsatz (Performance) */}
|
{/* Zone 3 — Betrieb & Wissen */}
|
||||||
<TokenPerformanceCard />
|
<section>
|
||||||
|
<ZoneLabel>Betrieb & Wissen</ZoneLabel>
|
||||||
{/* Bottom Grid: Agent, Roles, Memory & Token Stats */}
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
<UpdatesCard />
|
||||||
<AgentStatusCard />
|
<AgentStatusCard />
|
||||||
<RolesCard />
|
<MemoryInputCard />
|
||||||
<MemoryInputCard />
|
</div>
|
||||||
<TokenStatsCard />
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
import { useState } from "react"
|
|
||||||
import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react"
|
|
||||||
import { api } from "@/lib/api"
|
|
||||||
import { useServices } from "@/lib/queries"
|
|
||||||
import { useSystemHistory, type SysSample } from "@/lib/useSystemHistory"
|
|
||||||
import { useDialog } from "@/lib/useDialog"
|
|
||||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
|
||||||
import { gb } from "@/lib/format"
|
|
||||||
import { LiveAreaChart } from "@/components/dashboard/LiveAreaChart"
|
|
||||||
|
|
||||||
|
|
||||||
function MetricChartCard({ label, percent, detail, icon: Icon, color, seriesKey, data }: {
|
|
||||||
label: string; percent: number; detail?: string; icon: any
|
|
||||||
color: string; seriesKey: keyof SysSample; data: SysSample[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 shadow-lg shadow-black/10 hover:border-primary/30 transition-all duration-300">
|
|
||||||
<div className="mb-1 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon className="h-4.5 w-4.5" style={{ color }} />
|
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-foreground">{label}</span>
|
|
||||||
</div>
|
|
||||||
<span className="font-mono text-sm font-bold tabular-nums text-foreground">{Math.round(percent)}%</span>
|
|
||||||
</div>
|
|
||||||
{detail && <div className="mb-1 font-mono text-[10px] text-muted-foreground/70">{detail}</div>}
|
|
||||||
<LiveAreaChart data={data} series={[{ key: seriesKey as string, label, color }]} unit="%" yMode="percent" height={88} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SystemView() {
|
|
||||||
const { sys: s, hist, error: sErr } = useSystemHistory()
|
|
||||||
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>>({})
|
|
||||||
|
|
||||||
async function doBackup() {
|
|
||||||
setBackupMsg("Backup snapshotted...")
|
|
||||||
try {
|
|
||||||
const r = await api<{ ok: boolean; snapshot: string; files: string[] }>("/api/system/backup", { method: "POST" })
|
|
||||||
setBackupMsg(r.ok ? `Snapshot erzeugt: ${r.snapshot} (${r.files.length} Dateien)` : "Keine Änderungen zu sichern.")
|
|
||||||
} catch (e: any) {
|
|
||||||
setBackupMsg(`Fehler: ${e.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function restartService(serviceId: string) {
|
|
||||||
setRestartingServices(prev => ({ ...prev, [serviceId]: true }))
|
|
||||||
try {
|
|
||||||
const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ service: serviceId })
|
|
||||||
})
|
|
||||||
if (r.ok) {
|
|
||||||
showAlert("Erfolgreich", `Dienst ${serviceId} wurde erfolgreich neu gestartet.`)
|
|
||||||
} else {
|
|
||||||
showAlert("Fehler beim Neustart", `Fehler beim Neustart: ${r.err || "Unbekannter Fehler"}`)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
showAlert("Fehler", `Fehler: ${e.message}`)
|
|
||||||
} finally {
|
|
||||||
setRestartingServices(prev => ({ ...prev, [serviceId]: false }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
|
|
||||||
System-Diagnose & Status
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
|
||||||
Echtzeit-Ressourcen der Box, Dienst-Überwachung und Systempflege.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-xl border border-red-500/20 bg-red-500/5 p-4 text-xs text-red-400 font-mono">
|
|
||||||
System-Status nicht lesbar ({error}).
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metrics Section */}
|
|
||||||
{s && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<MetricChartCard label="CPU" color="#2dd4bf" seriesKey="cpu" data={hist}
|
|
||||||
percent={s.cpu.percent} detail={s.cpu.cores ? `${s.cpu.cores} Cores` : undefined} icon={Cpu} />
|
|
||||||
<MetricChartCard label="RAM" color="#38bdf8" seriesKey="ram" data={hist}
|
|
||||||
percent={s.ram.percent} detail={`${gb(s.ram.used)} / ${gb(s.ram.total)} GB`} icon={Activity} />
|
|
||||||
|
|
||||||
{s.gpu && s.gpu.busy_percent != null && (
|
|
||||||
<MetricChartCard
|
|
||||||
label="GPU" color="#a78bfa" seriesKey="gpu" data={hist}
|
|
||||||
percent={s.gpu.busy_percent}
|
|
||||||
detail={
|
|
||||||
s.gpu.gtt_used != null && s.gpu.gtt_total
|
|
||||||
? `${gb(s.gpu.gtt_used)} / ${gb(s.gpu.gtt_total)} GB (GTT/unified)`
|
|
||||||
: s.gpu.vram_used != null && s.gpu.vram_total
|
|
||||||
? `${gb(s.gpu.vram_used)} / ${gb(s.gpu.vram_total)} GB VRAM`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
icon={GpuIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{s.disk && (
|
|
||||||
<MetricChartCard label="Disk" color="#fbbf24" seriesKey="disk" data={hist}
|
|
||||||
percent={s.disk.percent} detail={`${gb(s.disk.used)} / ${gb(s.disk.total)} GB`} icon={HardDrive} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Temperatures display */}
|
|
||||||
{s.temp && (s.temp.cpu || s.temp.gpu) && (
|
|
||||||
<div className="flex gap-3 text-[10px] font-mono text-muted-foreground bg-background/25 border border-border/40 p-2.5 rounded-xl self-start w-fit">
|
|
||||||
{s.temp.cpu != null && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
CPU-Temperatur: <span className="text-foreground font-bold">{s.temp.cpu} °C</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{s.temp.cpu != null && s.temp.gpu != null && <span>|</span>}
|
|
||||||
{s.temp.gpu != null && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
GPU-Temperatur: <span className="text-foreground font-bold">{s.temp.gpu} °C</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Services Health matrix */}
|
|
||||||
{svc && (
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Homelab-Dienste</div>
|
|
||||||
<button
|
|
||||||
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab: "logs" } }))}
|
|
||||||
className="h-7 px-3 rounded-lg border border-border/60 bg-background/25 text-[10px] font-bold uppercase hover:bg-accent text-muted-foreground transition-all cursor-pointer"
|
|
||||||
>
|
|
||||||
System-Logs anzeigen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
{svc.services.map((x) => (
|
|
||||||
<div
|
|
||||||
key={x.name}
|
|
||||||
className="flex items-center justify-between p-3.5 rounded-xl bg-background/20 border border-border/30 hover:border-primary/20 transition-all group"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
|
||||||
<span className={cn(
|
|
||||||
"h-2.5 w-2.5 rounded-full ring-2 ring-black/40",
|
|
||||||
x.ok ? "bg-emerald-500" : "bg-amber-500"
|
|
||||||
)} />
|
|
||||||
<div className="truncate">
|
|
||||||
<div className="text-xs font-bold text-foreground truncate">{x.name}</div>
|
|
||||||
<div className="text-[9px] text-muted-foreground/60 font-mono mt-0.5 truncate">{x.url}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => restartService(x.name)}
|
|
||||||
disabled={restartingServices[x.name]}
|
|
||||||
className="h-7 w-7 rounded-lg border border-border/40 text-muted-foreground hover:text-primary hover:bg-primary/5 flex items-center justify-center transition-all opacity-0 group-hover:opacity-100"
|
|
||||||
title="Dienst neu starten"
|
|
||||||
>
|
|
||||||
<RefreshCw className={cn("h-3.5 w-3.5", restartingServices[x.name] && "animate-spin")} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Service Links */}
|
|
||||||
<div className="flex flex-wrap gap-4 border-t border-border/20 pt-4 text-[10px] font-semibold text-muted-foreground">
|
|
||||||
<a
|
|
||||||
href={resolveExternalUrl(svc.links.engine_ui)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
className="flex items-center gap-1 text-primary hover:text-primary-foreground hover:bg-primary/10 px-2 py-1 rounded transition-colors"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3 w-3" /> Engine Dashboard (llama-swap)
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={resolveExternalUrl(svc.links.gateway)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
className="flex items-center gap-1 text-primary hover:text-primary-foreground hover:bg-primary/10 px-2 py-1 rounded transition-colors"
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-3 w-3" /> OpenAI Gateway
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Backup snapshot panel */}
|
|
||||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col sm:flex-row justify-between sm:items-center gap-4">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h3 className="text-xs font-bold uppercase tracking-wider text-foreground">System-Backup & Snapshot</h3>
|
|
||||||
<p className="text-[10px] text-muted-foreground">Erzeuge einen Git-Snapshot der aktuellen Konfigurationen und des System-Zustands.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 self-start sm:self-auto shrink-0">
|
|
||||||
<button
|
|
||||||
onClick={doBackup}
|
|
||||||
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all flex items-center gap-1.5 cursor-pointer shadow-md shadow-primary/10"
|
|
||||||
>
|
|
||||||
<Save className="h-4 w-4" /> Snapshot erstellen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{backupMsg && (
|
|
||||||
<div className="text-[10px] font-mono text-primary font-medium bg-primary/5 p-3 rounded-xl border border-primary/20">
|
|
||||||
{backupMsg}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{dialogElement}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user