radar: „Jetzt testen“ – Kandidaten auf Knopfdruck prüfen, wenn der Speicher reicht

User 25.09.: „Warum kann ich Modell-Tests nicht selbst anstoßen, sondern muss
immer auf die Box warten?“ Nachts testet das Radar weiter selbst. Tagsüber jetzt
auch per Knopf – aber nur, wenn Kandidat, Warm-Set UND der heutige Coder
zusammen unter die 115-GB-Grenze passen (der Test-Wächter stoppt den Kandidaten
erst bis zu 10 s, nachdem der Coder zu laden beginnt), und nicht 00:00–03:30.
Sonst steht der Grund beim Kandidaten. Test als Auftrag (radar_lauf.py --test),
höchstens 2 h, dieselben Leitplanken wie nachts; jedes Ergebnis kommt als Meldung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 13:16:39 +02:00
co-authored by Claude Opus 5.5
parent c31ef827fc
commit 44f33c31d3
31 changed files with 278 additions and 36 deletions
+7
View File
@@ -259,6 +259,13 @@ export const useRadarAktion = () =>
},
)
/** „Jetzt testen“ (seit 25.09.2026): der Test läuft als Auftrag (Gruppe „radar“), das Ergebnis kommt als Meldung. */
export const useRadarTest = () =>
useAktion(({ id }: { id: string; name: string }) => post<AktionsErgebnis>(`/api/radar/${encodeURIComponent(id)}/testen`), {
erfolg: (v, e) => (e as AktionsErgebnis).text ?? `Der Test von ${v.name} läuft.`,
neuLaden: ["radar", "jobs"],
})
/** „Jetzt suchen“: Die Box startet die Suche als Auftrag (Gruppe „radar“) und antwortet sofort. */
export const useRadarSuche = () =>
useAktion(() => post<AktionsErgebnis>("/api/radar/suche"), {
+3
View File
@@ -205,6 +205,9 @@ export interface RadarKandidat {
quelle: string
status: RadarStatus
begruendung: string
/** „Jetzt testen“ geht (seit 25.09.2026) — sonst steht in testbar_grund, warum nicht (null: gar nicht mehr). */
testbar_jetzt?: boolean
testbar_grund?: string | null
}
export interface RadarTest {
+41
View File
@@ -0,0 +1,41 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"
import { post } from "@/lib/api"
import type { RadarKandidat } from "@/lib/typen"
import { Kandidat } from "./Modelle"
vi.mock("@/lib/api", async (original) => ({ ...(await original<typeof import("@/lib/api")>()), post: vi.fn() }))
// Wie am 25.09.2026: Ornith passt tagsüber samt Coder, Flash-Next (84,6 GB) nur nachts.
const ornith: RadarKandidat = {
id: "ornith", name: "Ornith-1.5-35B-A3B", rolle: "hirn", repo: "org/ornith", datei: "o.gguf", groesse_gb: 22.6,
passt: true, eng: false, quelle: "watchlist", status: "neu", begruendung: "Aus der Merkliste.",
testbar_jetzt: true, testbar_grund: null,
}
function zeigen(k: RadarKandidat) {
return render(
<QueryClientProvider client={new QueryClient()}>
<ul><Kandidat k={k} test={undefined} heute="Qwen3.6-35B-A3B" /></ul>
</QueryClientProvider>,
)
}
describe("Radar: Jetzt testen (seit 25.09.2026)", () => {
it("fragt nach und startet den Test", async () => {
vi.mocked(post).mockResolvedValue({ ok: true, text: "Der Test läuft." })
zeigen(ornith)
fireEvent.click(screen.getByRole("button", { name: "Jetzt testen" }))
const dialog = screen.getByRole("dialog")
expect(within(dialog).getByText(/Lucy und NerdQuiz etwas langsamer/)).toBeInTheDocument()
fireEvent.click(within(dialog).getByRole("button", { name: "Jetzt testen" }))
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/radar/ornith/testen"))
})
it("sagt, warum es gerade nicht geht", () => {
zeigen({ ...ornith, id: "flash", name: "Qwen3.8-Flash-Next", status: "wartet", testbar_jetzt: false,
testbar_grund: "Zu groß für einen Test tagsüber: braucht ~84,6 GB, neben Warm-Set und Coder sind 59 GB frei." })
expect(screen.queryByRole("button", { name: "Jetzt testen" })).not.toBeInTheDocument()
expect(screen.getByText(/Zu groß für einen Test tagsüber/)).toBeInTheDocument()
})
})
+27 -4
View File
@@ -9,7 +9,7 @@ import { Panel } from "@/components/cockpit/Panel"
import { Button } from "@/components/ui/button"
import {
useAltreste, useAltrestLoeschen, useAufraeumen, useAufraeumenLoeschen, useJobs, useModellLaden, useModelle,
useNutzung, useRadar, useRadarAktion, useRadarSuche, useStart,
useNutzung, useRadar, useRadarAktion, useRadarSuche, useRadarTest, useStart,
} from "@/lib/abfragen"
import { laeuftNoch, messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige"
import { ApiFehler } from "@/lib/api"
@@ -177,9 +177,10 @@ function Vergleich({ test }: { test: RadarTest }) {
)
}
function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | undefined; heute: string | undefined }) {
export function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | undefined; heute: string | undefined }) {
const aktion = useRadarAktion()
const [frage, setFrage] = useState<null | "uebernehmen" | "verwerfen">(null)
const testen = useRadarTest()
const [frage, setFrage] = useState<null | "uebernehmen" | "verwerfen" | "testen">(null)
const offen = ["bestanden", "wartet", "neu", "getestet"].includes(k.status)
const hirn = k.rolle === "hirn"
return (
@@ -197,8 +198,16 @@ function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | unde
{k.begruendung}
</p>
{test && ["bestanden", "durchgefallen"].includes(k.status) && <Vergleich test={test} />}
{k.testbar_grund && !k.begruendung.startsWith("Läuft gerade") && (
<p className="text-xs text-text-3">{k.testbar_grund}</p>
)}
{offen && (
<div className="flex flex-wrap gap-2">
{k.testbar_jetzt && (
<Button variant="info" size="sm" disabled={testen.isPending} onClick={() => setFrage("testen")}>
Jetzt testen
</Button>
)}
{k.status === "bestanden" && (
<Button size="sm" disabled={aktion.isPending} onClick={() => setFrage("uebernehmen")}>
Übernehmen
@@ -224,6 +233,19 @@ function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | unde
setFrage(null)
}}
/>
<Bestaetigen
offen={frage === "testen"}
titel={`${k.name} jetzt testen?`}
text={`Das Radar lädt ${k.name}${k.groesse_gb != null ? ` (${k.groesse_gb.toLocaleString("de-DE")} GB)` : ""} herunter, falls nötig, misst das heutige Modell der Rolle und dann den Kandidaten. Das dauert je nach Download eine halbe bis zwei Stunden. Solange antworten Lucy und NerdQuiz etwas langsamer; ein geladener Coder wird dafür entladen, und wird er währenddessen gebraucht, bricht der Test ab. Das Ergebnis kommt als Meldung; fällt der Kandidat durch, werden seine Dateien gelöscht.`}
knopf="Jetzt testen"
gesperrt={testen.isPending}
onNein={() => setFrage(null)}
onJa={() => {
if (frage !== "testen") return
testen.mutate({ id: k.id, name: k.name })
setFrage(null)
}}
/>
<Bestaetigen
offen={frage === "verwerfen"}
gefahr
@@ -286,7 +308,8 @@ function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string |
}
>
<p className="-mt-2 text-sm text-text-2">
Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche.
Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche. Tagsüber geht es mit
„Jetzt testen“, wenn der Kandidat samt Coder in den Speicher passt.
{d?.naechster_test ? ` Nächster Test: ${flugplanZeit(d.naechster_test)}.` : ""}
{sucht ? " Sucht gerade bei Hugging Face. Das dauert ein paar Minuten; den Stand zeigt der Auftrag oben unter „Aufträge“." : ""}
</p>