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
+65 -28
View File
@@ -3,16 +3,28 @@
// Takte: Steht der Live-Strom, meldet er Änderungen selbst — die Abfragen sind dann nur
// Sicherheitsnetz und laufen fünfmal seltener.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { type QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { api, post } from "./api"
import { fehlerText, laeuftNoch } from "./anzeige"
import { melden } from "./meldungen"
import { stromVerbunden } from "./strom"
import type {
AktionsErgebnis, AufraeumKandidat, Job, Modell, Nutzung, Radar, Sicherung, Start,
AktionsErgebnis, AufraeumKandidat, Job, Modell, Nutzung, Radar, Sicherung, Start, UpdateDetails,
} from "./typen"
import { gbText } from "./zeit"
const takt = (ms: number) => () => (stromVerbunden() ? ms * 5 : ms)
/** Was ein beendeter Auftrag verändert haben kann: Versionen, Modelle, Sicherungen, Platte. */
const NACH_AUFTRAG = ["start", "update-details", "updates-verlauf", "models", "sicherungen", "aufraeumen"]
/** Ist seit der letzten Abfrage ein Auftrag fertig geworden, lesen die betroffenen Ansichten neu. */
function auftragsEndeMelden(qc: QueryClient, neu: Job[]) {
const alt = qc.getQueryData<{ jobs: Job[] }>(["jobs"])?.jobs ?? []
const ebenFertig = neu.some((j) => !laeuftNoch(j) && alt.some((a) => a.id === j.id && laeuftNoch(a)))
if (ebenFertig) for (const k of NACH_AUFTRAG) qc.invalidateQueries({ queryKey: [k] })
}
export const useStart = () =>
useQuery({
queryKey: ["start"],
@@ -34,14 +46,19 @@ export const useNutzung = () =>
export const useRadar = () =>
useQuery({ queryKey: ["radar"], queryFn: () => api<Radar>("/api/radar"), retry: false, refetchInterval: 300_000 })
export const useJobs = () =>
useQuery({
export const useJobs = () => {
const qc = useQueryClient()
return useQuery({
queryKey: ["jobs"],
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
queryFn: async () => {
const daten = await api<{ jobs: Job[] }>("/api/jobs")
auftragsEndeMelden(qc, daten.jobs)
return daten
},
// Läuft etwas, wollen wir den Fortschritt sehen; sonst reicht der Strom.
refetchInterval: (q) =>
q.state.data?.jobs.some((j) => j.state === "running" || j.state === "queued") ? 2000 : takt(30_000)(),
refetchInterval: (q) => (q.state.data?.jobs.some(laeuftNoch) ? 2000 : takt(30_000)()),
})
}
export const useSicherungen = () =>
useQuery({
@@ -49,21 +66,6 @@ export const useSicherungen = () =>
queryFn: () => api<{ available: boolean; backups: Sicherung[] }>("/api/zeitmaschine"),
})
export interface UpdateDetails {
kind: string
installed_version?: string | null
behind?: number
installed_build?: number | null
latest_build?: number | null
latest_tag?: string | null
count?: number
packages?: { name: string; current: string; candidate: string }[]
summary?: string
action_needed?: boolean | null
action_text?: string
error?: string
}
export const useUpdateDetails = (art: string | null) =>
useQuery({
queryKey: ["update-details", art],
@@ -108,7 +110,12 @@ export const useDienste = () =>
function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
fn: (v: V) => Promise<E>,
opt: { erfolg: (v: V, e: E) => string; neuLaden: string[] },
opt: {
erfolg: (v: V, e: E) => string
/** Eigener Text, wenn die Box mit ok: false antwortet (sonst der Grund aus der Antwort). */
fehler?: (v: V, e: AktionsErgebnis) => string
neuLaden: string[]
},
) {
const qc = useQueryClient()
return useMutation({
@@ -116,7 +123,7 @@ function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
onSuccess: (e, v) => {
const ergebnis = e as AktionsErgebnis
if (ergebnis && ergebnis.ok === false) {
melden("fehler", ergebnis.detail || ergebnis.err || "Das hat nicht geklappt.")
melden("fehler", opt.fehler?.(v, ergebnis) ?? fehlerText(ergebnis))
} else {
melden("erfolg", opt.erfolg(v, e))
}
@@ -130,7 +137,11 @@ export const useHinweisAktion = () =>
useAktion(
({ hinweis, aktion }: { hinweis: string; aktion: string; label: string }) =>
post<AktionsErgebnis>(`/api/hinweise/${encodeURIComponent(hinweis)}/aktion/${aktion}`),
{ erfolg: (v) => `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`, neuLaden: ["start"] },
{
erfolg: (v, e) =>
v.aktion === "freigeben" && e.text ? e.text : `${v.label}: erledigt. Der Wächter prüft in einer Minute nach.`,
neuLaden: ["start"],
},
)
const UPDATE_PFADE: Record<string, string> = {
@@ -144,10 +155,34 @@ const UPDATE_PFADE: Record<string, string> = {
export const useUpdateStarten = () =>
useAktion((art: string) => post<AktionsErgebnis>(UPDATE_PFADE[art]), {
erfolg: (art) => (art === "pruefen" ? "Suche nach Neuem läuft." : "Update läuft. Den Fortschritt siehst du unten."),
erfolg: (art) =>
art === "pruefen"
? "Die Suche nach Neuem läuft. Den Fortschritt siehst du unten bei den Aufträgen."
: "Das Update läuft. Den Fortschritt siehst du unten bei den Aufträgen.",
neuLaden: ["jobs", "start"],
})
/** Einen laufenden Auftrag abbrechen (nur Downloads und die Suche, siehe `abbrechbar`). */
export const useJobAbbrechen = () =>
useAktion(({ id }: { id: string; titel: string }) => post<AktionsErgebnis>(`/api/jobs/${encodeURIComponent(id)}/cancel`), {
erfolg: (v) => `${v.titel}: wird abgebrochen.`,
fehler: (v) => `${v.titel} lief schon nicht mehr.`,
neuLaden: ["jobs"],
})
/** Ein Modell von Hugging Face holen (Hintergrund-Auftrag, mit optionaler Rolle nach dem Download). */
export const useInstallieren = () =>
useAktion(
({ repo, quant, rolle }: { repo: string; quant: string; rolle: string | null }) =>
post<AktionsErgebnis & { total_bytes?: number }>("/api/models/install", { repo, quant, role: rolle }),
{
erfolg: (v, e) =>
`${v.repo} (${v.quant}) wird heruntergeladen${e.total_bytes ? `, ${gbText(e.total_bytes, 1)}` : ""}. ` +
"Den Fortschritt siehst du unter Aufträge.",
neuLaden: ["jobs", "models"],
},
)
export const useAufraeumen = () =>
useQuery({
queryKey: ["aufraeumen"],
@@ -168,8 +203,10 @@ export const useFreigeben = () =>
)
export const useJetztSichern = () =>
useAktion(() => post<AktionsErgebnis>("/api/system/backup"), {
erfolg: () => "Sicherung erstellt.",
useAktion(() => post<AktionsErgebnis & { size_mb?: number }>("/api/system/backup"), {
erfolg: (_, e) =>
e.size_mb != null ? `Sicherung erstellt (${e.size_mb.toLocaleString("de-DE")} MB).` : "Sicherung erstellt.",
fehler: (_, e) => `Die Sicherung ging nicht: ${fehlerText(e)}`,
neuLaden: ["sicherungen", "start"],
})