updates: Urteil und Paketliste sichtbar, ehrliche Lade- und Fehlerzustaende, Rueckfragen, Auftraege aller Gruppen

- Update-Details zeigen jetzt das Urteil des Hirns ("Nichts zu tun" / "Du musst
  etwas tun" samt Text), die Zusammenfassung ist aufklappbar statt auf drei Zeilen
  gekappt, das Betriebssystem zeigt seine Paketliste (und was Ubuntu zurueckhaelt).
- Scheitert eine Pruefung (z. B. git fetch bei Hermes), steht "Konnte nicht pruefen: ..."
  statt "- -> neu"; waehrend des Ladens ein Platzhalter statt "..." und kein voreiliges "neu".
- Einzel-Update und Freigeben fragen vorher nach; der Ja-Knopf sperrt gegen Doppelklick.
- Neue Job-Karte (Fortschritt, Abbrechen mit Rueckfrage, letzte Meldung, Protokoll) fuer
  alle laufenden Auftraege, egal welche Gruppe. Updates lassen sich bewusst nicht abbrechen.
- Job-Typen an jobengine.py angeglichen ("canceled", group, started_at/finished_at):
  abgebrochene Auftraege erschienen bisher als "Laeuft".
- Fehler aus "Jetzt sichern" (Feld error) und "Es laeuft schon ein Update" (status busy)
  werden gemeldet; ist ein Auftrag fertig, lesen Versionen, Modelle und Sicherungen neu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 15:13:27 +02:00
co-authored by Claude Opus 5.5
parent c8718fcde0
commit 5ddc952ecd
8 changed files with 789 additions and 159 deletions
+304 -115
View File
@@ -1,5 +1,7 @@
import { useState } from "react"
import { ChevronRight } from "lucide-react"
import { cn } from "cn"
import { Auftraege } from "@/components/cockpit/Auftraege"
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
import { Panel } from "@/components/cockpit/Panel"
@@ -8,8 +10,8 @@ import {
type UpdateLauf, useFreigeben, useJetztSichern, useJobs, useSicherungen, useStart, useUpdateDetails,
useUpdateStarten, useUpdateVerlauf, useZurueckspielen,
} from "@/lib/abfragen"
import { versionen } from "@/lib/anzeige"
import type { Festgehalten, Job, Sicherung } from "@/lib/typen"
import { detailsHabenNeues, laeuftNoch, pruefFehlerText, versionen } from "@/lib/anzeige"
import type { Festgehalten, Sicherung, UpdateDetails } from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
const BAUSTEINE = [
@@ -19,46 +21,171 @@ const BAUSTEINE = [
{ id: "hermes", name: "Hermes" },
] as const
function BausteinZeile({ id, name, gelesen, hatUpdate, fest, onStart, onFreigeben, laeuftGerade }: {
/** Was ein einzelnes Update tut — steht in der Rückfrage, bevor es losgeht. */
const EINZEL_TEXT: Record<string, string> = {
os: "Die Ubuntu-Pakete werden eingespielt, danach prüft die Box, ob die Modelle noch antworten. Das kann einige Minuten dauern.",
engine: "Der Motor wird ersetzt, llama-swap startet dafür neu und die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.",
swap: "llama-swap wird ersetzt und startet neu, die Modelle sind kurz nicht erreichbar. Klappt die Prüfung danach nicht, holt die Box den alten Stand von selbst zurück.",
hermes: "Vorher wird gesichert. Dann wird Hermes aktualisiert und startet neu — Lucy ist dabei ein paar Minuten nicht erreichbar.",
}
/** Platzhalter, solange die Box noch nachsieht — statt „…“ oder eines voreiligen „neu“. */
function Skelett({ breit = "w-16" }: { breit?: string }) {
return (
<span className="inline-flex items-center">
<span aria-hidden className={cn("inline-block h-3.5 rounded bg-linie motion-safe:animate-pulse", breit)} />
<span className="sr-only">wird geprüft</span>
</span>
)
}
/** Die Zusammenfassung des Hirns: drei Zeilen sichtbar, der Rest auf Knopfdruck. */
function Zusammenfassung({ text }: { text: string }) {
const [offen, setOffen] = useState(false)
const lang = text.split("\n").length > 3 || text.length > 240
return (
<div className="flex flex-col items-start">
<p className={cn("m-0 whitespace-pre-line", lang && !offen && "line-clamp-3")}>{text}</p>
{lang && (
<button
type="button"
aria-expanded={offen}
onClick={() => setOffen(!offen)}
className="-mb-2 inline-flex h-11 items-center text-sm text-cyan hover:underline"
>
{offen ? "Weniger zeigen" : "Ganze Zusammenfassung zeigen"}
</button>
)}
</div>
)
}
/** Die Paketliste des Betriebssystems, aufklappbar — dazu, was Ubuntu noch zurückhält. */
function Paketliste({ d }: { d: UpdateDetails }) {
const pakete = d.packages ?? []
const zurueck = d.held_back ?? []
if (pakete.length === 0 && zurueck.length === 0) return null
return (
<details className="group">
<summary className="inline-flex h-11 cursor-pointer items-center gap-1 text-sm text-cyan hover:underline">
<ChevronRight aria-hidden className="size-4 transition-transform group-open:rotate-90" />
{pakete.length === 1 ? "Das Paket ansehen" : `Alle ${pakete.length} Pakete ansehen`}
</summary>
<ul className="ziffern m-0 flex max-h-64 list-none flex-col gap-1 overflow-auto rounded-md border border-linie bg-[#0b0d0f] p-2.5 text-xs">
{pakete.map((p) => (
<li key={p.name} className="break-all">
<span className="text-foreground">{p.name}</span>{" "}
<span className="text-text-3">{p.current} → {p.candidate}</span>
</li>
))}
</ul>
{zurueck.length > 0 && (
<p className="mt-2 text-xs text-text-3">
Dazu {zurueck.length === 1 ? "kommt ein Paket" : `kommen ${zurueck.length} Pakete`}, die Ubuntu noch
zurückhält ({zurueck.map((p) => p.name).join(", ")}). Die kommen von selbst, wenn Ubuntu sie freigibt.
</p>
)}
</details>
)
}
/** Das Urteil des Hirns aus den Versionshinweisen: Muss der Besitzer etwas tun? */
function Urteil({ d }: { d: UpdateDetails }) {
if (d.action_needed === false) return <Chip art="gruen">Nichts zu tun</Chip>
if (d.action_needed !== true) return null
return (
<span className="flex flex-col items-start gap-1.5">
<Chip art="bernstein">Du musst etwas tun</Chip>
{d.action_text && <span className="text-bernstein">{d.action_text}</span>}
</span>
)
}
function zusammenfassung(summary: string | undefined): string | null {
const s = summary?.trim()
if (!s) return null
// Das Backend meldet ein nicht antwortendes Hirn als „(Zusammenfassung nicht verfügbar: <Fehler>)“.
if (s.startsWith("(Zusammenfassung nicht verfügbar")) return "Die Zusammenfassung ist gerade nicht verfügbar."
return s
}
function BausteinZeile({ id, name, neuLautStart, fest, gesperrt, onStart, onFreigeben }: {
id: string
name: string
gelesen: boolean
hatUpdate: boolean
/** Was /api/start über diesen Baustein sagt („3 Pakete“, „neuer Build“), undefined = nichts Neues. */
neuLautStart: string | undefined
fest: Festgehalten | undefined
gesperrt: boolean
onStart: () => void
onFreigeben: () => void
laeuftGerade: boolean
}) {
const { data, isPending } = useUpdateDetails(id)
const v = versionen(id, data)
const neu = hatUpdate ? v.neu ?? "neu" : null
const was = !gelesen
? "Wird gerade geprüft …"
: !hatUpdate
? "Aktuell."
: data?.summary?.trim() || (id === "os" && data?.count ? "Paketliste unten in den Details." : isPending ? "Wird gelesen …" : "")
const neuText = neu ? <span className="text-cyan">{neu}</span> : <span className="text-text-3">–</span>
const details = useUpdateDetails(id)
const d = details.data
const laedt = details.isPending
const fehler = details.isError ? details.error.message : d?.error
const v = d ? versionen(id, d) : null
// Frische Details schlagen den gecachten Stand der Startseite; wissen sie nichts, gilt der Start.
const hatUpdate = detailsHabenNeues(id, d) ?? neuLautStart != null
const neu = hatUpdate ? v?.neu ?? neuLautStart ?? null : null
const summary = zusammenfassung(d?.summary)
const laeuftZelle = laedt ? <Skelett /> : <>{v?.laeuft ?? "–"}</>
const neuZelle = laedt ? (
<Skelett breit="w-20" />
) : neu ? (
<span className="text-cyan">{neu}</span>
) : fehler ? (
<span className="text-bernstein">nicht geprüft</span>
) : (
<span className="text-text-3">–</span>
)
let was
if (laedt) {
was = <span className="text-text-3">Wird geprüft …</span>
} else if (fehler && !hatUpdate) {
was = <span className="text-bernstein">{pruefFehlerText(fehler)}</span>
} else if (!hatUpdate) {
was = "Aktuell."
} else {
was = (
<span className="flex flex-col items-start gap-2">
{fehler && <span className="text-bernstein">{pruefFehlerText(fehler)}</span>}
{d && <Urteil d={d} />}
{id === "os" ? (
<>
<span>{d?.count ? `${d.count} ${d.count === 1 ? "Paket" : "Pakete"} aus Ubuntu.` : "Neue Pakete aus Ubuntu."}</span>
{d && <Paketliste d={d} />}
</>
) : summary ? (
<Zusammenfassung text={summary} />
) : null}
</span>
)
}
// Am PC eine Tabellenzeile, am Handy ein Block: Name und Versionen oben, darunter was sich
// ändert, darunter die Knöpfe — ohne seitliches Scrollen.
return (
<div
role="row"
aria-busy={laedt || undefined}
className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-x-4 gap-y-2 border-t border-linie py-3.5 md:grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] md:items-start"
>
<span role="rowheader" className="text-[15px] font-semibold">{name}</span>
<span role="cell" className="ziffern text-right text-sm text-text-2 md:hidden">
{v.laeuft}
{neu && <> → {neuText}</>}
{laeuftZelle}
{!laedt && (neu || fehler) && <> {neu ? "→" : "·"} {neuZelle}</>}
</span>
<span role="cell" className="ziffern hidden text-sm text-text-2 md:block">{v.laeuft}</span>
<span role="cell" className="ziffern hidden text-sm md:block">{neuText}</span>
<span role="cell" className="ziffern hidden text-sm text-text-2 md:block">{laeuftZelle}</span>
<span role="cell" className="ziffern hidden text-sm md:block">{neuZelle}</span>
<span role="cell" className="col-span-2 text-sm leading-relaxed text-text-2 md:col-span-1">
{fest && (
<span className="mb-1.5 block text-bernstein">
Festgehalten seit {fest.seit} auf {fest.version}: {fest.grund}. Der Sonntags-Lauf überspringt {name}.
</span>
)}
<span className="line-clamp-3 whitespace-pre-line">{was}</span>
{was}
</span>
{(fest || hatUpdate) && (
<span role="cell" className="col-span-2 flex flex-wrap gap-2 md:col-span-1 md:flex-col md:items-end">
@@ -68,7 +195,7 @@ function BausteinZeile({ id, name, gelesen, hatUpdate, fest, onStart, onFreigebe
</Button>
)}
{hatUpdate && (
<Button variant="outline" size="sm" onClick={onStart} disabled={laeuftGerade}>
<Button variant="outline" size="sm" onClick={onStart} disabled={gesperrt}>
Aktualisieren
</Button>
)}
@@ -89,17 +216,22 @@ const PUNKT: Record<UpdateLauf["stufe"], string> = {
function LaufEintrag({ lauf }: { lauf: UpdateLauf }) {
return (
<li className="flex flex-col gap-2 border-t border-linie py-3">
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 md:grid-cols-[112px_minmax(0,1fr)_auto]">
<span className="ziffern text-sm text-text-3">{flugplanZeit(lauf.start)}</span>
<span className="min-w-0 text-[15px]">{lauf.anlass}</span>
<Chip art={LAUF_ART[lauf.stufe]}>{lauf.titel}</Chip>
{/* Handy: Zeit und Ergebnis oben, der Anlass darunter über die volle Breite. */}
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 md:grid-cols-[112px_minmax(0,1fr)_auto]">
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(lauf.start)}</span>
<span className="col-span-2 row-start-2 min-w-0 text-[15px] break-words md:col-span-1 md:col-start-2 md:row-start-1">
{lauf.anlass}
</span>
<span className="col-start-2 row-start-1 justify-self-end md:col-start-3">
<Chip art={LAUF_ART[lauf.stufe]}>{lauf.titel}</Chip>
</span>
</div>
{lauf.stufe !== "grau" && lauf.zeilen.length > 0 && (
<ul className="m-0 flex list-none flex-col gap-1 p-0 md:pl-[124px]">
{lauf.zeilen.map((z) => (
<li key={`${z.baustein}-${z.text}`} className="flex items-baseline gap-2 text-sm leading-snug text-text-2">
<span aria-hidden className={cn("size-1.5 shrink-0 -translate-y-px rounded-full", PUNKT[z.stufe])} />
<span className="min-w-0">
<span className="min-w-0 break-words">
<span className="text-foreground">{z.baustein}</span> · {z.text}
</span>
</li>
@@ -110,28 +242,17 @@ function LaufEintrag({ lauf }: { lauf: UpdateLauf }) {
)
}
function JobKasten({ job }: { job: Job & { group?: string; log?: string[] } }) {
const art = job.state === "done" ? "gruen" : job.state === "failed" ? "rot" : "cyan"
const text = job.state === "done" ? "Fertig" : job.state === "failed" ? "Fehler" : job.state === "queued" ? "Wartet" : "Läuft"
return (
<div className="flex flex-col gap-2 rounded-xl border border-linie bg-erhaben/50 p-3">
<div className="flex items-center justify-between gap-3">
<span className="text-[15px] font-semibold">{job.label}</span>
<Chip art={art}>{text}</Chip>
</div>
{job.log && job.log.length > 0 && (
<pre className="ziffern max-h-40 overflow-auto rounded-md bg-[#0b0d0f] p-2 text-xs whitespace-pre-wrap text-text-2">
{job.log.slice(-8).join("\n")}
</pre>
)}
</div>
)
}
function sicherungsZeit(s: Sicherung) {
return s.mtime ? flugplanZeit(new Date(s.mtime * 1000).toISOString()) : s.snapshot
}
type Frage =
| null
| { art: "alle" }
| { art: "einzeln"; id: string; name: string }
| { art: "freigeben"; id: string; name: string; grund: string }
| { art: "zurueck"; datei: string; zeit: string }
export function UpdatesSeite() {
const start = useStart()
const jobs = useJobs()
@@ -141,12 +262,100 @@ export function UpdatesSeite() {
const freigeben = useFreigeben()
const sichern = useJetztSichern()
const zurueck = useZurueckspielen()
const [frage, setFrage] = useState<null | { art: "alle" } | { art: "zurueck"; datei: string; zeit: string }>(null)
const [frage, setFrage] = useState<Frage>(null)
const mitUpdate = new Set(start.data?.updates.bausteine.map((b) => b.id) ?? [])
const neuLautStart = new Map((start.data?.updates.bausteine ?? []).map((b) => [b.id, b.neu]))
const festgehalten = new Map((start.data?.updates.festgehalten ?? []).map((f) => [f.baustein, f]))
const wartungsJobs = (jobs.data?.jobs ?? []).filter((j) => (j as { group?: string }).group === "maintenance")
const laeuft = wartungsJobs.some((j) => j.state === "running" || j.state === "queued")
const alleJobs = jobs.data?.jobs ?? []
// Nur ein Update gleichzeitig (Riegel im Backend) — auch der Sonntags-Lauf zählt.
const updateLaeuft =
alleJobs.some((j) => j.group === "maintenance" && laeuftNoch(j)) || (start.data?.zustand.update_laeuft ?? false)
// Die Suche hat im Backend (noch) keine Gruppe; sobald sie "pruefen" heißt, sperrt der Knopf auch hier.
const suchtGerade = alleJobs.some((j) => j.group === "pruefen" && laeuftNoch(j))
const gesperrt = updateLaeuft || updateStarten.isPending
const zurueckMoeglich = sicherungen.data?.available ?? false
let tabelle
if (start.isPending) {
tabelle = <Zustandsfeld text="Updates werden gelesen …" />
} else if (start.isError && !start.data) {
tabelle = <Zustandsfeld fehler text={`Die Box antwortet nicht: ${start.error.message}`} />
} else {
tabelle = (
<div role="table" aria-label="Bausteine und ihre Updates" className="flex flex-col">
<div
role="row"
className="schild hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid"
>
<span role="columnheader">Baustein</span>
<span role="columnheader">Läuft</span>
<span role="columnheader">Neu</span>
<span role="columnheader">Was sich ändert</span>
<span role="columnheader" className="sr-only">Aktion</span>
</div>
{BAUSTEINE.map((b) => (
<BausteinZeile
key={b.id}
id={b.id}
name={b.name}
neuLautStart={neuLautStart.get(b.id)}
fest={festgehalten.get(b.id)}
gesperrt={gesperrt}
onStart={() => setFrage({ art: "einzeln", id: b.id, name: b.name })}
onFreigeben={() => {
const f = festgehalten.get(b.id)
if (f) setFrage({ art: "freigeben", id: b.id, name: b.name, grund: f.grund })
}}
/>
))}
</div>
)
}
let verlaufInhalt
if (verlauf.isPending) {
verlaufInhalt = <p className="text-[15px] text-text-2">Wird gelesen …</p>
} else if (verlauf.isError && !verlauf.data) {
verlaufInhalt = <p role="alert" className="text-[15px] text-rot-text">Der Verlauf lässt sich gerade nicht lesen: {verlauf.error.message}</p>
} else if (!verlauf.data?.laeufe.length) {
verlaufInhalt = <p className="text-[15px] text-text-2">Noch keine Update-Läufe aufgezeichnet.</p>
} else {
verlaufInhalt = (
<ol className="m-0 flex list-none flex-col p-0">
{verlauf.data.laeufe.map((l) => <LaufEintrag key={l.start} lauf={l} />)}
</ol>
)
}
let sicherungsInhalt
if (sicherungen.isPending) {
sicherungsInhalt = <p className="text-[15px] text-text-2">Wird gelesen …</p>
} else if (sicherungen.isError && !sicherungen.data) {
sicherungsInhalt = <p role="alert" className="text-[15px] text-rot-text">Die Sicherungen lassen sich gerade nicht lesen: {sicherungen.error.message}</p>
} else if (!sicherungen.data?.backups.length) {
sicherungsInhalt = <p className="text-[15px] text-text-2">Noch keine Sicherung vorhanden.</p>
} else {
sicherungsInhalt = (
<ul className="m-0 flex list-none flex-col p-0">
{sicherungen.data.backups.slice(0, 5).map((s) => (
<li key={s.file} className="flex items-center justify-between gap-3 border-t border-linie py-2.5">
<span className="ziffern text-sm">{sicherungsZeit(s)}</span>
<span className="flex items-center gap-3">
<span className="ziffern text-xs text-text-3">{s.size_mb.toLocaleString("de-DE")} MB</span>
<Button
variant="ghost"
size="sm"
disabled={!zurueckMoeglich || zurueck.isPending}
onClick={() => setFrage({ art: "zurueck", datei: s.file, zeit: sicherungsZeit(s) })}
>
Zurückspielen
</Button>
</span>
</li>
))}
</ul>
)
}
return (
<>
@@ -154,10 +363,14 @@ export function UpdatesSeite() {
titel="Updates"
rechts={
<div className="flex flex-wrap gap-2.5">
<Button variant="outline" onClick={() => updateStarten.mutate("pruefen")} disabled={updateStarten.isPending}>
<Button
variant="outline"
onClick={() => updateStarten.mutate("pruefen")}
disabled={updateStarten.isPending || suchtGerade}
>
Nach Neuem suchen
</Button>
<Button onClick={() => setFrage({ art: "alle" })} disabled={laeuft || mitUpdate.size === 0}>
<Button onClick={() => setFrage({ art: "alle" })} disabled={gesperrt || neuLautStart.size === 0}>
Alles jetzt aktualisieren
</Button>
</div>
@@ -166,103 +379,79 @@ export function UpdatesSeite() {
<p className="-mt-2 text-[15px] text-text-2">
Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.
</p>
{start.isPending ? (
<Zustandsfeld text="Updates werden gelesen …" />
) : (
<div role="table" aria-label="Bausteine und ihre Updates" className="flex flex-col">
<div
role="row"
className="schild hidden grid-cols-[170px_100px_170px_minmax(0,1fr)_auto] gap-x-4 pb-2 text-xs font-semibold text-text-3 md:grid"
>
<span role="columnheader">Baustein</span>
<span role="columnheader">Läuft</span>
<span role="columnheader">Neu</span>
<span role="columnheader">Was sich ändert</span>
<span role="columnheader" className="sr-only">Aktion</span>
</div>
{BAUSTEINE.map((b) => (
<BausteinZeile
key={b.id}
id={b.id}
name={b.name}
gelesen={start.data?.updates.gelesen ?? false}
hatUpdate={mitUpdate.has(b.id)}
fest={festgehalten.get(b.id)}
laeuftGerade={laeuft}
onStart={() => updateStarten.mutate(b.id)}
onFreigeben={() => freigeben.mutate(b.id)}
/>
))}
</div>
)}
{wartungsJobs.length > 0 && (
<div className="flex flex-col gap-2.5">
<h3 className="schild text-sm text-text-3">Laufende und letzte Aufträge</h3>
{wartungsJobs.slice(-3).reverse().map((j) => <JobKasten key={j.id} job={j} />)}
</div>
{updateLaeuft && (
<p role="status" className="text-[15px] text-cyan">
Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.
</p>
)}
{tabelle}
<Auftraege />
</Panel>
<div className="grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<Panel titel="Verlauf">
{verlauf.data?.laeufe.length ? (
<ol className="m-0 flex list-none flex-col p-0">
{verlauf.data.laeufe.map((l) => <LaufEintrag key={l.start} lauf={l} />)}
</ol>
) : (
<p className="text-[15px] text-text-2">
{verlauf.isPending ? "Wird gelesen …" : "Noch keine Update-Läufe aufgezeichnet."}
</p>
)}
</Panel>
<Panel titel="Verlauf">{verlaufInhalt}</Panel>
<Panel
titel="Sicherungen"
rechts={
<Button variant="outline" size="sm" onClick={() => sichern.mutate(undefined)} disabled={sichern.isPending}>
Jetzt sichern
{sichern.isPending ? "Sichert …" : "Jetzt sichern"}
</Button>
}
>
<p className="-mt-2 text-[15px] text-text-2">Täglich um 03:35, mit Kopie auf einem zweiten Gerät.</p>
<ul className="m-0 flex list-none flex-col p-0">
{(sicherungen.data?.backups ?? []).slice(0, 5).map((s) => (
<li key={s.file} className="flex items-center justify-between gap-3 border-t border-linie py-2.5">
<span className="ziffern text-sm">{sicherungsZeit(s)}</span>
<span className="flex items-center gap-3">
<span className="ziffern text-xs text-text-3">{s.size_mb.toLocaleString("de-DE")} MB</span>
<Button
variant="ghost"
size="sm"
disabled={!sicherungen.data?.available}
onClick={() => setFrage({ art: "zurueck", datei: s.file, zeit: sicherungsZeit(s) })}
>
Zurückspielen
</Button>
</span>
</li>
))}
</ul>
{sicherungen.data && !zurueckMoeglich && (
<p className="text-sm text-text-3">Zurückspielen geht nur auf der Box selbst.</p>
)}
{sicherungsInhalt}
</Panel>
</div>
<Bestaetigen
offen={frage?.art === "alle"}
titel="Alles jetzt aktualisieren?"
text="Betriebssystem, Motor, llama-swap und Hermes werden nacheinander aktualisiert. Vorher wird gesichert, danach geprüft. Lucy ist dabei ein paar Minuten nicht erreichbar."
text="Motor, llama-swap, Hermes und Betriebssystem werden nacheinander aktualisiert, soweit es Neues gibt. Vorher wird gesichert, danach geprüft. Lucy ist dabei ein paar Minuten nicht erreichbar."
knopf="Aktualisieren"
gesperrt={gesperrt}
onNein={() => setFrage(null)}
onJa={() => {
updateStarten.mutate("alle")
setFrage(null)
}}
/>
<Bestaetigen
offen={frage?.art === "einzeln"}
titel={frage?.art === "einzeln" ? `${frage.name} aktualisieren?` : ""}
text={frage?.art === "einzeln" ? EINZEL_TEXT[frage.id] ?? "" : ""}
knopf="Aktualisieren"
gesperrt={gesperrt}
onNein={() => setFrage(null)}
onJa={() => {
if (frage?.art === "einzeln") updateStarten.mutate(frage.id)
setFrage(null)
}}
/>
<Bestaetigen
offen={frage?.art === "freigeben"}
titel={frage?.art === "freigeben" ? `${frage.name} freigeben?` : ""}
text={frage?.art === "freigeben"
? `Festgehalten wurde ${frage.name}, weil: ${frage.grund}. Nach dem Freigeben versucht der nächste Sonntags-Lauf das Update wieder.`
: ""}
knopf="Freigeben"
gesperrt={freigeben.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage?.art === "freigeben") freigeben.mutate(frage.id)
setFrage(null)
}}
/>
<Bestaetigen
offen={frage?.art === "zurueck"}
gefahr
titel="Sicherung zurückspielen?"
text={`Die Einstellungen von Hermes und llama-swap werden auf den Stand von ${frage?.art === "zurueck" ? frage.zeit : ""} gesetzt. Vorher wird der jetzige Stand gesichert. Die Dienste starten danach neu.`}
knopf="Zurückspielen"
gesperrt={zurueck.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage?.art === "zurueck") zurueck.mutate(frage.datei)