Ampel / ampel (push) Failing after 22s
Der Verlauf las bisher nur den Hermes-Cron-Status ("Skript lief") und zeigte die
Laeufe vom 06. und 13.09. als "durchgelaufen", obwohl dort zurueckgerollt und
festgehalten wurde; ein abgebrochener Lauf stand als "laeuft" da. Jetzt kommt der
Verlauf aus dem Meldeprotokoll (~/mc2-notify.log), also aus dem, was autoupdate.sh
selbst je Baustein meldet.
Festgehaltene Bausteine (Pin-Register) werden zum Waechter-Hinweis mit Knopf
"Freigeben", auf der Updates-Seite markiert und in der Updates-Lampe gezaehlt.
Hermes zeigt seine laufende Version und die Zahl der neuen Aenderungen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
import { useState } from "react"
|
||
import { cn } from "cn"
|
||
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 { versionen } from "@/lib/anzeige"
|
||
import type { Festgehalten, Job, Sicherung } 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
|
||
|
||
function BausteinZeile({ id, name, hatUpdate, fest, onStart, onFreigeben, laeuftGerade }: {
|
||
id: string
|
||
name: string
|
||
hatUpdate: boolean
|
||
fest: Festgehalten | undefined
|
||
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 = !hatUpdate
|
||
? "Aktuell."
|
||
: data?.summary?.trim() || (id === "os" && data?.count ? "Paketliste unten in den Details." : isPending ? "Wird gelesen …" : "")
|
||
return (
|
||
<tr className="border-t border-linie align-top">
|
||
<th scope="row" className="py-3.5 pr-4 text-left font-sans text-[15px] font-semibold">{name}</th>
|
||
<td className="ziffern py-3.5 pr-4 text-sm text-text-2">{v.laeuft}</td>
|
||
<td className="ziffern py-3.5 pr-4 text-sm">{neu ? <span className="text-cyan">{neu}</span> : <span className="text-text-3">–</span>}</td>
|
||
<td className="py-3.5 pr-4 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>
|
||
)}
|
||
<span className="line-clamp-3 whitespace-pre-line">{was}</span>
|
||
</td>
|
||
<td className="py-2.5 text-right">
|
||
<span className="inline-flex flex-col items-end gap-2">
|
||
{fest && (
|
||
<Button variant="outline" size="sm" onClick={onFreigeben}>
|
||
Freigeben
|
||
</Button>
|
||
)}
|
||
{hatUpdate && (
|
||
<Button variant="outline" size="sm" onClick={onStart} disabled={laeuftGerade}>
|
||
Aktualisieren
|
||
</Button>
|
||
)}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
)
|
||
}
|
||
|
||
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">
|
||
<div className="grid grid-cols-[112px_minmax(0,1fr)_auto] items-center gap-3">
|
||
<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>
|
||
</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">
|
||
<span className="text-foreground">{z.baustein}</span> · {z.text}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</li>
|
||
)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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 | { art: "alle" } | { art: "zurueck"; datei: string; zeit: string }>(null)
|
||
|
||
const mitUpdate = new Set(start.data?.updates.bausteine.map((b) => b.id) ?? [])
|
||
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")
|
||
|
||
return (
|
||
<>
|
||
<Panel
|
||
titel="Updates"
|
||
rechts={
|
||
<div className="flex flex-wrap gap-2.5">
|
||
<Button variant="outline" onClick={() => updateStarten.mutate("pruefen")} disabled={updateStarten.isPending}>
|
||
Nach Neuem suchen
|
||
</Button>
|
||
<Button onClick={() => setFrage({ art: "alle" })} disabled={laeuft || mitUpdate.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>
|
||
{start.isPending ? (
|
||
<Zustandsfeld text="Updates werden gelesen …" />
|
||
) : (
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full min-w-[680px] border-collapse">
|
||
<thead>
|
||
<tr className="schild text-left text-xs text-text-3">
|
||
<th scope="col" className="pb-2 font-semibold">Baustein</th>
|
||
<th scope="col" className="pb-2 font-semibold">Läuft</th>
|
||
<th scope="col" className="pb-2 font-semibold">Neu</th>
|
||
<th scope="col" className="pb-2 font-semibold">Was sich ändert</th>
|
||
<th scope="col" className="pb-2 text-right font-semibold"><span className="sr-only">Aktion</span></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{BAUSTEINE.map((b) => (
|
||
<BausteinZeile
|
||
key={b.id}
|
||
id={b.id}
|
||
name={b.name}
|
||
hatUpdate={mitUpdate.has(b.id)}
|
||
fest={festgehalten.get(b.id)}
|
||
laeuftGerade={laeuft}
|
||
onStart={() => updateStarten.mutate(b.id)}
|
||
onFreigeben={() => freigeben.mutate(b.id)}
|
||
/>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</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>
|
||
)}
|
||
</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="Sicherungen"
|
||
rechts={
|
||
<Button variant="outline" size="sm" onClick={() => sichern.mutate(undefined)} disabled={sichern.isPending}>
|
||
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>
|
||
</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."
|
||
knopf="Aktualisieren"
|
||
onNein={() => setFrage(null)}
|
||
onJa={() => {
|
||
updateStarten.mutate("alle")
|
||
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"
|
||
onNein={() => setFrage(null)}
|
||
onJa={() => {
|
||
if (frage?.art === "zurueck") zurueck.mutate(frage.datei)
|
||
setFrage(null)
|
||
}}
|
||
/>
|
||
</>
|
||
)
|
||
}
|