import { useEffect, useState, useRef } from "react" import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, AlertTriangle, Camera, Bot, Box, FileText, Package, GitCommit, ExternalLink, ArrowRight, Shuffle } from "lucide-react" import { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api" import { cn } from "@/lib/utils" import { CustomDialog } from "./CustomDialog" interface SystemDrawerProps { open: boolean onClose: () => void defaultTab?: "maintenance" | "logs" | "settings" } // systemd-Units (restart/logs) + reach = Stichwort zum Mappen auf /api/system/services. const SERVICES = [ { id: "mission-control-2", label: "Mission Control", type: "user", reach: "gateway (integr" }, { id: "hermes-gateway", label: "Hermes Gateway", type: "user", reach: "hermes-gateway" }, { id: "mem0-service", label: "Mem0 (Gedächtnis)", type: "user", reach: "mem0" }, { id: "hermes-terminal", label: "Hermes Terminal", type: "user", reach: "hermes-terminal" }, { id: "llama-swap", label: "Llama Swap", type: "system", reach: "llama-swap" }, ] function UpdateRow({ icon: Icon, iconClass, name, status, available, busy, actionLabel, onAction }: { icon: any; iconClass: string; name: string; status: string; available: boolean; busy?: boolean; actionLabel: string; onAction: () => void }) { return (
{name}
{status}
) } function ServiceRow({ label, ok, system, busy, onRestart, onLogs }: { label: string; ok?: boolean; system?: boolean; busy?: boolean; onRestart: () => void; onLogs: () => void }) { return (
{label}{system && (root)}
) } function formatBytes(bytes?: number) { if (bytes == null) return "" if (bytes > 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB` return `${(bytes / 1024 ** 2).toFixed(1)} MB` } export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: SystemDrawerProps) { const [updates, setUpdates] = useState(null) const [jobs, setJobs] = useState([]) const [selectedService, setSelectedService] = useState("llama-swap") const [logs, setLogs] = useState("") const [loadingLogs, setLoadingLogs] = useState(false) const [logStatus, setLogStatus] = useState(null) const [restartingServices, setRestartingServices] = useState>({}) const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance") const [checkingUpdates, setCheckingUpdates] = useState(false) const [backupMsg, setBackupMsg] = useState("") const [backupRunning, setBackupRunning] = useState(false) const [services, setServices] = useState(null) const [backups, setBackups] = useState<{ snapshot: string; size_mb?: number }[]>([]) const [hermesUpdating, setHermesUpdating] = useState(false) // Update-Detail-Fenster: zeigt VOR dem Anwenden, was genau aktualisiert wird. const [detail, setDetail] = useState<{ kind: "os" | "engine" | "swap" | "hermes"; loading: boolean; data: UpdateDetails | null } | null>(null) // 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 showConfirm(title: string, message: string, onConfirm: () => void) { setDialog({ type: "confirm", title, message, onConfirm: () => { setDialog(null) onConfirm() }, onCancel: () => setDialog(null) }) } function formatLastCheck(ts?: number | null) { if (!ts) return "Nie" return new Date(ts * 1000).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }) } // Credentials State const [sudoPasswordInput, setSudoPasswordInput] = useState("") const [hfTokenInput, setHfTokenInput] = useState("") const [showSudo, setShowSudo] = useState(false) const [showHf, setShowHf] = useState(false) useEffect(() => { if (open) { setSudoPasswordInput(localStorage.getItem("mc_sudo_password") || "") setHfTokenInput(localStorage.getItem("mc_hf_token") || "") } }, [open]) useEffect(() => { if (open && defaultTab) { setActiveTab(defaultTab) } }, [open, defaultTab]) const logContainerRef = useRef(null) function loadUpdates() { api("/api/maintenance/updates") .then(setUpdates) .catch((e) => console.error("Error loading updates", e)) } function loadJobs() { api<{ jobs: Job[] }>("/api/jobs") .then((data) => setJobs(data.jobs || [])) .catch((e) => console.error("Error loading jobs", e)) } function loadServices() { api("/api/system/services").then(setServices).catch(() => {}) } function loadBackups() { api<{ backups: { snapshot: string; size_mb?: number }[] }>("/api/system/backups") .then((d) => setBackups(d.backups || [])).catch(() => {}) } function loadServiceLogs(service: string) { setLoadingLogs(true) setLogStatus(null) api<{ ok: boolean; text: string; err?: string; status?: string }>(`/api/maintenance/logs?service=${service}&lines=150`) .then((res) => { if (res.ok) { setLogs(res.text) } else { setLogs(`Fehler beim Laden der Logs: ${res.err || "Unbekannter Fehler"}`) if (res.status === "incorrect_password" || res.status === "password_required") { setLogStatus(res.status) } } }) .catch((e) => setLogs(`Fehler: ${e.message}`)) .finally(() => { setLoadingLogs(false) // Scroll to bottom setTimeout(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight } }, 50) }) } // Poll jobs & updates when open useEffect(() => { if (!open) return loadUpdates() loadJobs() loadServices() loadBackups() const timer = setInterval(() => { loadJobs() loadUpdates() loadServices() }, 3000) return () => clearInterval(timer) }, [open]) // Load logs when tab is active or service changes useEffect(() => { if (!open || activeTab !== "logs") return loadServiceLogs(selectedService) }, [open, activeTab, selectedService]) // Backend-Wartungsriegel: lehnt ein zweites Update ab, solange eines läuft. function handledBusy(res: any): boolean { if (res?.status === "busy") { showAlert("Update läuft bereits", `Es läuft gerade „${res.running}". Bitte warte, bis es fertig ist.`) loadJobs() return true } return false } async function triggerOsUpdate() { try { const res = await api("/api/maintenance/os-update", { method: "POST" }) if (handledBusy(res)) return loadJobs() setActiveTab("maintenance") } catch (e: any) { showAlert("Fehler", `Fehler beim Starten des OS-Updates: ${e.message}`) } } async function triggerEngineUpdate() { try { const res = await api("/api/maintenance/engine-update", { method: "POST" }) if (handledBusy(res)) return loadJobs() setActiveTab("maintenance") } catch (e: any) { showAlert("Fehler", `Fehler beim Engine-Update: ${e.message}`) } } async function triggerSwapUpdate() { try { const res = await api("/api/maintenance/swap-update", { method: "POST" }) if (handledBusy(res)) return loadJobs() setActiveTab("maintenance") } catch (e: any) { showAlert("Fehler", `Fehler beim Router-Update: ${e.message}`) } } async function doHermesUpdate() { setHermesUpdating(true) try { const res = await api("/api/maintenance/hermes-update", { method: "POST" }) if (handledBusy(res)) return loadJobs() } catch (e: any) { showAlert("Fehler", `Hermes-Update fehlgeschlagen: ${e.message}`) } finally { setHermesUpdating(false) } } // Öffnet das Detail-Fenster und lädt, was genau aktualisiert würde. async function openUpdateDetails(kind: "os" | "engine" | "swap" | "hermes") { setDetail({ kind, loading: true, data: null }) try { const d = await api(`/api/maintenance/update-details?kind=${kind}`) setDetail({ kind, loading: false, data: d }) } catch (e: any) { setDetail({ kind, loading: false, data: { kind, error: e.message } }) } } // Bestätigung aus dem Detail-Fenster → startet das passende Update. function applyFromDetail() { const kind = detail?.kind setDetail(null) if (kind === "os") triggerOsUpdate() else if (kind === "engine") triggerEngineUpdate() else if (kind === "swap") triggerSwapUpdate() else if (kind === "hermes") doHermesUpdate() } async function triggerCheckUpdates() { setCheckingUpdates(true) try { await api("/api/maintenance/check-updates", { method: "POST" }) loadJobs() setActiveTab("maintenance") } catch (e: any) { showAlert("Fehler", `Fehler bei der Update-Suche: ${e.message}`) } finally { setCheckingUpdates(false) } } async function triggerModelUpgrade(repo: string, role: string) { try { await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, role }) }) showAlert("Gestartet", `Modell-Upgrade für '${role}' (${repo}) gestartet.`) loadJobs() setActiveTab("maintenance") } catch (e: any) { showAlert("Fehler", `Fehler beim Starten des Modell-Upgrades: ${e.message}`) } } async function triggerReboot() { showConfirm( "Reboot bestätigen", "Bist du sicher, dass du das gesamte Host-System neu starten willst?", async () => { try { await api("/api/maintenance/reboot", { method: "POST" }) showAlert("Reboot", "Reboot ausgelöst. System startet neu...", () => { onClose() }) } catch (e: any) { showAlert("Fehler", `Fehler beim Reboot: ${e.message}`) } } ) } 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} Komponenten)` : "Backup fehlgeschlagen.") loadBackups() } catch (e: any) { setBackupMsg(`Fehler: ${e.message}`) } finally { setBackupRunning(false) } } async function restartService(serviceId: string) { setRestartingServices(prev => ({ ...prev, [serviceId]: true })) try { const res = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", { method: "POST", body: JSON.stringify({ service: serviceId }) }) if (res.ok) { showAlert("Dienst neu gestartet", `Dienst ${serviceId} wurde erfolgreich neu gestartet.`, () => { if (activeTab === "logs" && selectedService === serviceId) { loadServiceLogs(serviceId) } }) } else { showAlert("Fehler", `Fehler beim Neustart: ${res.err || "Unbekannter Fehler"}`) } } catch (e: any) { showAlert("Fehler", `Fehler beim Neustart: ${e.message}`) } finally { setRestartingServices(prev => ({ ...prev, [serviceId]: false })) } } async function cancelJob(jobId: string) { try { await api(`/api/jobs/${jobId}/cancel`, { method: "POST" }) loadJobs() } catch (e: any) { showAlert("Fehler", `Fehler beim Abbrechen: ${e.message}`) } } // Aktiver Wartungs-Job (System-Update) → UI-Aktionen sperren, Dashboard bleibt sichtbar. const maintenanceJob = jobs.find(j => (j.state === "running" || j.state === "queued") && j.group === "maintenance") return ( <> {/* Backdrop */}
{/* Drawer */}
{/* Header */}

