aufträge und „Alle aktualisieren“: beendete lassen sich ausblenden

User 25.09.: Die Karten waren „immer da“ und ließen sich weder bestätigen noch
wegklicken. Beendete Aufträge der KI-Box und das letzte „Alle aktualisieren“
im Homelab haben jetzt einen Knopf „Ausblenden“ (quittiert wird auf dem Server,
gilt also auf allen Geräten; das Protokoll behält alles).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 12:54:21 +02:00
co-authored by Claude Opus 5.5
parent 0df593c05a
commit 54a3649d5e
39 changed files with 172 additions and 42 deletions
@@ -1,4 +1,4 @@
import { useJobAbbrechen, useJobs } from "@/lib/abfragen"
import { useJobAbbrechen, useJobQuittieren, useJobs } from "@/lib/abfragen"
import { jobTitel, sichtbareJobs } from "@/lib/wartung"
import { JobKarte } from "./JobKarte"
import { Panel } from "./Panel"
@@ -10,6 +10,7 @@ import { Panel } from "./Panel"
export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
const jobs = useJobs()
const abbrechen = useJobAbbrechen()
const quittieren = useJobQuittieren()
const liste = sichtbareJobs(jobs.data?.jobs ?? [])
let inhalt
@@ -23,6 +24,7 @@ export function Auftraege({ alsPanel = false }: { alsPanel?: boolean }) {
ebene={alsPanel ? 3 : 4}
abbrechenGesperrt={abbrechen.isPending}
onAbbrechen={(job) => abbrechen.mutate({ id: job.id, titel: jobTitel(job) })}
onQuittieren={(job) => quittieren.mutate(job.id)}
/>
))}
</div>
@@ -97,3 +97,16 @@ describe("sichtbareJobs", () => {
expect(liste.map((j) => j.id)).toEqual(["download", "update", "f4", "f3", "f2"])
})
})
describe("Ausblenden (seit 25.09.2026)", () => {
it("bietet es nur für beendete Aufträge an und blendet quittierte aus", () => {
const onQuittieren = vi.fn()
const { rerender } = render(<JobKarte job={job({})} onQuittieren={onQuittieren} />)
expect(screen.queryByRole("button", { name: "Ausblenden" })).not.toBeInTheDocument()
const fertig = job({ state: "failed", returncode: 1, finished_at: JETZT - 60 })
rerender(<JobKarte job={fertig} onQuittieren={onQuittieren} />)
fireEvent.click(screen.getByRole("button", { name: "Ausblenden" }))
expect(onQuittieren).toHaveBeenCalledWith(fertig)
expect(sichtbareJobs([fertig, { ...fertig, id: "j2", quittiert: true }], JETZT).map((j) => j.id)).toEqual(["j1"])
})
})
+9 -2
View File
@@ -41,9 +41,11 @@ function useJetzt(aktiv: boolean) {
* die letzte Meldung, auf Wunsch das Protokoll. Abbrechen nur, wo es wirklich stoppt (`abbrechbar`).
* `ebene` ist die Überschriftenebene (unter einem Panel-Titel h3, unter einer Zwischenüberschrift h4).
*/
export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
export function JobKarte({ job, onAbbrechen, onQuittieren, abbrechenGesperrt, ebene = 4 }: {
job: Job
onAbbrechen?: (job: Job) => void
/** Beendete Aufträge lassen sich ausblenden. */
onQuittieren?: (job: Job) => void
abbrechenGesperrt?: boolean
ebene?: 3 | 4
}) {
@@ -95,7 +97,7 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
</p>
)}
{(darfAbbrechen || protokollZeilen.length > 1) && (
{(darfAbbrechen || protokollZeilen.length > 1 || (!laeuft && onQuittieren)) && (
<div className="flex flex-wrap gap-2">
{darfAbbrechen && (
<Button variant="outline" size="sm" disabled={abbrechenGesperrt} onClick={() => setFrage(true)}>
@@ -107,6 +109,11 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt, ebene = 4 }: {
{protokoll ? "Protokoll zu" : "Protokoll"}
</Button>
)}
{!laeuft && onQuittieren && (
<Button variant="outline" size="sm" className="ml-auto" onClick={() => onQuittieren(job)}>
Ausblenden
</Button>
)}
</div>
)}
+11 -2
View File
@@ -75,7 +75,7 @@ const LAUF: Record<Sammellauf["status"], { art: ChipArt; text: string }> = {
}
/** Fortschritt des Sammellaufs: jeder Schritt mit Zustand; fertige Läufe bleiben bis zum nächsten stehen. */
export function SammellaufFortschritt({ lauf }: { lauf: Sammellauf }) {
export function SammellaufFortschritt({ lauf, onQuittieren }: { lauf: Sammellauf; onQuittieren?: () => void }) {
const fertig = lauf.schritte.filter((s) => !["wartet", "laeuft"].includes(s.status)).length
const l = LAUF[lauf.status]
return (
@@ -104,7 +104,16 @@ export function SammellaufFortschritt({ lauf }: { lauf: Sammellauf }) {
)
})}
</ol>
{lauf.text && <p className="text-sm text-text-2">{lauf.text}</p>}
{(lauf.text || (lauf.status !== "laeuft" && onQuittieren)) && (
<div className="flex flex-wrap items-center justify-between gap-3">
{lauf.text && <p className="text-sm text-text-2">{lauf.text}</p>}
{lauf.status !== "laeuft" && onQuittieren && (
<Button variant="outline" size="sm" className="ml-auto" onClick={onQuittieren}>
Ausblenden
</Button>
)}
</div>
)}
</section>
)
}
@@ -73,3 +73,19 @@ describe("SammellaufFortschritt", () => {
expect(screen.getByText(/118 Pakete · wartet/)).toBeInTheDocument()
})
})
describe("SammellaufFortschritt ausblenden", () => {
it("zeigt „Ausblenden“ erst, wenn der Lauf fertig ist", () => {
const fertig: Sammellauf = {
id: "s2", status: "fertig", start: 1_790_000_000, ende: 1_790_001_000, aktuell: null,
text: "8 Updates eingespielt, alle Prüfungen grün.",
schritte: [{ ziel: "ct-104", name: "Gitea", baustein: "os", was: "73 Pakete", status: "eingespielt", lauf: "l1", text: null }],
}
const weg = vi.fn()
const { rerender } = render(<SammellaufFortschritt lauf={{ ...fertig, status: "laeuft", ende: null }} onQuittieren={weg} />)
expect(screen.queryByRole("button", { name: "Ausblenden" })).not.toBeInTheDocument()
rerender(<SammellaufFortschritt lauf={fertig} onQuittieren={weg} />)
fireEvent.click(screen.getByRole("button", { name: "Ausblenden" }))
expect(weg).toHaveBeenCalled()
})
})