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:
Hitonabi
2026-06-27 16:57:16 +02:00
parent 35189fde0e
commit ec9488d94e
14 changed files with 630 additions and 792 deletions
+32 -20
View File
@@ -4,13 +4,16 @@ import { UpdatesCard } from "@/components/dashboard/UpdatesCard"
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
import { RolesCard } from "@/components/dashboard/RolesCard"
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
import { TokenStatsCard } from "@/components/dashboard/TokenStatsCard"
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() {
return (
<div className="space-y-6">
{/* Title */}
<div className="space-y-7">
<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">
Zentrale
@@ -18,25 +21,34 @@ export function DashboardView() {
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
</div>
{/* Live: aktuell geladene/arbeitende Modelle */}
<ActiveModelsCard />
{/* Zone 1 — Live-Telemetrie: beide Charts nebeneinander */}
<section>
<ZoneLabel>Live-Telemetrie</ZoneLabel>
<div className="grid gap-6 lg:grid-cols-2">
<SystemStatusCard />
<TokenPerformanceCard />
</div>
</section>
{/* Top Grid: System stats & Updates */}
<div className="grid gap-6 md:grid-cols-3">
<SystemStatusCard />
<UpdatesCard />
</div>
{/* Zone 2 — Stack-Status */}
<section>
<ZoneLabel>Stack-Status</ZoneLabel>
<div className="grid gap-6 md:grid-cols-3">
<ActiveModelsCard />
<RolesCard />
<ServicesCard />
</div>
</section>
{/* Live-Durchsatz (Performance) */}
<TokenPerformanceCard />
{/* Bottom Grid: Agent, Roles, Memory & Token Stats */}
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
<AgentStatusCard />
<RolesCard />
<MemoryInputCard />
<TokenStatsCard />
</div>
{/* Zone 3 — Betrieb & Wissen */}
<section>
<ZoneLabel>Betrieb &amp; Wissen</ZoneLabel>
<div className="grid gap-6 md:grid-cols-3">
<UpdatesCard />
<AgentStatusCard />
<MemoryInputCard />
</div>
</section>
</div>
)
}
-225
View File
@@ -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 &amp; 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 &amp; 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>
)
}