OS-Zentrale & Pflege

{/* Navigation Tabs */}
{/* Content */}
{activeTab === "maintenance" && ( <> {/* Updates */}

Updates

{updates?.last_check && (
Zuletzt gesucht: {formatLastCheck(updates.last_check)}
)} {maintenanceJob && (
Update läuft: {maintenanceJob.label} — bitte warten. Weitere Updates sind solange gesperrt.
)}
openUpdateDetails("os")} /> openUpdateDetails("engine")} /> openUpdateDetails("swap")} /> {(() => { const h = updates?.components?.find((c) => c.key === "hermes_agent") return ( openUpdateDetails("hermes")} /> ) })()} {updates?.model_list?.map((m) => ( triggerModelUpgrade(m.repo, m.role)} /> ))}
{/* Dienste */}

Dienste

{SERVICES.map((s) => ( x.name.toLowerCase().includes(s.reach))?.ok} busy={restartingServices[s.id]} onRestart={() => restartService(s.id)} onLogs={() => { setSelectedService(s.id); setActiveTab("logs") }} /> ))}
{/* Backup */}

Backup

{backups[0] ? `Letztes: ${backups[0].snapshot}` : "Noch kein Backup"}
{backups.length} Snapshots · Restore per CLI (restore.sh)
{backupMsg && (
{backupMsg}
)}
{/* Gefahrenzone */}

