Fix: Engine-Update-Badge bleibt nicht mehr haengen + Update-Detail-Fenster
- engine_update_job leert jetzt _engine_cache nach Abschluss (on_done), sonst zeigte das Dashboard bis zu 1h "Update verfuegbar" trotz erfolgter Aktualisierung (1h-Cache wurde nie invalidiert wie bei den anderen Jobs). - check_updates_job leert zusaetzlich _comp_cache, damit "Nach Updates suchen" auch den Hermes-Status frisch prueft. - Neu: GET /api/maintenance/update-details (os|engine|hermes) liefert, was genau aktualisiert wird (apt-Paketliste, Engine Build X->Y + Release-Notes, Hermes-Commits HEAD..origin/branch). - Frontend: "Aktualisieren"-Buttons -> "Anzeigen"; oeffnen ein Detail-Fenster mit den konkreten Aenderungen, erst "Jetzt aktualisieren" startet das Update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
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 { X, RefreshCw, Terminal, Cpu, Server, Shield, Power, Eye, EyeOff, Key, AlertTriangle, Camera, Bot, Box, FileText, Package, GitCommit, ExternalLink, ArrowRight } from "lucide-react"
|
||||
import { api, type Job, type UpdatesResp, type ServicesResp, type UpdateDetails } from "@/lib/api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CustomDialog } from "./CustomDialog"
|
||||
|
||||
@@ -80,6 +80,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
const [backups, setBackups] = useState<{ snapshot: string; size_mb?: number }[]>([])
|
||||
const [hermesUpdating, setHermesUpdating] = useState(false)
|
||||
|
||||
// Update-Detail-Fenster: zeigt VOR dem Anwenden, was genau aktualisiert wird.
|
||||
const [detail, setDetail] = useState<{ kind: "os" | "engine" | "hermes"; loading: boolean; data: UpdateDetails | null } | null>(null)
|
||||
|
||||
// Custom Dialog State
|
||||
const [dialog, setDialog] = useState<{
|
||||
type: "alert" | "confirm"
|
||||
@@ -234,22 +237,36 @@ 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 doHermesUpdate() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Öffnet das Detail-Fenster und lädt, was genau aktualisiert würde.
|
||||
async function openUpdateDetails(kind: "os" | "engine" | "hermes") {
|
||||
setDetail({ kind, loading: true, data: null })
|
||||
try {
|
||||
const d = await api<UpdateDetails>(`/api/maintenance/update-details?kind=${kind}`)
|
||||
setDetail({ kind, loading: false, data: d })
|
||||
} catch (e: any) {
|
||||
setDetail({ kind, loading: false, data: { kind, error: e.message } })
|
||||
}
|
||||
}
|
||||
|
||||
// Bestätigung aus dem Detail-Fenster → startet das passende Update.
|
||||
function applyFromDetail() {
|
||||
const kind = detail?.kind
|
||||
setDetail(null)
|
||||
if (kind === "os") triggerOsUpdate()
|
||||
else if (kind === "engine") triggerEngineUpdate()
|
||||
else if (kind === "hermes") doHermesUpdate()
|
||||
}
|
||||
|
||||
async function triggerCheckUpdates() {
|
||||
@@ -428,12 +445,12 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
<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} iconClass="text-cyan-400" name="OS-Pakete (apt)" available={!!updates?.os} status={updates?.os ? `${updates.os} verfügbar` : "aktuell"} actionLabel="Aktualisieren" onAction={triggerOsUpdate} />
|
||||
<UpdateRow icon={Server} iconClass="text-violet-400" name="Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Aktualisieren" onAction={triggerEngineUpdate} />
|
||||
<UpdateRow icon={Shield} iconClass="text-cyan-400" name="OS-Pakete (apt)" available={!!updates?.os} status={updates?.os ? `${updates.os} verfügbar` : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("os")} />
|
||||
<UpdateRow icon={Server} iconClass="text-violet-400" name="Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("engine")} />
|
||||
{(() => {
|
||||
const h = updates?.components?.find((c) => c.key === "hermes_agent")
|
||||
return (
|
||||
<UpdateRow icon={Bot} iconClass="text-amber-400" 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} />
|
||||
<UpdateRow icon={Bot} iconClass="text-amber-400" name="Hermes-Agent" available={h?.update === true} busy={hermesUpdating} status={h?.update === true ? `Update: ${h.latest}` : h?.reachable === false ? "offline" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("hermes")} />
|
||||
)
|
||||
})()}
|
||||
{updates?.model_list?.map((m) => (
|
||||
@@ -743,6 +760,119 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detail && (() => {
|
||||
const d = detail.data
|
||||
const meta = {
|
||||
os: { icon: Shield, cls: "text-cyan-400", title: "OS-Pakete (apt)" },
|
||||
engine: { icon: Server, cls: "text-violet-400", title: "Engine (llama.cpp)" },
|
||||
hermes: { icon: Bot, cls: "text-amber-400", title: "Hermes-Agent" },
|
||||
}[detail.kind]
|
||||
const Icon = meta.icon
|
||||
const nothing = !d ? true
|
||||
: detail.kind === "os" ? (d.count ?? 0) === 0
|
||||
: detail.kind === "hermes" ? (d.behind ?? 0) === 0
|
||||
: (d.installed_build != null && d.latest_build != null && d.latest_build <= d.installed_build)
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={() => setDetail(null)} />
|
||||
<div className="relative w-full max-w-lg max-h-[80vh] flex flex-col rounded-2xl border border-border/60 bg-card shadow-2xl font-sans text-foreground">
|
||||
{/* Header */}
|
||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-border/40 px-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={cn("h-4.5 w-4.5", meta.cls)} />
|
||||
<h3 className="text-sm font-semibold">{meta.title}</h3>
|
||||
</div>
|
||||
<button onClick={() => setDetail(null)} className="flex h-7 w-7 items-center justify-center rounded-lg border border-border/40 text-muted-foreground hover:bg-accent transition-colors">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="flex-1 overflow-y-auto p-5 text-xs space-y-3 scrollbar-thin">
|
||||
{detail.loading ? (
|
||||
<div className="flex h-24 items-center justify-center gap-2 text-muted-foreground">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" /> Details werden geladen…
|
||||
</div>
|
||||
) : d?.error ? (
|
||||
<div className="rounded-lg border border-red-500/30 bg-red-500/5 p-3 text-red-400">{d.error}</div>
|
||||
) : detail.kind === "os" ? (
|
||||
(d?.count ?? 0) === 0 ? (
|
||||
<div className="text-muted-foreground">Keine Pakete zu aktualisieren — System ist aktuell.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-muted-foreground">{d!.count} Paket(e) werden aktualisiert:</div>
|
||||
<div className="space-y-1">
|
||||
{d!.packages!.map((p) => (
|
||||
<div key={p.name} className="flex items-center justify-between gap-2 rounded-md border border-border/40 bg-background/30 px-2.5 py-1.5">
|
||||
<span className="flex items-center gap-1.5 font-mono text-[11px] truncate">
|
||||
<Package className="h-3 w-3 text-cyan-400 shrink-0" />{p.name}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 font-mono text-[10px] text-muted-foreground shrink-0">
|
||||
<span>{p.current}</span><ArrowRight className="h-3 w-3" /><span className="text-emerald-400">{p.candidate}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : detail.kind === "engine" ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2 font-mono text-[11px]">
|
||||
<span className="rounded-md border border-border/40 bg-background/30 px-2 py-1">Build {d?.installed_build ?? "?"}</span>
|
||||
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="rounded-md border border-emerald-500/30 bg-emerald-500/5 px-2 py-1 text-emerald-400">Build {d?.latest_build ?? "?"}</span>
|
||||
</div>
|
||||
{(d?.name || d?.latest_tag) && (
|
||||
<div className="text-muted-foreground">Release: <span className="text-foreground">{d?.name}</span>{d?.latest_tag ? ` (${d.latest_tag})` : ""}</div>
|
||||
)}
|
||||
{d?.url && (
|
||||
<a href={d.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-primary hover:underline">
|
||||
Release-Notes auf GitHub <ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
)}
|
||||
{d?.body && (
|
||||
<pre className="whitespace-pre-wrap rounded-lg border border-border/40 bg-background/30 p-3 text-[10px] leading-relaxed text-muted-foreground max-h-60 overflow-y-auto scrollbar-thin">{d.body}</pre>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// hermes
|
||||
(d?.commits?.length ?? 0) === 0 ? (
|
||||
<div className="text-muted-foreground">Keine neuen Commits — Hermes-Agent ist bereits aktuell.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-muted-foreground">{d!.behind} neue Commit(s) auf <span className="font-mono text-foreground">origin/{d!.branch}</span>:</div>
|
||||
<div className="space-y-1">
|
||||
{d!.commits!.map((c) => (
|
||||
<div key={c.hash} className="flex items-start gap-2 rounded-md border border-border/40 bg-background/30 px-2.5 py-1.5">
|
||||
<GitCommit className="h-3.5 w-3.5 text-primary shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] truncate">{c.subject}</div>
|
||||
<div className="font-mono text-[9px] text-muted-foreground">{c.hash} · {c.when}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground leading-normal">Vor dem Update wird automatisch ein Backup erstellt; danach startet der Hermes-Gateway neu.</p>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex gap-3 border-t border-border/40 p-4 shrink-0">
|
||||
<button onClick={() => setDetail(null)} className="h-9 flex-1 rounded-lg border border-border/60 bg-background/20 text-xs font-semibold text-muted-foreground hover:bg-accent transition-colors cursor-pointer">
|
||||
Schließen
|
||||
</button>
|
||||
<button onClick={applyFromDetail} disabled={detail.loading || nothing}
|
||||
className="h-9 flex-1 rounded-lg bg-primary text-xs font-semibold text-primary-foreground hover:opacity-90 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default">
|
||||
Jetzt aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{dialog && (
|
||||
<CustomDialog
|
||||
type={dialog.type}
|
||||
|
||||
Reference in New Issue
Block a user