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:
co-authored by
Claude Opus 5.5
parent
0df593c05a
commit
54a3649d5e
@@ -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"])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,7 +143,8 @@ function useAktion<V, E extends AktionsErgebnis | Record<string, unknown>>(
|
||||
const ergebnis = e as AktionsErgebnis
|
||||
if (ergebnis && ergebnis.ok === false) {
|
||||
melden("fehler", opt.fehler?.(v, ergebnis) ?? fehlerText(ergebnis))
|
||||
} else {
|
||||
} else if (opt.erfolg(v, e)) {
|
||||
// Leerer Text: keine Erfolgsmeldung (etwa beim Ausblenden — da sieht man es ja).
|
||||
melden("erfolg", opt.erfolg(v, e))
|
||||
}
|
||||
for (const k of opt.neuLaden) qc.invalidateQueries({ queryKey: [k] })
|
||||
@@ -189,6 +190,13 @@ export const useJobAbbrechen = () =>
|
||||
neuLaden: ["jobs"],
|
||||
})
|
||||
|
||||
/** Einen beendeten Auftrag ausblenden (seit 25.09.2026; vorher blieb er 24 Stunden stehen). */
|
||||
export const useJobQuittieren = () =>
|
||||
useAktion((id: string) => post<AktionsErgebnis>(`/api/jobs/${encodeURIComponent(id)}/quittieren`), {
|
||||
erfolg: () => "",
|
||||
neuLaden: ["jobs"],
|
||||
})
|
||||
|
||||
/** Ein Modell von Hugging Face holen (Hintergrund-Auftrag, mit optionaler Rolle nach dem Download). */
|
||||
export const useInstallieren = () =>
|
||||
useAktion(
|
||||
@@ -414,6 +422,13 @@ export const useAlleAktualisieren = () =>
|
||||
neuLaden: ["homelab-sammellauf", "homelab-laeufe", "homelab"],
|
||||
})
|
||||
|
||||
/** Ein beendetes „Alle aktualisieren“ ausblenden (das Protokoll behält es). */
|
||||
export const useSammellaufQuittieren = () =>
|
||||
useAktion((id: string) => post<AktionsErgebnis>(`/api/homelab/sammellauf/${encodeURIComponent(id)}/quittieren`), {
|
||||
erfolg: () => "",
|
||||
neuLaden: ["homelab-sammellauf"],
|
||||
})
|
||||
|
||||
export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean) =>
|
||||
useQuery({
|
||||
queryKey: ["homelab-protokoll", berichte],
|
||||
|
||||
@@ -173,6 +173,8 @@ export interface Job {
|
||||
started_at: number
|
||||
finished_at: number | null
|
||||
canceled?: boolean
|
||||
/** Vom Nutzer ausgeblendet (seit 25.09.2026). */
|
||||
quittiert?: boolean
|
||||
/** Nur bei Downloads: Fortschritt 0…100 und die Bytes dazu. */
|
||||
progress?: number | null
|
||||
total_bytes?: number
|
||||
|
||||
@@ -128,7 +128,7 @@ export function letzteMeldung(j: Pick<Job, "log">): string | null {
|
||||
export function sichtbareJobs(jobs: Job[], jetzt = Date.now() / 1000): Job[] {
|
||||
const laufend = jobs.filter(laeuftNoch).sort((a, b) => a.started_at - b.started_at)
|
||||
const beendet = jobs
|
||||
.filter((j) => !laeuftNoch(j) && jetzt - (j.finished_at ?? j.started_at) < 86_400)
|
||||
.filter((j) => !laeuftNoch(j) && !j.quittiert && jetzt - (j.finished_at ?? j.started_at) < 86_400)
|
||||
.sort((a, b) => (b.finished_at ?? b.started_at) - (a.finished_at ?? a.started_at))
|
||||
.slice(0, 3)
|
||||
return [...laufend, ...beendet]
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AlleKnopf, SammellaufFortschritt } from "@/components/homelab/Sammellau
|
||||
import { UpdateTabelle } from "@/components/homelab/UpdateTabelle"
|
||||
import {
|
||||
useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher, useInstanz, usePartner,
|
||||
useSammellauf,
|
||||
useSammellauf, useSammellaufQuittieren,
|
||||
useZielAktion,
|
||||
} from "@/lib/abfragen"
|
||||
import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab"
|
||||
@@ -242,6 +242,7 @@ export function HomelabSeite() {
|
||||
export function HomelabUpdatesSeite() {
|
||||
const { ersatz, stand, laufend, laeufe, aktion } = useHomelabBereich()
|
||||
const sammel = useSammellauf(!!stand)
|
||||
const quittieren = useSammellaufQuittieren()
|
||||
if (ersatz || !stand) return ersatz
|
||||
|
||||
const lauf = sammel.data?.sammellauf ?? null
|
||||
@@ -259,7 +260,7 @@ export function HomelabUpdatesSeite() {
|
||||
</p>
|
||||
<AusfuehrerHinweis stand={stand} />
|
||||
{/* Ein fertiger Lauf bleibt sechs Stunden stehen — gemessen am letzten Bericht, nicht an der Uhr beim Zeichnen. */}
|
||||
{lauf && (sammelLaeuft || (lauf.ende && (stand.bericht?.empfangen ?? 0) - lauf.ende < 6 * 3600)) && <SammellaufFortschritt lauf={lauf} />}
|
||||
{lauf && (sammelLaeuft || (lauf.ende && (stand.bericht?.empfangen ?? 0) - lauf.ende < 6 * 3600)) && <SammellaufFortschritt lauf={lauf} onQuittieren={() => quittieren.mutate(lauf.id)} />}
|
||||
<UpdateTabelle
|
||||
ziele={stand.ziele}
|
||||
laufend={laufend}
|
||||
|
||||
Reference in New Issue
Block a user