start: Radar-Kasten springt wirklich zum Radar, Checkliste fragt vor Neustart und Freigeben
- "Ansehen" im Radar-Kasten fuehrt nach /modelle#radar. Der Router sprang beim Rendern zum Anker, als dort noch "wird gelesen" stand; jetzt springt die Modelle-Seite, sobald alles ueber dem Radar geladen ist. - Knoepfe der Checkliste, die einen Dienst (neu) starten, einen Job erneut laufen lassen oder ein Update freigeben, fragen vorher nach. Das Protokoll oeffnet weiter sofort; der Dialog wird erst bei Bedarf geladen und bleibt aus dem Start-Buendel. - Scheitert nur ein spaeteres Nachladen, bleibt der letzte Stand der Startseite stehen statt einer Fehlerflaeche. - Test fuer die Rueckfrage-Logik (wann gefragt wird, Checkliste erst nach "Ja", gesperrter Ja-Knopf, Ersatz-Hinweis beim Installieren). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3c1d8bf88f
commit
739f27a7e5
@@ -2,31 +2,43 @@ import { lazy, Suspense, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useHinweisAktion } from "@/lib/abfragen"
|
||||
import { hinweisRueckfrage } from "@/lib/anzeige"
|
||||
import { post } from "@/lib/api"
|
||||
import { melden } from "@/lib/meldungen"
|
||||
import type { AktionsErgebnis, Hinweis, VerlaufEintrag } from "@/lib/typen"
|
||||
import type { AktionsErgebnis, Hinweis, HinweisAktion, VerlaufEintrag } from "@/lib/typen"
|
||||
import { seit, vor } from "@/lib/zeit"
|
||||
|
||||
// Erst laden, wenn jemand ein Protokoll öffnet (Radix-Dialog gehört nicht ins Start-Bündel).
|
||||
// Erst laden, wenn jemand ein Protokoll öffnet oder eine Rückfrage kommt — die Radix-Dialog-Kette
|
||||
// gehört nicht ins Start-Bündel.
|
||||
const Protokollfenster = lazy(() => import("./Protokollfenster").then((m) => ({ default: m.Protokollfenster })))
|
||||
const Bestaetigen = lazy(() => import("./Bestaetigen").then((m) => ({ default: m.Bestaetigen })))
|
||||
|
||||
/** Ein offener Punkt: Titel · Leitpunkte · PRÜFEN/JETZT, darunter Text und Knöpfe. */
|
||||
function Punkt({ h }: { h: Hinweis }) {
|
||||
const aktion = useHinweisAktion()
|
||||
const [protokoll, setProtokoll] = useState<{ titel: string; text: string } | null>(null)
|
||||
const [frage, setFrage] = useState<HinweisAktion | null>(null)
|
||||
const rot = h.stufe === "rot"
|
||||
const rueckfrage = frage ? hinweisRueckfrage(h, frage) : null
|
||||
|
||||
async function ausloesen(aid: string, label: string) {
|
||||
if (aid === "protokoll") {
|
||||
try {
|
||||
const e = await post<AktionsErgebnis>(`/api/hinweise/${encodeURIComponent(h.id)}/aktion/protokoll`)
|
||||
setProtokoll({ titel: h.titel, text: e.text || e.out || e.err || "Kein Protokoll vorhanden." })
|
||||
} catch (f) {
|
||||
melden("fehler", (f as Error).message)
|
||||
}
|
||||
return
|
||||
async function protokollZeigen() {
|
||||
try {
|
||||
const e = await post<AktionsErgebnis>(`/api/hinweise/${encodeURIComponent(h.id)}/aktion/protokoll`)
|
||||
setProtokoll({ titel: h.titel, text: e.text || e.out || e.err || "Kein Protokoll vorhanden." })
|
||||
} catch (f) {
|
||||
melden("fehler", (f as Error).message)
|
||||
}
|
||||
aktion.mutate({ hinweis: h.id, aktion: aid, label })
|
||||
}
|
||||
|
||||
function ausfuehren(a: HinweisAktion) {
|
||||
aktion.mutate({ hinweis: h.id, aktion: a.id, label: a.label })
|
||||
}
|
||||
|
||||
function klick(a: HinweisAktion) {
|
||||
if (a.id === "protokoll") protokollZeigen()
|
||||
// Was etwas neu startet, erneut laufen lässt oder freigibt, fragt vorher nach.
|
||||
else if (hinweisRueckfrage(h, a)) setFrage(a)
|
||||
else ausfuehren(a)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,7 +62,7 @@ function Punkt({ h }: { h: Hinweis }) {
|
||||
variant={i === 0 && a.id !== "protokoll" ? "default" : "outline"}
|
||||
size="sm"
|
||||
disabled={aktion.isPending}
|
||||
onClick={() => ausloesen(a.id, a.label)}
|
||||
onClick={() => klick(a)}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
@@ -62,6 +74,22 @@ function Punkt({ h }: { h: Hinweis }) {
|
||||
<Protokollfenster offen titel={protokoll.titel} text={protokoll.text} onSchliessen={() => setProtokoll(null)} />
|
||||
</Suspense>
|
||||
)}
|
||||
{frage && rueckfrage && (
|
||||
<Suspense fallback={null}>
|
||||
<Bestaetigen
|
||||
offen
|
||||
titel={rueckfrage.titel}
|
||||
text={rueckfrage.text}
|
||||
knopf={rueckfrage.knopf}
|
||||
gesperrt={aktion.isPending}
|
||||
onNein={() => setFrage(null)}
|
||||
onJa={() => {
|
||||
ausfuehren(frage)
|
||||
setFrage(null)
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"
|
||||
import { hinweisRueckfrage } from "@/lib/anzeige"
|
||||
import { post } from "@/lib/api"
|
||||
import type { Hinweis } from "@/lib/typen"
|
||||
import { rollenErsatzText } from "@/lib/wartung"
|
||||
import { Bestaetigen } from "./Bestaetigen"
|
||||
import { Checkliste } from "./Checkliste"
|
||||
|
||||
vi.mock("@/lib/api", async (original) => ({ ...(await original<typeof import("@/lib/api")>()), post: vi.fn() }))
|
||||
|
||||
const abgestuerzt: Hinweis = {
|
||||
id: "dienst:hermes-gateway", stufe: "rot", titel: "Hermes-Gateway ist abgestürzt", text: "failed",
|
||||
quelle: "hermes-gateway", seit: 1_790_000_000, zuletzt: 1_790_000_060,
|
||||
aktionen: [
|
||||
{ id: "neustart", label: "Neu starten", dienst: "hermes-gateway" },
|
||||
{ id: "protokoll", label: "Protokoll", dienst: "hermes-gateway" },
|
||||
],
|
||||
}
|
||||
|
||||
describe("hinweisRueckfrage", () => {
|
||||
it("fragt vor Neustart, Wiederholen und Freigeben, nie vor dem Protokoll", () => {
|
||||
expect(hinweisRueckfrage(abgestuerzt, { id: "protokoll", label: "Protokoll" })).toBeNull()
|
||||
expect(hinweisRueckfrage(abgestuerzt, { id: "neustart", label: "Neu starten" })?.text)
|
||||
.toBe("„Hermes-Gateway ist abgestürzt“: Der Dienst wird neu gestartet und ist dabei kurz nicht erreichbar.")
|
||||
expect(hinweisRueckfrage({ id: "dienst:x", titel: "X ist gestoppt" }, { id: "neustart", label: "Starten" })?.text)
|
||||
.toContain("wird gestartet")
|
||||
expect(hinweisRueckfrage({ id: "timer:mc2-backup", titel: "Sicherung scheitert" }, { id: "neustart", label: "Jetzt erneut laufen lassen" })?.text)
|
||||
.toContain("läuft sofort noch einmal")
|
||||
expect(hinweisRueckfrage({ id: "job:1", titel: "Job „News“" }, { id: "job-wiederholen", label: "Erneut ausführen" })?.titel)
|
||||
.toBe("Job erneut ausführen?")
|
||||
expect(hinweisRueckfrage({ id: "pin:engine", titel: "Motor bekommt keine Updates" }, { id: "freigeben", label: "Freigeben" })?.text)
|
||||
.toContain("Sonntags-Lauf")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Checkliste", () => {
|
||||
it("startet einen Dienst erst neu, wenn die Rückfrage bestätigt ist", async () => {
|
||||
vi.mocked(post).mockResolvedValue({ ok: true })
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<Checkliste hinweise={[abgestuerzt]} verlauf={[]} />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
fireEvent.click(screen.getByRole("button", { name: "Neu starten" }))
|
||||
const dialog = await screen.findByRole("dialog", { name: "Neu starten?" })
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Neu starten" }))
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/hinweise/dienst%3Ahermes-gateway/aktion/neustart"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("Bestaetigen", () => {
|
||||
it("lässt den Ja-Knopf nicht los, solange die Aktion schon unterwegs ist", () => {
|
||||
const onJa = vi.fn()
|
||||
render(<Bestaetigen offen titel="Installieren?" text="…" knopf="Installieren" gesperrt onJa={onJa} onNein={() => {}} />)
|
||||
const knopf = screen.getByRole("button", { name: "Installieren" })
|
||||
expect(knopf).toBeDisabled()
|
||||
fireEvent.click(knopf)
|
||||
expect(onJa).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("rollenErsatzText", () => {
|
||||
it("sagt deutlich, welches Modell die Rolle verliert", () => {
|
||||
expect(rollenErsatzText("org/Neu-GGUF", "Coder", { name: "Qwen3.8-27B-Heretic", zwilling: true })).toBe(
|
||||
"Achtung: Nach dem Download übernimmt org/Neu-GGUF die Rolle „Coder“ und ersetzt damit Qwen3.8-27B-Heretic. " +
|
||||
"Das bisherige Modell bleibt auf der Platte. Bilder liest vorerst weiter sein Bild-Zwilling, also noch das bisherige Modell.",
|
||||
)
|
||||
expect(rollenErsatzText("org/Neu-GGUF", "Coder", undefined)).toBe("Nach dem Download übernimmt org/Neu-GGUF die Rolle „Coder“.")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user