Feat: Add manual updates check, Settings tab, direct model upgrades, logs password redirection, fix engine version detection, and expand AI agent Guide tab

This commit is contained in:
Hitonabi
2026-06-26 13:09:50 +02:00
parent 563e7837b9
commit 0c7b0b19af
11 changed files with 1122 additions and 599 deletions
+211 -8
View File
@@ -1,12 +1,12 @@
import { useEffect, useState, useRef } from "react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power } from "lucide-react"
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle } from "lucide-react"
import { api, type Job, type UpdatesResp } from "@/lib/api"
import { cn } from "@/lib/utils"
interface SystemDrawerProps {
open: boolean
onClose: () => void
defaultTab?: "maintenance" | "logs"
defaultTab?: "maintenance" | "logs" | "settings"
}
const SERVICES = [
@@ -29,8 +29,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
const [selectedService, setSelectedService] = useState("llama-swap")
const [logs, setLogs] = useState("")
const [loadingLogs, setLoadingLogs] = useState(false)
const [logStatus, setLogStatus] = useState<string | null>(null)
const [restartingServices, setRestartingServices] = useState<Record<string, boolean>>({})
const [activeTab, setActiveTab] = useState<"maintenance" | "logs">("maintenance")
const [activeTab, setActiveTab] = useState<"maintenance" | "logs" | "settings">("maintenance")
const [checkingUpdates, setCheckingUpdates] = useState(false)
// 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) {
@@ -54,12 +69,16 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
function loadServiceLogs(service: string) {
setLoadingLogs(true)
api<{ ok: boolean; text: string; err?: string }>(`/api/maintenance/logs?service=${service}&lines=150`)
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}`))
@@ -112,6 +131,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
}
}
async function triggerCheckUpdates() {
setCheckingUpdates(true)
try {
await api("/api/maintenance/check-updates", { method: "POST" })
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
alert(`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 })
})
alert(`Modell-Upgrade für '${role}' (${repo}) gestartet.`)
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
alert(`Fehler beim Starten des Modell-Upgrades: ${e.message}`)
}
}
async function triggerReboot() {
if (!confirm("Bist du sicher, dass du das gesamte Host-System neu starten willst?")) return
try {
@@ -210,15 +256,36 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
>
System-Logs
</button>
<button
onClick={() => setActiveTab("settings")}
className={cn(
"flex-1 py-3 text-xs font-semibold uppercase tracking-wider border-b-2 text-center transition-all",
activeTab === "settings"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
Einstellungen
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{activeTab === "maintenance" ? (
{activeTab === "maintenance" && (
<>
{/* Quick Actions */}
<div className="space-y-3">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
<button
onClick={triggerCheckUpdates}
disabled={checkingUpdates}
className="flex items-center gap-1 text-[10px] font-semibold text-primary hover:text-primary/80 transition-colors disabled:opacity-50"
>
<RefreshCw className={cn("h-3 w-3", checkingUpdates && "animate-spin")} />
Nach Updates suchen
</button>
</div>
<div className="grid grid-cols-2 gap-3">
<button
@@ -256,6 +323,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</button>
</div>
{/* Modell Upgrades */}
{updates?.model_list && updates.model_list.length > 0 && (
<div className="space-y-3">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Verfügbare Modell-Upgrades</h3>
<div className="space-y-2">
{updates.model_list.map((m) => (
<div key={m.role} className="p-3 rounded-xl border border-border/60 bg-background/20 flex flex-col gap-2">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-xs font-semibold">{m.title}</div>
<div className="text-[10px] font-mono text-muted-foreground">{m.repo}</div>
<div className="text-[10px] text-primary font-semibold uppercase mt-0.5">Rolle: {m.role}</div>
</div>
<button
onClick={() => triggerModelUpgrade(m.repo, m.role)}
className="flex items-center gap-1.5 text-[10px] font-semibold text-emerald-400 hover:text-emerald-300 border border-emerald-500/20 bg-emerald-500/5 hover:bg-emerald-500/10 px-2 py-1 rounded-lg transition-colors shrink-0"
>
<Download className="h-3.5 w-3.5" />
Upgrade
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Active Jobs */}
<div className="space-y-3">
<div className="flex items-center justify-between">
@@ -347,7 +441,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</div>
</div>
</>
) : (
)}
{activeTab === "logs" && (
// Logs View
<div className="flex flex-col h-full space-y-4">
{/* Service Select & Restart */}
@@ -397,7 +493,23 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
ref={logContainerRef}
className="flex-1 p-4 overflow-y-auto text-[10px] font-mono text-cyan-300/90 whitespace-pre-wrap select-text leading-relaxed scrollbar-thin"
>
{loadingLogs && !logs ? (
{logStatus === "password_required" || logStatus === "incorrect_password" ? (
<div className="flex flex-col items-center justify-center p-6 text-center h-full space-y-3">
<AlertTriangle className="h-8 w-8 text-amber-400 animate-pulse animate-duration-1000" />
<div className="text-xs font-semibold text-amber-300">
{logStatus === "incorrect_password" ? "Falsches Sudo-Passwort hinterlegt." : "Sudo-Passwort für systemd-Dienste erforderlich."}
</div>
<p className="text-[10px] text-muted-foreground max-w-xs leading-normal">
Für das Auslesen der systemd-Logs von {selectedService} werden Root-Rechte benötigt. Hinterlege dein Passwort in den Einstellungen.
</p>
<button
onClick={() => setActiveTab("settings")}
className="px-3 py-1.5 text-[10px] font-semibold text-primary-foreground bg-primary hover:bg-primary/95 rounded-md transition-colors mt-2"
>
Sudo-Passwort eintragen
</button>
</div>
) : loadingLogs && !logs ? (
<span className="text-muted-foreground">Lade Logs...</span>
) : (
logs || <span className="text-muted-foreground">Keine Logeinträge vorhanden.</span>
@@ -406,6 +518,97 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
</div>
</div>
)}
{activeTab === "settings" && (
<div className="space-y-6">
<div className="space-y-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Zugangsdaten & Schlüssel</h3>
<p className="text-[10px] text-muted-foreground leading-normal">
Diese Daten werden ausschließlich lokal in deinem Browser (localStorage) gespeichert und bei Bedarf an das Backend übertragen.
</p>
</div>
{/* Sudo Passwort */}
<div className="space-y-2">
<label className="text-xs font-semibold flex items-center gap-1.5">
<Shield className="h-4 w-4 text-amber-400" />
Host Sudo-Passwort
</label>
<div className="relative">
<input
type={showSudo ? "text" : "password"}
value={sudoPasswordInput}
onChange={(e) => setSudoPasswordInput(e.target.value)}
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"
/>
<button
type="button"
onClick={() => setShowSudo(!showSudo)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showSudo ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
Wird benötigt für systemd-Dienste (Llama Swap logs/restart), OS-Update (apt) und Reboot.
</p>
</div>
{/* HuggingFace Token */}
<div className="space-y-2">
<label className="text-xs font-semibold flex items-center gap-1.5">
<Key className="h-4 w-4 text-violet-400" />
HuggingFace API Token
</label>
<div className="relative">
<input
type={showHf ? "text" : "password"}
value={hfTokenInput}
onChange={(e) => setHfTokenInput(e.target.value)}
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"
/>
<button
type="button"
onClick={() => setShowHf(!showHf)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground transition-colors"
>
{showHf ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-[9px] text-muted-foreground leading-normal">
Erlaubt das Herunterladen von geschützten oder Gate-restricted Modellen direkt über Mission Control.
</p>
</div>
{/* Buttons */}
<div className="flex gap-3 pt-2">
<button
onClick={() => {
localStorage.setItem("mc_sudo_password", sudoPasswordInput);
localStorage.setItem("mc_hf_token", hfTokenInput);
alert("Einstellungen erfolgreich lokal gespeichert.");
}}
className="flex-1 h-9 flex items-center justify-center text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors"
>
Speichern
</button>
<button
onClick={() => {
setSudoPasswordInput("");
setHfTokenInput("");
localStorage.removeItem("mc_sudo_password");
localStorage.removeItem("mc_hf_token");
alert("Zugangsdaten gelöscht.");
}}
className="h-9 px-4 flex items-center justify-center text-xs font-semibold text-muted-foreground border border-border/60 bg-background/20 hover:bg-accent rounded-lg transition-colors"
>
Zurücksetzen
</button>
</div>
</div>
)}
</div>
</div>
</>