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 ( -
- -
-
{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) @@ -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 ( +
+ +
+
{name}
+
{status}
+
+ +
+ ) +} + +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` +}