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
@@ -0,0 +1,82 @@
import { fireEvent, render, screen } from "@testing-library/react"
import { sichtbareJobs } from "@/lib/anzeige"
import type { Job } from "@/lib/typen"
import { JobKarte } from "./JobKarte"
const JETZT = 1_790_000_000 // Unix-Sekunden, fester Bezugspunkt
function job(teil: Partial<Job>): Job {
return {
id: "j1", label: "Nach Updates suchen", state: "running", group: null,
log: ["$ bash -c 'sudo apt-get update'"], returncode: null,
started_at: JETZT - 120, finished_at: null, ...teil,
}
}
describe("JobKarte", () => {
it("zeigt bei einem Download Fortschritt und fragt vor dem Abbrechen nach", () => {
const onAbbrechen = vi.fn()
render(
<JobKarte
job={job({
label: "download unsloth/Qwen3.8-27B-GGUF", progress: 42,
done_bytes: 12 * 1024 ** 3, total_bytes: 29 * 1024 ** 3, eta_s: 360,
})}
onAbbrechen={onAbbrechen}
/>,
)
expect(screen.getByText("Download unsloth/Qwen3.8-27B-GGUF")).toBeInTheDocument()
expect(screen.getByRole("progressbar")).toHaveAttribute("aria-valuenow", "42")
expect(screen.getByText("42 % · 12,0 GB von 29,0 GB · noch etwa 6 min")).toBeInTheDocument()
fireEvent.click(screen.getByRole("button", { name: "Abbrechen" }))
expect(screen.getByRole("dialog", { name: "Auftrag abbrechen?" })).toBeInTheDocument()
expect(onAbbrechen).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole("button", { name: "Ja, abbrechen" }))
expect(onAbbrechen).toHaveBeenCalledTimes(1)
})
it("nennt einen abgebrochenen Auftrag „Abgebrochen“ und nicht „Läuft“", () => {
render(<JobKarte job={job({ state: "canceled", finished_at: JETZT })} onAbbrechen={() => {}} />)
expect(screen.getByText("Abgebrochen")).toBeInTheDocument()
expect(screen.queryByText("Läuft")).not.toBeInTheDocument()
expect(screen.queryByRole("button", { name: "Abbrechen" })).not.toBeInTheDocument()
})
it("bietet bei Updates kein Abbrechen an und zeigt bei Fehlern die letzte Meldung", () => {
const { rerender } = render(
<JobKarte job={job({ label: "Hermes-Agent-Update", group: "maintenance" })} onAbbrechen={() => {}} />,
)
expect(screen.queryByRole("button", { name: "Abbrechen" })).not.toBeInTheDocument()
rerender(
<JobKarte
job={job({
label: "Hermes-Agent-Update", group: "maintenance", state: "failed", finished_at: JETZT,
log: ["$ bash -c …", "✗ HERMES-UPDATE FEHLGESCHLAGEN (Dienste laufen wieder, aber alter Stand)", ""],
})}
/>,
)
expect(screen.getByText("Fehler")).toBeInTheDocument()
expect(screen.getByText(/Letzte Meldung: ✗ HERMES-UPDATE FEHLGESCHLAGEN/)).toBeInTheDocument()
})
})
describe("sichtbareJobs", () => {
it("zeigt alle laufenden Aufträge jeder Gruppe und die drei zuletzt beendeten", () => {
const liste = sichtbareJobs(
[
job({ id: "alt", state: "done", finished_at: JETZT - 2 * 86_400 }),
job({ id: "f1", state: "done", finished_at: JETZT - 50 }),
job({ id: "f2", state: "failed", finished_at: JETZT - 40 }),
job({ id: "f3", state: "canceled", finished_at: JETZT - 30 }),
job({ id: "f4", state: "done", finished_at: JETZT - 20 }),
job({ id: "download", label: "download a/b", state: "running", group: null }),
job({ id: "update", group: "maintenance", state: "queued", started_at: JETZT - 10 }),
],
JETZT,
)
expect(liste.map((j) => j.id)).toEqual(["download", "update", "f4", "f3", "f2"])
})
})