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:
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user