import { useEffect, useState } from "react" import { X, RefreshCw, Cpu, Server, Shield, Power, Camera, Bot, Box, 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" import { SERVICES, ServiceRow, UpdateRow, formatBytes } from "./system/rows" import { LogConsole } from "./system/LogConsole" import { SettingsTab } from "./system/SettingsTab" import { UpdateDetailModal, type UpdateDetailState } from "./system/UpdateDetailModal" interface SystemDrawerProps { open: boolean onClose: () => void defaultTab?: "maintenance" | "logs" | "settings" } 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(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" }) } useEffect(() => { if (open && defaultTab) { setActiveTab(defaultTab) } }, [open, defaultTab]) 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)) } // 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 } // Passwort-Fehler NICHT mehr verschlucken: OS-Update braucht das Box-Passwort. Früher // lief die Aktion bei fehlendem/falschem Passwort ins Leere („passiert nichts"). function handledSudo(res: any): boolean { if (res?.status === "password_required" || res?.status === "incorrect_password") { showAlert( "Box-Passwort nötig", (res.status === "incorrect_password" ? "Das gespeicherte Box-Passwort stimmt nicht. " : "Für dieses Update braucht die Box dein Passwort. ") + "Bitte oben im Reiter „Einstellungen“ setzen bzw. korrigieren.", ) return true } if (res && res.ok === false) { showAlert("Update fehlgeschlagen", res.err || "Unbekannter Fehler.") return true } return false } async function triggerOsUpdate() { try { const res = await api("/api/maintenance/os-update", { method: "POST" }) if (handledBusy(res) || handledSudo(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) || handledSudo(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) || handledSudo(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 */}
loadServiceLogs(selectedService)} onOpenSettings={() => setActiveTab("settings")} />
)} {activeTab === "settings" && }
{detail && ( setDetail(null)} onApply={applyFromDetail} /> )} {dialog && ( )} ) }