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 (
)
}
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 */}
{/* 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 */}
{/* HuggingFace Token */}
{/* 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 && (
)}
>
)
}