feat(2.0): W8 — Wartung (OS/Engine/Modell-Updates, Restart, Reboot, Logs)

services/maintenance.py + routers/maintenance.py: updates-Badge (apt/Engine-
Release/dyn. Modell-Upgrades via discover), os-update + engine-update als
jobengine-Jobs, system-/user-aware Restart (llama-swap via sudo -n NOPASSWD),
reboot, logs (journalctl). Frontend: Wartungs-Block in SystemView mit Badge,
Buttons + Modell-Upgrade-Vorschlaegen. Passwortfrei (sudo -n).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 17:43:53 +02:00
parent 2ea3d01b58
commit 1f391644ca
9 changed files with 455 additions and 190 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DP8A83Cd.js"></script>
<script type="module" crossorigin src="/assets/index-S7P9XrdI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-IpcnHTkp.css">
</head>
<body>
+7
View File
@@ -102,6 +102,13 @@ export interface ServicesResp {
links: { engine_ui: string; gateway: string; hermes_webui: string }
}
export interface UpdatesResp {
os: number
engine: number
models: number
model_list: { role: string; title: string; repo: string }[]
}
export interface ConnectTool {
label: string
lang: string
+82 -2
View File
@@ -1,12 +1,90 @@
import { useEffect, useState } from "react"
import { ExternalLink, Save } from "lucide-react"
import { api, type ServicesResp, type SystemStatus } from "@/lib/api"
import { ExternalLink, RefreshCw, Save } from "lucide-react"
import { api, type ServicesResp, type SystemStatus, type UpdatesResp } from "@/lib/api"
import { cn } from "@/lib/utils"
function gb(b: number) {
return (b / 1024 ** 3).toFixed(1)
}
function Maintenance() {
const [u, setU] = useState<UpdatesResp | null>(null)
const [msg, setMsg] = useState("")
function load() {
api<UpdatesResp>("/api/maintenance/updates").then(setU).catch(() => {})
}
useEffect(load, [])
async function post(path: string, label: string) {
setMsg(`${label}`)
try {
const r = await api<{ job_id?: string; ok?: boolean; err?: string }>(path, { method: "POST" })
setMsg(r.job_id ? `${label} gestartet (Job ${r.job_id})` : r.ok ? `${label}` : `${label}: ${r.err || "?"}`)
} catch (e) {
setMsg(`${label}: ${e}`)
}
}
async function restart(service: string) {
setMsg(`Restart ${service}`)
try {
const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", {
method: "POST", body: JSON.stringify({ service }),
})
setMsg(r.ok ? `Restart ${service}` : `Restart ${service}: ${r.err || "?"}`)
} catch (e) {
setMsg(`Restart ${service}: ${e}`)
}
}
async function upgrade(repo: string, role: string) {
setMsg(`Lade ${repo}`)
try {
await api("/api/models/install", { method: "POST", body: JSON.stringify({ repo, role, quant: "Q4_K_M", jinja: true }) })
setMsg(`Upgrade ${repo} gestartet`)
} catch (e) {
setMsg(`Fehler: ${e}`)
}
}
return (
<div className="rounded-xl border border-border bg-card p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-sm font-medium">Wartung &amp; Updates</span>
{u && (
<span className="flex gap-2 text-xs">
<span className={cn("rounded px-1.5 py-0.5", u.os ? "bg-amber-500/15 text-amber-500" : "bg-muted text-muted-foreground")}>OS: {u.os}</span>
<span className={cn("rounded px-1.5 py-0.5", u.engine ? "bg-amber-500/15 text-amber-500" : "bg-muted text-muted-foreground")}>Engine: {u.engine ? "neu" : "aktuell"}</span>
<span className={cn("rounded px-1.5 py-0.5", u.models ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground")}>Modelle: {u.models}</span>
</span>
)}
</div>
<div className="flex flex-wrap gap-2 text-sm">
<button onClick={() => post("/api/maintenance/os-update", "OS-Update")} className="rounded-md border border-border px-2.5 py-1.5 hover:bg-accent">OS aktualisieren</button>
<button onClick={() => post("/api/maintenance/engine-update", "Engine-Update")} className="rounded-md border border-border px-2.5 py-1.5 hover:bg-accent">Engine aktualisieren</button>
<button onClick={() => restart("llama-swap")} className="flex items-center gap-1 rounded-md border border-border px-2.5 py-1.5 hover:bg-accent"><RefreshCw className="h-3.5 w-3.5" /> Engine neu starten</button>
<button onClick={() => { if (confirm("Box wirklich neu starten?")) post("/api/maintenance/reboot", "Reboot") }} className="rounded-md border border-border px-2.5 py-1.5 text-amber-500 hover:bg-accent">Reboot</button>
</div>
{u && u.model_list.length > 0 && (
<div className="mt-3 space-y-1">
<div className="text-xs text-muted-foreground">Modell-Upgrades verfügbar:</div>
{u.model_list.map((m) => (
<div key={m.repo} className="flex items-center justify-between text-xs">
<span className="truncate"><span className="text-primary">{m.role}</span> · {m.repo}</span>
<button onClick={() => upgrade(m.repo, m.role)} className="rounded-md border border-border px-2 py-0.5 hover:bg-accent">Upgrade</button>
</div>
))}
</div>
)}
{msg && <div className="mt-2 text-xs text-muted-foreground">{msg}</div>}
<div className="mt-2 text-[11px] text-muted-foreground">
OS-Update/Reboot brauchen einmalig erweiterte NOPASSWD-sudoers (siehe BEDIENUNG/CUTOVER).
</div>
</div>
)
}
function Bar({ label, percent, detail }: { label: string; percent: number; detail?: string }) {
return (
<div className="rounded-lg border border-border bg-card p-4">
@@ -126,6 +204,8 @@ export function SystemView() {
</button>
{backupMsg && <span className="text-xs text-muted-foreground">{backupMsg}</span>}
</div>
<Maintenance />
</div>
)
}