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