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,5 +1,5 @@
|
||||
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 { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "./CustomDialog"
|
||||
@@ -35,6 +35,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
|
||||
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false)
|
||||
const [backupMsg, setBackupMsg] = useState("")
|
||||
const [backupRunning, setBackupRunning] = useState(false)
|
||||
|
||||
// Custom Dialog State
|
||||
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) {
|
||||
setRestartingServices(prev => ({ ...prev, [serviceId]: true }))
|
||||
try {
|
||||
@@ -384,6 +399,28 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</button>
|
||||
</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 */}
|
||||
{updates?.model_list && updates.model_list.length > 0 && (
|
||||
<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 (
|
||||
<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 className="mb-3 flex items-center gap-2">
|
||||
<Cpu className="h-4.5 w-4.5 text-primary" />
|
||||
|
||||
@@ -55,8 +55,13 @@ export function TokenPerformanceCard() {
|
||||
</div>
|
||||
)}
|
||||
{ts && (
|
||||
<div className="mt-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 className="mt-0.5 space-y-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user