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 = { 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 ( wird geprüft ) } /** 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 (

{text}

{lang && ( )}
) } /** 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 (
{pakete.length === 1 ? "Das Paket ansehen" : `Alle ${pakete.length} Pakete ansehen`}
    {pakete.map((p) => (
  • {p.name}{" "} {p.current} → {p.candidate}
  • ))}
{zurueck.length > 0 && (

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.

)}
) } /** Das Urteil des Hirns aus den Versionshinweisen: Muss der Besitzer etwas tun? */ function Urteil({ d }: { d: UpdateDetails }) { if (d.action_needed === false) return Nichts zu tun if (d.action_needed !== true) return null return ( Du musst etwas tun {d.action_text && {d.action_text}} ) } 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: )“. 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 ? : <>{laeuftText} const neuZelle = laedt ? ( ) : neu ? ( {neu} ) : fehler ? ( nicht geprüft ) : ( – ) let was if (laedt) { was = Wird geprüft … } else if (fehler && !hatUpdate) { was = {pruefFehlerText(fehler)} } else if (!hatUpdate) { was = "Aktuell." } else { was = (
{fehler && {pruefFehlerText(fehler)}} {d && } {id === "os" ? ( <> {d?.count ? `${d.count} ${d.count === 1 ? "Paket" : "Pakete"} aus Ubuntu.` : "Neue Pakete aus Ubuntu."} {d && } ) : summary ? ( ) : null}
) } // 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 (
{name} {laeuftZelle} {!laedt && (neu || fehler) && <> {neu ? "→" : "·"} {neuZelle}} {laeuftZelle} {neuZelle}
{fest && ( Festgehalten seit {fest.seit} auf {fest.version}: {fest.grund}. Der Sonntags-Lauf überspringt {name}. )} {was}
{(fest || hatUpdate) && ( {fest && ( )} {hatUpdate && ( )} )}
) } const LAUF_ART: Record = { rot: "rot", gelb: "bernstein", gruen: "gruen", info: "cyan", grau: "grau", } const PUNKT: Record = { 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 (
  • {/* Schmales Feld: Zeit und Ergebnis oben, der Anlass darunter über die volle Breite. */}
    {flugplanZeit(lauf.start)} {lauf.anlass} {lauf.titel}
    {lauf.stufe !== "grau" && lauf.zeilen.length > 0 && (
      {lauf.zeilen.map((z) => (
    • {z.baustein} · {z.text}
    • ))}
    )}
  • ) } 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(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 = } else if (start.isError && !start.data) { tabelle = } else { tabelle = (
    Baustein Läuft Neu Was sich ändert Aktion
    {BAUSTEINE.map((b) => ( 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 }) }} /> ))}
    ) } let verlaufInhalt if (verlauf.isPending) { verlaufInhalt =

    Wird gelesen …

    } else if (verlauf.isError && !verlauf.data) { verlaufInhalt =

    Der Verlauf lässt sich gerade nicht lesen: {verlauf.error.message}

    } else if (!verlauf.data?.laeufe.length) { verlaufInhalt =

    Noch keine Update-Läufe aufgezeichnet.

    } else { verlaufInhalt = (
      {verlauf.data.laeufe.map((l) => )}
    ) } let sicherungsInhalt if (sicherungen.isPending) { sicherungsInhalt =

    Wird gelesen …

    } else if (sicherungen.isError && !sicherungen.data) { sicherungsInhalt =

    Die Sicherungen lassen sich gerade nicht lesen: {sicherungen.error.message}

    } else if (!sicherungen.data?.backups.length) { sicherungsInhalt =

    Noch keine Sicherung vorhanden.

    } else { sicherungsInhalt = (
      {sicherungen.data.backups.slice(0, 5).map((s) => (
    • {sicherungsZeit(s)} {s.size_mb.toLocaleString("de-DE")} MB
    • ))}
    ) } return ( <> } >

    Jeden Sonntag um 04:30 automatisch. Vorher wird gesichert, danach geprüft.

    {updateLaeuft && (

    Gerade läuft ein Update. Weitere lassen sich erst starten, wenn es fertig ist.

    )} {tabelle}
    {verlaufInhalt} sichern.mutate(undefined)} disabled={sichern.isPending}> {sichern.isPending ? "Sichert …" : "Jetzt sichern"} } >

    Täglich um 03:35, mit Kopie auf einem zweiten Gerät.

    {sicherungen.data && !zurueckMoeglich && (

    Zurückspielen geht nur auf der Box selbst.

    )} {sicherungsInhalt}
    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) }} /> setFrage(null)} onJa={() => { if (frage?.art === "einzeln") updateStarten.mutate(frage.id) setFrage(null) }} /> setFrage(null)} onJa={() => { if (frage?.art === "freigeben") freigeben.mutate(frage.id) setFrage(null) }} /> setFrage(null)} onJa={() => { if (frage?.art === "zurueck") zurueck.mutate(frage.datei) setFrage(null) }} /> ) }