import { useEffect, useState } from "react" import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert, X, Power, Shield, Download, RefreshCw, Check } from "lucide-react" import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp, type Job } from "@/lib/api" import { cn, resolveExternalUrl } from "@/lib/utils" import { CustomDialog } from "@/components/CustomDialog" function gb(b: number) { return (b / 1024 ** 3).toFixed(1) } function RadialGauge({ value, label, detail }: { value: number; label: string; detail?: string }) { const radius = 24 const circ = 2 * Math.PI * radius const offset = circ - (Math.min(value, 100) / 100) * circ const strokeColor = value > 90 ? "stroke-red-500" : value > 75 ? "stroke-amber-500" : "stroke-primary" return (
{Math.round(value)}%
{label} {detail && {detail}}
) } export function DashboardView() { const [sys, setSys] = useState(null) const [agent, setAgent] = useState(null) const [models, setModels] = useState([]) const [running, setRunning] = useState([]) const [memories, setMemories] = useState([]) const [updates, setUpdates] = useState(null) const [jobs, setJobs] = useState([]) // Sudo & Action states const [msg, setMsg] = useState("") const [loading, setLoading] = useState(false) const [sudoPassword, setSudoPassword] = useState("") const [sudoLoading, setSudoLoading] = useState(false) const [sudoModal, setSudoModal] = useState<{ open: boolean actionPath: string actionLabel: string payload?: any error?: string }>({ open: false, actionPath: "", actionLabel: "" }) // Custom Dialog State const [dialog, setDialog] = useState<{ type: "alert" | "confirm" title: string message: string onConfirm: () => void onCancel?: () => void } | null>(null) const [showBrainSelect, setShowBrainSelect] = useState(false) async function changeBrainModel(model: string) { try { await api("/api/agent/brain", { method: "POST", body: JSON.stringify({ model }) }) setDialog({ type: "alert", title: "Erfolgreich", message: `Hermes-Gehirn wurde auf '${model}' geändert. Der Gateway-Dienst wurde neu gestartet.`, onConfirm: () => setDialog(null) }) loadData() setShowBrainSelect(false) } catch (e: any) { setDialog({ type: "alert", title: "Fehler", message: `Fehler beim Wechseln des Gehirns: ${e.message}`, onConfirm: () => setDialog(null) }) } } function showConfirm(title: string, message: string, onConfirm: () => void) { setDialog({ type: "confirm", title, message, onConfirm: () => { setDialog(null) onConfirm() }, onCancel: () => setDialog(null) }) } // Quick Memory Form State const [memContent, setMemContent] = useState("") const [memCat, setMemCat] = useState("stable") const [savingMem, setSavingMem] = useState(false) function loadData() { api("/api/system/status").then(setSys).catch(() => {}) api("/api/agent/status").then(setAgent).catch(() => {}) api<{ models: ModelInfo[]; running?: string[] }>("/api/models") .then((d) => { setModels(d.models || []) setRunning(d.running || []) }) .catch(() => {}) api("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {}) api("/api/maintenance/updates").then(setUpdates).catch(() => {}) api<{ jobs: Job[] }>("/api/jobs").then((d) => setJobs(d.jobs || [])).catch(() => {}) } useEffect(() => { loadData() const t = setInterval(loadData, 3000) return () => clearInterval(t) }, []) async function postAction(path: string, label: string, payload?: any, password?: string) { setMsg(`${label} wird ausgeführt...`) setLoading(true) try { const body: any = { ...payload } if (password) { body.sudo_password = password } const r = await api<{ job_id?: string; ok?: boolean; status?: string; err?: string }>(path, { method: "POST", body: JSON.stringify(body) }) if (r.status === "password_required" || r.status === "incorrect_password") { setSudoModal({ open: true, actionPath: path, actionLabel: label, payload, error: r.status === "incorrect_password" ? "Falsches Sudo-Passwort. Bitte erneut versuchen." : undefined }) setMsg("") return } if (r.job_id) { setMsg(`${label} gestartet (Job-ID: ${r.job_id})`) } else if (r.ok) { setMsg(`${label} erfolgreich ausgeführt.`) } else { setMsg(`Fehler: ${r.err || "Unbekannter Fehler"}`) } loadData() } catch (e: any) { setMsg(`Fehler bei ${label}: ${e.message}`) } finally { setLoading(false) } } async function handleSudoSubmit() { setSudoLoading(true) try { const body: any = { ...sudoModal.payload, sudo_password: sudoPassword } const r = await api<{ job_id?: string; ok?: boolean; status?: string; err?: string }>(sudoModal.actionPath, { method: "POST", body: JSON.stringify(body) }) if (r.status === "password_required" || r.status === "incorrect_password") { setSudoModal(prev => ({ ...prev, error: "Falsches Sudo-Passwort. Bitte erneut versuchen." })) return } if (r.job_id) { setMsg(`${sudoModal.actionLabel} gestartet (Job-ID: ${r.job_id})`) } else if (r.ok) { setMsg(`${sudoModal.actionLabel} erfolgreich ausgeführt.`) } else { setMsg(`Fehler: ${r.err || "Unbekannter Fehler"}`) } setSudoModal({ open: false, actionPath: "", actionLabel: "" }) setSudoPassword("") loadData() } catch (e: any) { setMsg(`Fehler: ${e.message}`) setSudoModal({ open: false, actionPath: "", actionLabel: "" }) setSudoPassword("") } finally { setSudoLoading(false) } } async function upgradeModel(repo: string, role: string) { setMsg(`Upgrade für ${repo} wird gestartet...`) try { await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, role, quant: "Q4_K_M", jinja: true }) }) setMsg(`Upgrade-Download gestartet.`) loadData() } catch (e: any) { setMsg(`Upgrade fehlgeschlagen: ${e.message}`) } } async function saveQuickMemory() { if (!memContent.trim() || savingMem) return setSavingMem(true) try { await api("/api/memory", { method: "POST", body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }), }) setMemContent("") api("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {}) } catch (e) { console.error(e) } finally { setSavingMem(false) } } // Active updates check const activeOsJob = jobs.find(j => j.label.includes("OS-Update") && (j.state === "running" || j.state === "queued")) const activeEngineJob = jobs.find(j => j.label.includes("Engine-Update") && (j.state === "running" || j.state === "queued")) return (
{/* Sudo Password Dialog Modal */} {sudoModal.open && (
Sudo-Passwort erforderlich

Für die Aktion {sudoModal.actionLabel} wird das Administrator-Passwort (Sudo) auf der Box benötigt.

setSudoPassword(e.target.value)} placeholder="Sudo-Passwort eingeben..." className="w-full h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none focus:ring-1 focus:ring-primary/50 text-foreground" onKeyDown={(e) => e.key === "Enter" && handleSudoSubmit()} autoFocus /> {sudoModal.error && (
{sudoModal.error}
)}
)} {/* Title */}