Gefahrenzone

{/* Active Jobs */}

Hintergrund-Aufgaben

{jobs.filter(j => j.state === "running" || j.state === "queued").length} Aktiv
{jobs.length === 0 ? (
Aktuell keine aktiven Hintergrund-Jobs.
) : ( jobs.map((job) => { const isActive = job.state === "running" || job.state === "queued" return (
{isActive && ( )} {job.label}
ID: {job.id} {job.state}
{isActive && ( )}
{/* Progress */} {job.state === "running" && (
{job.progress ?? 0}% {job.done_bytes != null && job.total_bytes != null && ( {formatBytes(job.done_bytes)} / {formatBytes(job.total_bytes)} {job.rate_bps != null && ` (${formatBytes(job.rate_bps)}/s)`} )} {job.eta_s != null && ETA: {job.eta_s}s}
)}
) }) )}
)} {activeTab === "logs" && ( // Logs View
{/* Service Select & Restart */}
{/* Log Console Container */}
{/* Console Header */}
stdout/stderr - {selectedService}
{/* Console Log Area */}
                  {logStatus === "password_required" || logStatus === "incorrect_password" ? (
                    
{logStatus === "incorrect_password" ? "Falsches Sudo-Passwort hinterlegt." : "Sudo-Passwort für systemd-Dienste erforderlich."}

Für das Auslesen der systemd-Logs von {selectedService} werden Root-Rechte benötigt. Hinterlege dein Passwort in den Einstellungen.

) : loadingLogs && !logs ? ( Lade Logs... ) : ( logs || Keine Logeinträge vorhanden. )}
)} {activeTab === "settings" && (

Zugangsdaten & Schlüssel

Diese Daten werden ausschließlich lokal in deinem Browser (localStorage) gespeichert und bei Bedarf an das Backend übertragen.

{/* Sudo Passwort */}
setSudoPasswordInput(e.target.value)} aria-label="Host Sudo-Passwort" name="sudo-password" autoComplete="off" spellCheck={false} placeholder="Sudo-Passwort für System-Operationen" className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" />

Wird benötigt für systemd-Dienste (Llama Swap logs/restart), OS-Update (apt) und Reboot.

{/* HuggingFace Token */}
setHfTokenInput(e.target.value)} aria-label="HuggingFace API Token" name="hf-token" autoComplete="off" spellCheck={false} placeholder="hf_…" className="w-full h-10 pl-3 pr-10 text-xs bg-background/40 border border-border/60 rounded-lg outline-none focus:border-primary transition-colors text-foreground" />

