diff --git a/frontend/src/components/SystemDrawer.tsx b/frontend/src/components/SystemDrawer.tsx
index ddfa91c..0a32aab 100644
--- a/frontend/src/components/SystemDrawer.tsx
+++ b/frontend/src/components/SystemDrawer.tsx
@@ -1,8 +1,11 @@
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 { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, AlertTriangle, 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 { SettingsTab } from "./system/SettingsTab"
+import { UpdateDetailModal, type UpdateDetailState } from "./system/UpdateDetailModal"
interface SystemDrawerProps {
@@ -11,58 +14,6 @@ interface SystemDrawerProps {
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)
@@ -81,7 +32,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
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)
+ const [detail, setDetail] = useState(null)
// Custom Dialog State
const [dialog, setDialog] = useState<{
@@ -128,18 +79,6 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
})
}
- // 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) {
@@ -702,223 +641,14 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
)}
- {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 */}
-
-
-
-
-
- )}
+ {activeTab === "settings" && }
- {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 */}
-
-
-
-
-
-
- )
- })()}
+ {detail && (
+
setDetail(null)} onApply={applyFromDetail} />
+ )}
{dialog && (
void
+}) {
+ 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])
+
+ return (
+
+
+
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 */}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/system/UpdateDetailModal.tsx b/frontend/src/components/system/UpdateDetailModal.tsx
new file mode 100644
index 0000000..b0c3d2c
--- /dev/null
+++ b/frontend/src/components/system/UpdateDetailModal.tsx
@@ -0,0 +1,127 @@
+import { ArrowRight, Bot, ExternalLink, GitCommit, Package, RefreshCw, Server, Shield, Shuffle, X } from "lucide-react"
+import { cn } from "@/lib/utils"
+import type { Job, UpdateDetails } from "@/lib/api"
+
+export type UpdateDetailState = { kind: "os" | "engine" | "swap" | "hermes"; loading: boolean; data: UpdateDetails | null }
+
+// Update-Detail-Fenster des SystemDrawers (Review P2-14: extrahiert): zeigt VOR dem
+// Anwenden, was genau aktualisiert wird (apt-Pakete, Engine-Builds, Hermes-Commits).
+export function UpdateDetailModal({ detail, maintenanceJob, onClose, onApply }: {
+ detail: UpdateDetailState
+ maintenanceJob?: Job
+ onClose: () => void
+ onApply: () => void
+}) {
+ 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 (
+
+
+
+ {/* 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 */}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/system/rows.tsx b/frontend/src/components/system/rows.tsx
new file mode 100644
index 0000000..6500d62
--- /dev/null
+++ b/frontend/src/components/system/rows.tsx
@@ -0,0 +1,57 @@
+import { RefreshCw, FileText } from "lucide-react"
+import { cn } from "@/lib/utils"
+
+// Präsentations-Atome des SystemDrawers (Review P2-14: aus dem 934-Z-Monolithen extrahiert).
+
+// systemd-Units (restart/logs) + reach = Stichwort zum Mappen auf /api/system/services.
+export 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" },
+]
+
+export 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 (
+
+
+
+
+
+ )
+}
+
+export function ServiceRow({ label, ok, system, busy, onRestart, onLogs }: {
+ label: string; ok?: boolean; system?: boolean; busy?: boolean; onRestart: () => void; onLogs: () => void
+}) {
+ return (
+
+
+ {label}{system && (root)}
+
+
+
+ )
+}
+
+export 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`
+}