Feat: System-Wartung-Rework + Hermes-Update-Button
Wartungs-Tab im SystemDrawer neu strukturiert: - UPDATES: eine einheitliche Liste (OS, Engine, Hermes-Agent, Modell-Upgrades) mit Status + Aktion-Button, der nur aktiv ist wenn ein Update ansteht (statt Klick-Karten, die sofort updaten). Konsistent mit der Dashboard-UpdatesCard. - DIENSTE: neue Sektion, alle systemd-Units mit Status-Punkt (aus /api/system/services, jetzt inkl. mem0-service) + Restart + Logs-Sprung. - BACKUP: kompakt (letztes Backup + Snapshot-Button + Restore-Hinweis). - GEFAHRENZONE: Reboot abgetrennt. Jobs nur wenn vorhanden. Hermes-Update-Button: POST /api/maintenance/hermes-update -> Job (Backup -> `hermes update --yes` (git pull + deps) -> hermes-gateway restart). Backend: hermes_update_job + USER_SERVICES um mem0-service/hermes-terminal ergaenzt (Restart ging vorher nicht), mem0 in services-API. Live verifiziert: Button hat Hermes d470ed0 -> 3b44a3c aktualisiert, Integration intakt (memory.provider, Plugin laedt), Pre-Update-Backup angelegt, Drawer rendert fehlerfrei. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,11 @@ def engine_update(body: SudoReq) -> dict:
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/maintenance/hermes-update")
|
||||
def hermes_update() -> dict:
|
||||
return maintenance.hermes_update_job()
|
||||
|
||||
|
||||
@router.post("/maintenance/reboot")
|
||||
def reboot(body: SudoReq) -> dict:
|
||||
return maintenance.reboot(body.sudo_password)
|
||||
|
||||
@@ -12,7 +12,9 @@ import subprocess
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL
|
||||
import httpx
|
||||
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL
|
||||
from services import backup as backup_svc
|
||||
from services.agent import agent_status
|
||||
from services.gateway import gateway_reachable
|
||||
@@ -36,6 +38,13 @@ def status() -> dict:
|
||||
return system_status()
|
||||
|
||||
|
||||
def _mem0_reachable() -> bool:
|
||||
try:
|
||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/system/services")
|
||||
def services() -> dict:
|
||||
"""Aggregierte Erreichbarkeit aller Stack-Dienste (für die Health-Anzeige)."""
|
||||
@@ -47,6 +56,7 @@ def services() -> dict:
|
||||
{"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()},
|
||||
{"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
|
||||
{"name": "Hermes-Terminal", "url": a["terminal_url"], "ok": a["terminal_reachable"]},
|
||||
{"name": "Mem0 (Gedächtnis)", "url": MEM0_SERVICE_URL, "ok": _mem0_reachable()},
|
||||
],
|
||||
"links": {
|
||||
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
|
||||
|
||||
@@ -19,7 +19,7 @@ from services import catalog, discover, jobengine, llamaswap, system
|
||||
|
||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||
SYSTEM_SERVICES = {"llama-swap"}
|
||||
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-dashboard", "hermes-webui"}
|
||||
USER_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-terminal", "mem0-service"}
|
||||
|
||||
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
@@ -346,6 +346,25 @@ def engine_update_job(sudo_password: str | None = None) -> dict | None:
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
def hermes_update_job() -> dict:
|
||||
"""Hermes-Agent aktualisieren wie die CLI (`hermes update` = git pull + Deps), danach
|
||||
den Gateway neu starten. Davor ein Sicherheits-Backup (unser deploy/backup.sh). Kein sudo
|
||||
(alles im User-Space). Läuft als Hintergrund-Job (kann ~1 Min dauern)."""
|
||||
git = system.find_hermes_agent_git()
|
||||
path = (git or {}).get("path") or os.path.expanduser("~/.hermes/hermes-agent")
|
||||
py = os.path.join(path, "venv", "bin", "python")
|
||||
backup = os.path.join(_REPO_ROOT, "deploy", "backup.sh")
|
||||
cmd = (f"bash {backup} || true; "
|
||||
f"cd {path} && {py} -m hermes_cli.main update --yes "
|
||||
f"&& systemctl --user restart hermes-gateway")
|
||||
|
||||
def on_done():
|
||||
_comp_cache.update(ts=0.0, data=[]) # Update-Status neu berechnen lassen
|
||||
|
||||
job_id = jobengine.start_job(["bash", "-c", cmd], "Hermes-Agent-Update", on_done=on_done)
|
||||
return {"ok": True, "job_id": job_id}
|
||||
|
||||
|
||||
def reboot(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-DkZXHl-q.js";/**
|
||||
import{r as K,a as Hv,g as Hr,R as Um,c as q2,j as Ne,l as Z2,S as K2,b as Vx,T as $2}from"./index-cq5kdO0r.js";/**
|
||||
* @license
|
||||
* Copyright 2010-2023 Three.js Authors
|
||||
* SPDX-License-Identifier: MIT
|
||||
+1
File diff suppressed because one or more lines are too long
-494
File diff suppressed because one or more lines are too long
+504
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-DkZXHl-q.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-qbOD334z.css">
|
||||
<script type="module" crossorigin src="/assets/index-cq5kdO0r.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-3d2FUwyu.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, Download, AlertTriangle, Save } from "lucide-react"
|
||||
import { api, type Job, type UpdatesResp } from "@/lib/api"
|
||||
import { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, AlertTriangle, Camera, Bot, Box, FileText } from "lucide-react"
|
||||
import { api, type Job, type UpdatesResp, type ServicesResp } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "./CustomDialog"
|
||||
|
||||
@@ -11,13 +11,53 @@ interface SystemDrawerProps {
|
||||
defaultTab?: "maintenance" | "logs" | "settings"
|
||||
}
|
||||
|
||||
// systemd-Units (restart/logs) + reach = Stichwort zum Mappen auf /api/system/services.
|
||||
const SERVICES = [
|
||||
{ id: "llama-swap", label: "Llama Swap", type: "system" },
|
||||
{ id: "mission-control-2", label: "Mission Control 2", type: "user" },
|
||||
{ id: "hermes-gateway", label: "Hermes Gateway", type: "user" },
|
||||
{ id: "hermes-terminal", label: "Hermes Terminal", type: "user" }
|
||||
{ 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, name, status, available, busy, actionLabel, onAction }: {
|
||||
icon: any; name: string; status: string; available: boolean; busy?: boolean; actionLabel: string; onAction: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-3 rounded-lg border px-3 py-2",
|
||||
available ? "border-amber-500/30 bg-amber-500/5" : "border-border/50 bg-background/20")}>
|
||||
<Icon className={cn("h-4 w-4 shrink-0", available ? "text-amber-400" : "text-muted-foreground")} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-foreground truncate">{name}</div>
|
||||
<div className={cn("text-[10px] truncate", available ? "text-amber-400/90" : "text-muted-foreground")}>{status}</div>
|
||||
</div>
|
||||
<button onClick={onAction} disabled={!available || busy}
|
||||
className={cn("text-[11px] font-semibold rounded-lg px-2.5 py-1 transition-colors shrink-0",
|
||||
available ? "bg-primary text-primary-foreground hover:opacity-90 cursor-pointer" : "border border-border/50 text-muted-foreground/40 cursor-default")}>
|
||||
{busy ? "…" : actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServiceRow({ label, ok, system, busy, onRestart, onLogs }: {
|
||||
label: string; ok?: boolean; system?: boolean; busy?: boolean; onRestart: () => void; onLogs: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 rounded-lg border border-border/50 bg-background/20 px-3 py-2">
|
||||
<span className={cn("w-1.5 h-1.5 rounded-full shrink-0",
|
||||
ok === true ? "bg-emerald-500" : ok === false ? "bg-red-500" : "bg-muted-foreground/40")} />
|
||||
<span className="flex-1 text-xs text-foreground truncate">{label}{system && <span className="text-[9px] text-muted-foreground"> (root)</span>}</span>
|
||||
<button onClick={onRestart} disabled={busy} title="Neu starten" className="text-muted-foreground hover:text-primary transition-colors disabled:opacity-50">
|
||||
<RefreshCw className={cn("h-3.5 w-3.5", busy && "animate-spin")} />
|
||||
</button>
|
||||
<button onClick={onLogs} title="Logs ansehen" className="text-muted-foreground hover:text-primary transition-colors">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatBytes(bytes?: number) {
|
||||
if (bytes == null) return ""
|
||||
if (bytes > 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`
|
||||
@@ -36,6 +76,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
const [checkingUpdates, setCheckingUpdates] = useState(false)
|
||||
const [backupMsg, setBackupMsg] = useState("")
|
||||
const [backupRunning, setBackupRunning] = useState(false)
|
||||
const [services, setServices] = useState<ServicesResp | null>(null)
|
||||
const [backups, setBackups] = useState<{ snapshot: string; size_mb?: number }[]>([])
|
||||
const [hermesUpdating, setHermesUpdating] = useState(false)
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
@@ -115,6 +158,15 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
.catch((e) => console.error("Error loading jobs", e))
|
||||
}
|
||||
|
||||
function loadServices() {
|
||||
api<ServicesResp>("/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)
|
||||
@@ -146,9 +198,12 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
if (!open) return
|
||||
loadUpdates()
|
||||
loadJobs()
|
||||
loadServices()
|
||||
loadBackups()
|
||||
const timer = setInterval(() => {
|
||||
loadJobs()
|
||||
loadUpdates()
|
||||
loadServices()
|
||||
}, 3000)
|
||||
return () => clearInterval(timer)
|
||||
}, [open])
|
||||
@@ -179,6 +234,24 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
}
|
||||
}
|
||||
|
||||
function triggerHermesUpdate() {
|
||||
showConfirm(
|
||||
"Hermes-Agent aktualisieren",
|
||||
"Zieht die neuesten Änderungen aus git, installiert Abhängigkeiten neu und startet den Hermes-Gateway neu (vorher automatisches Backup). Fortschritt unter Hintergrund-Aufgaben.",
|
||||
async () => {
|
||||
setHermesUpdating(true)
|
||||
try {
|
||||
await api<{ job_id: string }>("/api/maintenance/hermes-update", { method: "POST" })
|
||||
loadJobs()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Hermes-Update fehlgeschlagen: ${e.message}`)
|
||||
} finally {
|
||||
setHermesUpdating(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function triggerCheckUpdates() {
|
||||
setCheckingUpdates(true)
|
||||
try {
|
||||
@@ -228,7 +301,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
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} Dateien)` : "Keine Änderungen zu sichern.")
|
||||
setBackupMsg(r.ok ? `Snapshot erzeugt: ${r.snapshot} (${r.files.length} Komponenten)` : "Backup fehlgeschlagen.")
|
||||
loadBackups()
|
||||
} catch (e: any) {
|
||||
setBackupMsg(`Fehler: ${e.message}`)
|
||||
} finally {
|
||||
@@ -341,120 +415,72 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{activeTab === "maintenance" && (
|
||||
<>
|
||||
{/* Quick Actions */}
|
||||
<div className="space-y-3">
|
||||
{/* Updates */}
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Wartungsaktionen</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{updates?.last_check && (
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
Zuletzt gesucht: {formatLastCheck(updates.last_check)}
|
||||
</span>
|
||||
)}
|
||||
<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>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Updates</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>
|
||||
{updates?.last_check && (
|
||||
<div className="text-[9px] text-muted-foreground -mt-1">Zuletzt gesucht: {formatLastCheck(updates.last_check)}</div>
|
||||
)}
|
||||
<div className="space-y-1.5">
|
||||
<UpdateRow icon={Shield} name="OS-Pakete (apt)" available={!!updates?.os} status={updates?.os ? `${updates.os} verfügbar` : "aktuell"} actionLabel="Aktualisieren" onAction={triggerOsUpdate} />
|
||||
<UpdateRow icon={Server} name="Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Aktualisieren" onAction={triggerEngineUpdate} />
|
||||
{(() => {
|
||||
const h = updates?.components?.find((c) => c.key === "hermes_agent")
|
||||
return (
|
||||
<UpdateRow icon={Bot} name="Hermes-Agent" available={h?.update === true} busy={hermesUpdating} status={h?.update === true ? `Update: ${h.latest}` : h?.reachable === false ? "offline" : "aktuell"} actionLabel="Aktualisieren" onAction={triggerHermesUpdate} />
|
||||
)
|
||||
})()}
|
||||
{updates?.model_list?.map((m) => (
|
||||
<UpdateRow key={m.role} icon={Box} name={`Modell · ${m.role}`} available={true} status={m.title} actionLabel="Upgrade" onAction={() => triggerModelUpgrade(m.repo, m.role)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dienste */}
|
||||
<div className="space-y-2.5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Dienste</h3>
|
||||
<div className="space-y-1.5">
|
||||
{SERVICES.map((s) => (
|
||||
<ServiceRow key={s.id} label={s.label} system={s.type === "system"} ok={services?.services.find((x) => x.name.toLowerCase().includes(s.reach))?.ok} busy={restartingServices[s.id]} onRestart={() => restartService(s.id)} onLogs={() => { setSelectedService(s.id); setActiveTab("logs") }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backup */}
|
||||
<div className="space-y-2.5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Backup</h3>
|
||||
<div className="rounded-lg border border-border/50 bg-background/20 px-3 py-2.5 flex items-center gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-foreground truncate">{backups[0] ? `Letztes: ${backups[0].snapshot}` : "Noch kein Backup"}</div>
|
||||
<div className="text-[10px] text-muted-foreground">{backups.length} Snapshots · Restore per CLI (restore.sh)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
onClick={triggerOsUpdate}
|
||||
className="flex flex-col items-start gap-1 p-4 rounded-xl border border-border/60 bg-background/20 hover:border-primary/50 transition-all text-left group"
|
||||
>
|
||||
<Shield className="h-5 w-5 text-cyan-400 mb-1 group-hover:scale-105 transition-transform" />
|
||||
<span className="text-xs font-semibold">OS Update (apt)</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{updates?.os ? `${updates.os} Updates verfügbar` : "Auf neuestem Stand"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={triggerEngineUpdate}
|
||||
className="flex flex-col items-start gap-1 p-4 rounded-xl border border-border/60 bg-background/20 hover:border-primary/50 transition-all text-left group"
|
||||
>
|
||||
<Server className="h-5 w-5 text-violet-400 mb-1 group-hover:scale-105 transition-transform" />
|
||||
<span className="text-xs font-semibold">Engine Update</span>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{updates?.engine ? "Update verfügbar" : "Auf neuestem Stand"}
|
||||
</span>
|
||||
<button onClick={doBackup} disabled={backupRunning} className="flex items-center gap-1.5 text-[11px] font-semibold rounded-lg px-2.5 py-1 bg-primary text-primary-foreground hover:opacity-90 transition-all cursor-pointer disabled:opacity-50 shrink-0">
|
||||
<Camera className={cn("h-3.5 w-3.5", backupRunning && "animate-pulse")} /> Snapshot
|
||||
</button>
|
||||
</div>
|
||||
{backupMsg && (
|
||||
<div className="text-[10px] font-mono text-primary bg-primary/5 p-2 rounded-lg border border-primary/20 break-all leading-normal">{backupMsg}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={triggerReboot}
|
||||
className="flex w-full items-center gap-3 p-3 rounded-xl border border-red-500/20 bg-red-500/5 hover:bg-red-500/10 text-red-400 transition-all text-left text-xs font-semibold"
|
||||
>
|
||||
{/* Gefahrenzone */}
|
||||
<div className="space-y-2.5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-red-400/80">Gefahrenzone</h3>
|
||||
<button onClick={triggerReboot} className="flex w-full items-center gap-3 p-3 rounded-lg border border-red-500/20 bg-red-500/5 hover:bg-red-500/10 text-red-400 transition-all text-left text-xs font-semibold">
|
||||
<Power className="h-4.5 w-4.5" />
|
||||
<div>
|
||||
<div>Host-System neu starten (Reboot)</div>
|
||||
<div className="text-[10px] text-red-400/80 font-normal">Startet das gesamte Betriebssystem des Homelabs neu</div>
|
||||
<div>Host-System neu starten</div>
|
||||
<div className="text-[10px] text-red-400/80 font-normal">Startet die ganze Box neu</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Backup / Snapshot */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">System-Backup & Snapshot</h3>
|
||||
<div className="p-4 rounded-xl border border-border/60 bg-background/20 space-y-3">
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">
|
||||
Voll-Backup: Gedächtnis (Mem0), Hermes-Config & Secrets, llama-swap-Config.
|
||||
Läuft auch täglich automatisch. Wiederherstellen per <code>deploy/restore.sh</code> (siehe docs/BACKUP.md).
|
||||
</p>
|
||||
<button
|
||||
onClick={doBackup}
|
||||
disabled={backupRunning}
|
||||
className="w-full h-9 flex items-center justify-center gap-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer disabled:opacity-50 shadow-md shadow-primary/10"
|
||||
>
|
||||
<Save className={cn("h-4 w-4", backupRunning && "animate-pulse")} /> Snapshot erstellen
|
||||
</button>
|
||||
{backupMsg && (
|
||||
<div className="text-[10px] font-mono text-primary bg-primary/5 p-2 rounded-lg border border-primary/20 break-all leading-normal">
|
||||
{backupMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modell Upgrades */}
|
||||
{updates?.model_list && updates.model_list.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Verfügbare Modell-Upgrades</h3>
|
||||
{updates?.last_check && (
|
||||
<span className="text-[9px] text-muted-foreground">
|
||||
Gesucht: {formatLastCheck(updates.last_check)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user