Befunde aus der Sichtpruefung (Attrappe des Backends, 320/768/1024/1440 px):
- Zaehlwerk: Das "H" ragte bei 320 px 30 px ueber sein Feld. Ziffern skalieren jetzt
mit der Feldbreite (Container-Einheiten), am PC bleibt es bei 38 x 56 px.
- Flugplan und Update-Verlauf: Der Text bekam schmal nur ~50 px und lief in den Zustand.
Schmales Feld: Zeit und Zustand oben, Text darunter (nach Feldbreite, nicht Bildschirm).
- Update-Zeilen am Handy: Name und Version stiessen aneinander ("BetriebssystemUbuntu"),
die Version steht jetzt darunter; scheitert die Pruefung, heisst sie "unbekannt" statt "-".
- Schubladen (Modell-Suche, Dienste, Einstellungen, Protokoll) waren am Handy nur 75 %
und am PC hoechstens 384 px breit - die shadcn-Grundklassen schlugen die eigenen.
- Doppelklick: Die Sperre beim Installieren haengt jetzt an einem Ref und greift sofort
(vorher starteten zwei Klicks im selben Takt zwei Downloads); die Ja-Knoepfe von
"Alles aktualisieren" und "Auftrag abbrechen" ignorieren den zweiten Klick.
- Balken eines abgebrochenen Downloads grau statt cyan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
468 lines
19 KiB
TypeScript
468 lines
19 KiB
TypeScript
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"
|
||
import { Button } from "@/components/ui/button"
|
||
import {
|
||
type UpdateLauf, useFreigeben, useJetztSichern, useJobs, useSicherungen, useStart, useUpdateDetails,
|
||
useUpdateStarten, useUpdateVerlauf, useZurueckspielen,
|
||
} from "@/lib/abfragen"
|
||
import { laeuftNoch } from "@/lib/anzeige"
|
||
import { detailsHabenNeues, pruefFehlerText, versionen } from "@/lib/wartung"
|
||
import type { Festgehalten, Sicherung, UpdateDetails } from "@/lib/typen"
|
||
import { flugplanZeit } from "@/lib/zeit"
|
||
|
||
const BAUSTEINE = [
|
||
{ id: "os", name: "Betriebssystem" },
|
||
{ id: "engine", name: "Motor (llama.cpp)" },
|
||
{ id: "swap", name: "llama-swap" },
|
||
{ id: "hermes", name: "Hermes" },
|
||
] as const
|
||
|
||
/** 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
|
||
/** 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
|
||
}) {
|
||
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)
|
||
|
||
// Scheitert die Prüfung, kennt das Backend oft nicht einmal die laufende Version (Hermes: git fetch).
|
||
const laeuftText = v && v.laeuft !== "–" ? v.laeuft : fehler ? "unbekannt" : "–"
|
||
const laeuftZelle = laedt ? <Skelett /> : <>{laeuftText}</>
|
||
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 = (
|
||
<div 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}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Am PC eine Tabellenzeile, am Handy ein Block: Name, darunter die Versionen, darunter was sich
|
||
// ändert, darunter die Knöpfe — ohne seitliches Scrollen und ohne dass Name und Version aneinanderstoßen.
|
||
return (
|
||
<div
|
||
role="row"
|
||
aria-busy={laedt || undefined}
|
||
className="grid grid-cols-1 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 -mt-1 text-sm text-text-2 md:hidden">
|
||
{laeuftZelle}
|
||
{!laedt && (neu || fehler) && <> {neu ? "→" : "·"} {neuZelle}</>}
|
||
</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>
|
||
<div role="cell" className="text-sm leading-relaxed text-text-2">
|
||
{fest && (
|
||
<span className="mb-1.5 block text-bernstein">
|
||
Festgehalten seit {fest.seit} auf {fest.version}: {fest.grund}. Der Sonntags-Lauf überspringt {name}.
|
||
</span>
|
||
)}
|
||
{was}
|
||
</div>
|
||
{(fest || hatUpdate) && (
|
||
<span role="cell" className="flex flex-wrap gap-2 md:flex-col md:items-end">
|
||
{fest && (
|
||
<Button variant="outline" size="sm" onClick={onFreigeben}>
|
||
Freigeben
|
||
</Button>
|
||
)}
|
||
{hatUpdate && (
|
||
<Button variant="outline" size="sm" onClick={onStart} disabled={gesperrt}>
|
||
Aktualisieren
|
||
</Button>
|
||
)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const LAUF_ART: Record<UpdateLauf["stufe"], ChipArt> = {
|
||
rot: "rot", gelb: "bernstein", gruen: "gruen", info: "cyan", grau: "grau",
|
||
}
|
||
const PUNKT: Record<UpdateLauf["stufe"], string> = {
|
||
rot: "bg-rot", gelb: "bg-bernstein", gruen: "bg-gruen", info: "bg-cyan", grau: "bg-text-3",
|
||
}
|
||
|
||
/** Ein Update-Lauf mit dem Ergebnis je Baustein — so, wie autoupdate.sh es gemeldet hat. */
|
||
function LaufEintrag({ lauf }: { lauf: UpdateLauf }) {
|
||
return (
|
||
<li className="flex flex-col gap-2 border-t border-linie py-3">
|
||
{/* Schmales Feld: 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 @sm: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 @sm:col-span-1 @sm:col-start-2 @sm:row-start-1">
|
||
{lauf.anlass}
|
||
</span>
|
||
<span className="col-start-2 row-start-1 justify-self-end @sm: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 @sm: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 break-words">
|
||
<span className="text-foreground">{z.baustein}</span> · {z.text}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</li>
|
||
)
|
||
}
|
||
|
||
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()
|
||
const verlauf = useUpdateVerlauf()
|
||
const sicherungen = useSicherungen()
|
||
const updateStarten = useUpdateStarten()
|
||
const freigeben = useFreigeben()
|
||
const sichern = useJetztSichern()
|
||
const zurueck = useZurueckspielen()
|
||
const [frage, setFrage] = useState<Frage>(null)
|
||
|
||
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 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="@container 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 (
|
||
<>
|
||
<Panel
|
||
titel="Updates"
|
||
rechts={
|
||
<div className="flex flex-wrap gap-2.5">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => updateStarten.mutate("pruefen")}
|
||
disabled={updateStarten.isPending || suchtGerade}
|
||
>
|
||
Nach Neuem suchen
|
||
</Button>
|
||
<Button onClick={() => setFrage({ art: "alle" })} disabled={gesperrt || neuLautStart.size === 0}>
|
||
Alles jetzt aktualisieren
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<p className="-mt-2 text-[15px] text-text-2">
|
||
Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.
|
||
</p>
|
||
{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">{verlaufInhalt}</Panel>
|
||
|
||
<Panel
|
||
titel="Sicherungen"
|
||
rechts={
|
||
<Button variant="outline" size="sm" onClick={() => sichern.mutate(undefined)} disabled={sichern.isPending}>
|
||
{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>
|
||
{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="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={() => {
|
||
// Die Abfrage auf `frage` fängt den zweiten Klick eines Doppelklicks: Dann ist sie schon null.
|
||
if (frage?.art === "alle") 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)
|
||
setFrage(null)
|
||
}}
|
||
/>
|
||
</>
|
||
)
|
||
}
|