Update-all-Button: alle ausstehenden System-Updates in EINEM Job

Neuer POST /api/maintenance/update-all kettet die AUSSTEHENDEN Updates
sequenziell (Engine -> Router -> Hermes -> OS) in einem maintenance-Job.
Bewusst reine Wiederverwendung: jeder Teil ist exakt der Befehl des
Einzel-Updates inkl. dessen Backup/Postcheck/Selbst-Rollback; &&-Kette
stoppt beim ersten Fehler, Banner-Zeilen im Log zeigen den Schritt.
OS zuletzt (breitester Eingriff, braucht als einziges das Box-Passwort;
fehlt es, laufen die sudo-freien Teile trotzdem und OS wird uebersprungen).
Hermes-Befehlskette in _hermes_update_cmd() extrahiert (DRY).

UI (SystemDrawer/Updates): Button 'Alle aktualisieren (N)' neben der
Update-Suche, nur sichtbar wenn etwas aussteht, gesperrt waehrend ein
Wartungs-Job laeuft, mit Bestaetigungs-Dialog der die Kette benennt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-08 17:36:52 +02:00
parent 49807ace7b
commit f2d357c5d8
6 changed files with 284 additions and 180 deletions
+44 -5
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"
import { X, RefreshCw, Cpu, Server, Shield, Power, Camera, Bot, Box, Shuffle } from "lucide-react"
import { X, RefreshCw, Cpu, Server, Shield, Power, Camera, Bot, Box, Shuffle, DownloadCloud } 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"
@@ -245,6 +245,33 @@ export function SystemDrawer({ open, onClose, defaultTab = "maintenance" }: Syst
else if (kind === "hermes") doHermesUpdate()
}
// „Alle aktualisieren": kettet alle AUSSTEHENDEN Updates in EINEM Wartungs-Job
// (Engine → Router → Hermes → OS; das Backend nutzt exakt die Einzel-Update-Befehle
// inkl. deren Backup/Postcheck/Rollback — bei einem Fehler stoppt die Kette).
function triggerUpdateAll() {
const teile = [
updates?.engine ? "Engine" : null,
updates?.swap ? "Router" : null,
updates?.components?.some((c) => c.update === true) ? "Hermes-Agent" : null,
updates?.os ? `OS (${updates.os} Pakete)` : null,
].filter(Boolean)
showConfirm(
"Alle Updates einspielen?",
`Nacheinander in einem Job: ${teile.join(" → ")}. Jeder Schritt sichert und prüft sich selbst; schlägt einer fehl, stoppt der Rest. (OS braucht das Box-Passwort aus den Einstellungen — fehlt es, wird OS übersprungen.)`,
async () => {
try {
const res = await api<any>("/api/maintenance/update-all", { method: "POST" })
if (handledBusy(res) || handledSudo(res)) return
if (res?.status === "nothing") { showAlert("Nichts zu tun", "Es stehen keine Updates aus."); return }
loadJobs()
setActiveTab("maintenance")
} catch (e: any) {
showAlert("Fehler", `„Alle aktualisieren" konnte nicht starten: ${e.message}`)
}
},
)
}
async function triggerCheckUpdates() {
setCheckingUpdates(true)
try {
@@ -337,6 +364,10 @@ 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")
// Wie viele Update-ARTEN ausstehen (für den „Alle aktualisieren"-Button).
const pendingAll =
(updates ? (updates.os > 0 ? 1 : 0) + (updates.engine ? 1 : 0) + (updates.swap ? 1 : 0) : 0) +
(updates?.components?.filter((c) => c.update === true).length ?? 0)
return (
<>
@@ -415,10 +446,18 @@ 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 || !!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>
<div className="flex items-center gap-3">
{pendingAll > 0 && (
<button onClick={triggerUpdateAll} disabled={!!maintenanceJob}
className="flex items-center gap-1 rounded-md border border-primary/40 bg-primary/10 px-2 py-1 text-[10px] font-bold text-primary transition-colors hover:bg-primary/20 disabled:opacity-50">
<DownloadCloud className="h-3 w-3" /> Alle aktualisieren ({pendingAll})
</button>
)}
<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>
</div>
</div>
{updates?.last_check && (
<div className="text-[9px] text-muted-foreground -mt-1">Zuletzt gesucht: {formatLastCheck(updates.last_check)}</div>