Feat: Wartungs-Riegel — nur EIN System-Update gleichzeitig
Bisher kein Schutz: Doppelklick/zwei Tabs konnten zwei update-engine.sh parallel
starten → racende .bak-Sicherung + parallele llama-swap-Restarts + sich gegenseitig
als kaputt sehende Postchecks.
Backend: jobengine.start_job bekommt group-Tag + active_in_group(); os/engine/swap/
hermes-update sind group="maintenance" und lehnen einen Start ab, solange eines laeuft
({ok:false, status:"busy", running:<label>}). Schuetzt auch gegen parallele Sessions.
Frontend: laeuft ein Wartungs-Job, zeigt der Drawer ein Banner "Update laeuft: <label>"
und sperrt "Jetzt aktualisieren" + "Nach Updates suchen". Logs/Job-Fortschritt/Dienste
bleiben voll nutzbar (Dashboard nicht hart gesperrt). Busy-Antwort wird als Hinweis gezeigt.
Modell-Upgrades bleiben erlaubt (parallel unkritisch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -217,9 +217,20 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
loadServiceLogs(selectedService)
|
||||
}, [open, activeTab, selectedService])
|
||||
|
||||
// Backend-Wartungsriegel: lehnt ein zweites Update ab, solange eines läuft.
|
||||
function handledBusy(res: any): boolean {
|
||||
if (res?.status === "busy") {
|
||||
showAlert("Update läuft bereits", `Es läuft gerade „${res.running}". Bitte warte, bis es fertig ist.`)
|
||||
loadJobs()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function triggerOsUpdate() {
|
||||
try {
|
||||
await api<{ job_id: string }>("/api/maintenance/os-update", { method: "POST" })
|
||||
const res = await api<any>("/api/maintenance/os-update", { method: "POST" })
|
||||
if (handledBusy(res)) return
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
@@ -229,7 +240,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
|
||||
async function triggerEngineUpdate() {
|
||||
try {
|
||||
await api<{ job_id: string }>("/api/maintenance/engine-update", { method: "POST" })
|
||||
const res = await api<any>("/api/maintenance/engine-update", { method: "POST" })
|
||||
if (handledBusy(res)) return
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
@@ -239,7 +251,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
|
||||
async function triggerSwapUpdate() {
|
||||
try {
|
||||
await api<{ job_id: string }>("/api/maintenance/swap-update", { method: "POST" })
|
||||
const res = await api<any>("/api/maintenance/swap-update", { method: "POST" })
|
||||
if (handledBusy(res)) return
|
||||
loadJobs()
|
||||
setActiveTab("maintenance")
|
||||
} catch (e: any) {
|
||||
@@ -250,7 +263,8 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
async function doHermesUpdate() {
|
||||
setHermesUpdating(true)
|
||||
try {
|
||||
await api<{ job_id: string }>("/api/maintenance/hermes-update", { method: "POST" })
|
||||
const res = await api<any>("/api/maintenance/hermes-update", { method: "POST" })
|
||||
if (handledBusy(res)) return
|
||||
loadJobs()
|
||||
} catch (e: any) {
|
||||
showAlert("Fehler", `Hermes-Update fehlgeschlagen: ${e.message}`)
|
||||
@@ -370,6 +384,9 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
}
|
||||
}
|
||||
|
||||
// Aktiver Wartungs-Job (System-Update) → UI-Aktionen sperren, Dashboard bleibt sichtbar.
|
||||
const maintenanceJob = jobs.find(j => (j.state === "running" || j.state === "queued") && j.group === "maintenance")
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
@@ -447,7 +464,7 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
<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">Updates</h3>
|
||||
<button onClick={triggerCheckUpdates} disabled={checkingUpdates}
|
||||
<button onClick={triggerCheckUpdates} disabled={checkingUpdates || !!maintenanceJob}
|
||||
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>
|
||||
@@ -455,6 +472,12 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
{updates?.last_check && (
|
||||
<div className="text-[9px] text-muted-foreground -mt-1">Zuletzt gesucht: {formatLastCheck(updates.last_check)}</div>
|
||||
)}
|
||||
{maintenanceJob && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px] text-amber-300">
|
||||
<RefreshCw className="h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
<span>Update läuft: <span className="font-semibold">{maintenanceJob.label}</span> — bitte warten. Weitere Updates sind solange gesperrt.</span>
|
||||
</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="Anzeigen" onAction={() => openUpdateDetails("os")} />
|
||||
<UpdateRow icon={Server} iconClass="text-violet-400" name="Inferenz-Engine (llama.cpp)" available={!!updates?.engine} status={updates?.engine ? "Update verfügbar" : "aktuell"} actionLabel="Anzeigen" onAction={() => openUpdateDetails("engine")} />
|
||||
@@ -876,9 +899,10 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
|
||||
<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}
|
||||
<button onClick={applyFromDetail} disabled={detail.loading || nothing || !!maintenanceJob}
|
||||
title={maintenanceJob ? `Update läuft bereits: ${maintenanceJob.label}` : undefined}
|
||||
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
|
||||
{maintenanceJob ? "Update läuft…" : "Jetzt aktualisieren"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user