Erlaubt das Herunterladen von geschützten oder Gate-restricted Modellen direkt über Mission Control.

{/* Buttons */}
)}
{detail && (() => { const d = detail.data const meta = { os: { icon: Shield, cls: "text-cyan-400", title: "OS-Pakete (apt)" }, engine: { icon: Server, cls: "text-violet-400", title: "Inferenz-Engine (llama.cpp)" }, swap: { icon: Shuffle, cls: "text-fuchsia-400", title: "Router (llama-swap)" }, hermes: { icon: Bot, cls: "text-amber-400", title: "Hermes-Agent" }, }[detail.kind] const Icon = meta.icon const nothing = !d ? true : detail.kind === "os" ? (d.count ?? 0) === 0 : detail.kind === "hermes" ? (d.behind ?? 0) === 0 : (d.installed_build != null && d.latest_build != null && d.latest_build <= d.installed_build) return (
setDetail(null)} />
{/* Header */}

{meta.title}

{/* Body */}
{detail.loading ? (
Details werden geladen…
) : d?.error ? (
{d.error}
) : detail.kind === "os" ? ( (d?.count ?? 0) === 0 ? (
Keine Pakete zu aktualisieren — System ist aktuell.
) : ( <>
{d!.count} Paket(e) werden aktualisiert:
{d!.packages!.map((p) => (
{p.name} {p.current}{p.candidate}
))}
) ) : detail.kind === "engine" || detail.kind === "swap" ? ( <>
Build {d?.installed_build ?? "?"} Build {d?.latest_build ?? "?"}
{(d?.name || d?.latest_tag) && (
Release: {d?.name}{d?.latest_tag ? ` (${d.latest_tag})` : ""}
)} {d?.url && ( Release-Notes auf GitHub )} {d?.body && (
{d.body}
)} ) : ( // hermes (d?.commits?.length ?? 0) === 0 ? (
Keine neuen Commits — Hermes-Agent ist bereits aktuell.
) : ( <>
{d!.behind} neue Commit(s) auf origin/{d!.branch}:
{d!.commits!.map((c) => (
{c.subject}
{c.hash} · {c.when}
))}

Vor dem Update wird automatisch ein Backup erstellt; danach startet der Hermes-Gateway neu.

) )}
{/* Footer */}
) })()} {dialog && ( )} ) }