Zentrale

Aktueller Status von System, Modellen und Agent.

{/* Top Grid: System stats & Updates (3 Columns) */}
{/* Card 1: System Status (2 columns wide) */}

System-Status

{sys ? (
{sys.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null && ( )} {sys.disk && ( )}
) : (
Lade Systemdaten…
)}
{sys?.temp && (sys.temp.cpu || sys.temp.gpu) && (
{sys.temp.cpu != null && CPU Temp: {sys.temp.cpu} °C} {sys.temp.gpu != null && GPU Temp: {sys.temp.gpu} °C}
)}
{/* Card 2: Updates & Wartung (1 column wide) */}

Updates & Pflege

{updates?.last_check && ( Zuletzt gesucht: {new Date(updates.last_check * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })} )}
{updates ? (
0 ? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold" : "border-border/30 bg-background/25 text-muted-foreground" )}> OS-Pakete {updates.os > 0 ? `${updates.os} verfügbar` : "aktuell"}
0 ? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold" : "border-border/30 bg-background/25 text-muted-foreground" )}> Engine (llama.cpp) {updates.engine > 0 ? "Update verfügbar" : "aktuell"}
0 ? "border-primary/30 bg-primary/5 text-primary font-semibold animate-pulse" : "border-border/30 bg-background/25 text-muted-foreground" )}> Modell-Upgrades {updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"}
{/* Inline Action Buttons */}
{/* Model Upgrades list */} {updates.model_list.length > 0 && (
Verfügbare Modell-Upgrades:
{updates.model_list.map((m) => (
{m.role}: {m.repo.split("/").pop()}
))}
)}
) : (
Lade Updates...
)} {/* Status messages display */} {msg && (
{msg}
)} {/* Sudoers explanation text */}
OS-Update & Reboot benötigen NOPASSWD in /etc/sudoers (z.B. hitonabi ALL=(root) NOPASSWD:...) oder ein gültiges Sudo-Passwort per Pop-up.
{/* Bottom Grid: Agent, Roles & Memory (3 Columns) */}
{/* Card 3: Hermes Agent Status */}

Hermes Agent

{agent?.webui_url && ( Hermes öffnen )}
{agent ? (
Gateway
{agent.gateway_reachable ? "Online" : "Offline"}
WebUI
{agent.webui_reachable ? "Online" : "Offline"}
setShowBrainSelect(true)} className="p-3 bg-background/20 rounded-xl border border-border/40 hover:border-primary/45 hover:bg-background/30 transition-all duration-300 cursor-pointer group" >
Aktives Gehirn
{agent.brain_model ? `model: ${agent.brain_model}` : "model: auto"}
) : (
Lade Agenten-Status…
)}
Gedächtnis & Stack-Tools via MCP gekoppelt.
{/* Card 4: Slot-Belegung (Rollen & Modelle) */}

Rollen-Belegung

{["fast", "heavy", "coder", "reasoning", "vision", "scout"].map((role) => { const m = models.find((x) => x.role === role) const isRunning = m ? running.includes(m.name) : false return (
{role} {m ? m.name.split("/").pop()?.replace(/\.gguf$/i, "") : "nicht zugewiesen"}
{m ? ( isRunning ? ( warm ) : ( bereit ) ) : ( )}
) })}
Laden erfolgt automatisch per Auto-Swap.
{/* Card 5: Quick Memory Input */}

Gedächtnis