boxwart: Modell-Radar sucht, testet nachts selbst und empfiehlt (Hirn und Coder)
Ampel / ampel (push) Failing after 21s

Suche aus Merkliste (deploy/radar-watchlist.json) und Hugging-Face-Entdeckung; nur
Kandidaten mit Bild-Projektor, die neben das Warm-Set passen (<= 115 GB inkl. KV-Cache).
Nachtlauf mc2-radar.timer 00:30, Tests nur bis 02:30 (um 03:00 kommt NerdQuiz),
hoechstens ein neuer Kandidat pro Woche, Notbremse 02:35, RuntimeMaxSec als letzte
Sicherung. Pruefstand als Modul (deploy/bench/pruefstand.py): Tempo, Werkzeuge,
Deutsch/JSON bzw. Programmieraufgaben und Bild-Probe gegen das heutige Modell der Rolle.
Durchgefallene werden geloescht, Bestandene gemeldet und erst nach "Uebernehmen" getauscht.

Beim Uebernehmen wandern die Zweitrollen mit (fast beim Hirn, heavy beim Coder), und das
Warm-Set des Stewards wird selbst umgestellt statt als Handgriff zu bleiben. Modell-Code
im Pruefstand darf keine Prozesse starten (RLIMIT_NPROC=0). Radar steht im Flugplan und
unter Waechter-Aufsicht; deploy.sh spielt seine Units ein. Oberflaeche: Vergleichswerte
je Kandidat, Rueckfrage vor Uebernehmen und Verwerfen, Status auf Deutsch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-23 22:29:38 +02:00
co-authored by Claude Opus 5.5
parent 4b226d5d22
commit 177c9a311c
30 changed files with 3511 additions and 194 deletions
+74 -11
View File
@@ -1,14 +1,15 @@
import { useState } from "react"
import { Search } from "lucide-react"
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
import { Panel } from "@/components/cockpit/Panel"
import { Button } from "@/components/ui/button"
import {
useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion, useRadarSuche, useStart,
} from "@/lib/abfragen"
import { rolleVon } from "@/lib/anzeige"
import { messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige"
import { useLetzteMetrik } from "@/lib/strom"
import type { Modell, Nutzung, RadarKandidat } from "@/lib/typen"
import type { Modell, Nutzung, RadarKandidat, RadarTest } from "@/lib/typen"
import { flugplanZeit, gb, gbText, zahl } from "@/lib/zeit"
import { ModellSuche } from "./ModellSuche"
@@ -122,39 +123,94 @@ function NutzungsPanel({ nutzung }: { nutzung: Nutzung | undefined }) {
const STATUS_ART: Record<string, ChipArt> = {
bestanden: "gruen", durchgefallen: "rot", wartet: "cyan", neu: "cyan", getestet: "grau", uebernommen: "gruen", verworfen: "grau",
}
const URTEIL_FARBE: Record<string, string> = { besser: "text-gruen", schlechter: "text-rot-text", gleich: "text-text-2" }
function Kandidat({ k }: { k: RadarKandidat }) {
type Vergleichszeile = { kandidat: unknown; baseline: unknown; einheit?: string; urteil?: string }
/** Die Messwerte hinter dem Urteil: Kandidat gegen das heutige Modell der Rolle. */
function Vergleich({ test }: { test: RadarTest }) {
const zeilen = Object.entries(test.vergleich ?? {}) as [string, Vergleichszeile][]
if (zeilen.length === 0) return null
return (
<div role="table" aria-label="Messwerte gegen das heutige Modell" className="rounded-lg border border-linie bg-erhaben/50 px-3 py-2 text-sm">
<div role="row" className="schild grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 pb-1 text-xs text-text-3">
<span role="columnheader">Messwert</span>
<span role="columnheader" className="text-right">Kandidat</span>
<span role="columnheader" className="text-right">Heute</span>
</div>
{zeilen.map(([name, z]) => (
<div key={name} role="row" className="grid grid-cols-[minmax(0,1fr)_auto_auto] gap-x-4 border-t border-linie py-1">
<span role="rowheader" className="text-text-2">{name}</span>
<span role="cell" className={`ziffern text-right ${URTEIL_FARBE[z.urteil ?? ""] ?? "text-text-2"}`}>
{messwert(z.kandidat, z.einheit)}
</span>
<span role="cell" className="ziffern text-right text-text-3">{messwert(z.baseline, z.einheit)}</span>
</div>
))}
</div>
)
}
function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | undefined; heute: string | undefined }) {
const aktion = useRadarAktion()
const offen = ["bestanden", "wartet", "neu"].includes(k.status)
const [frage, setFrage] = useState<null | "uebernehmen" | "verwerfen">(null)
const offen = ["bestanden", "wartet", "neu", "getestet"].includes(k.status)
const hirn = k.rolle === "hirn"
return (
<li className="flex flex-col gap-2 border-t border-linie py-3.5">
<div className="flex flex-wrap items-center gap-2">
<span className="ziffern text-[15px] font-medium">{k.name}</span>
<Chip art="cyan">{k.rolle === "hirn" ? "fürs Hirn" : "fürs Coden"}</Chip>
<Chip art="cyan">{hirn ? "fürs Hirn" : "fürs Coden"}</Chip>
<Chip art={k.eng ? "bernstein" : k.passt ? "gruen" : "rot"}>{k.eng ? "passt knapp" : k.passt ? "passt" : "passt nicht"}</Chip>
<Chip art={STATUS_ART[k.status] ?? "grau"}>{k.status}</Chip>
<Chip art={STATUS_ART[k.status] ?? "grau"}>{RADAR_STATUS_TEXT[k.status] ?? k.status}</Chip>
</div>
<p className="text-sm text-text-2">
{k.groesse_gb != null ? `${k.groesse_gb.toLocaleString("de-DE")} GB. ` : ""}
{k.begruendung}
</p>
{test && ["bestanden", "durchgefallen"].includes(k.status) && <Vergleich test={test} />}
{offen && (
<div className="flex flex-wrap gap-2">
{k.status === "bestanden" && (
<Button size="sm" disabled={aktion.isPending} onClick={() => aktion.mutate({ id: k.id, aktion: "uebernehmen", name: k.name })}>
<Button size="sm" disabled={aktion.isPending} onClick={() => setFrage("uebernehmen")}>
Übernehmen
</Button>
)}
<Button variant="outline" size="sm" disabled={aktion.isPending} onClick={() => aktion.mutate({ id: k.id, aktion: "verwerfen", name: k.name })}>
<Button variant="outline" size="sm" disabled={aktion.isPending} onClick={() => setFrage("verwerfen")}>
Verwerfen
</Button>
</div>
)}
<Bestaetigen
offen={frage === "uebernehmen"}
titel={`${k.name} übernehmen?`}
text={hirn
? `${k.name} wird Lucys Hirn${heute ? ` und löst ${heute} ab` : ""}. NerdQuiz und OpenChamber nutzen ab dann ebenfalls das neue Modell. Hermes startet dafür kurz neu, Lucy ist etwa eine Minute nicht erreichbar. Das alte Modell bleibt auf der Platte.`
: `${k.name} wird der Coder${heute ? ` und löst ${heute} ab` : ""}. OpenChamber plant und baut ab dann damit. Das alte Modell bleibt auf der Platte.`}
knopf="Übernehmen"
onNein={() => setFrage(null)}
onJa={() => {
aktion.mutate({ id: k.id, aktion: "uebernehmen", name: k.name })
setFrage(null)
}}
/>
<Bestaetigen
offen={frage === "verwerfen"}
gefahr
titel={`${k.name} verwerfen?`}
text="Die heruntergeladenen Dateien werden gelöscht, und das Radar testet dieses Modell nicht noch einmal."
knopf="Verwerfen"
onNein={() => setFrage(null)}
onJa={() => {
aktion.mutate({ id: k.id, aktion: "verwerfen", name: k.name })
setFrage(null)
}}
/>
</li>
)
}
function RadarPanel() {
function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string | undefined }) {
const radar = useRadar()
const suche = useRadarSuche()
if (radar.isError) {
@@ -181,7 +237,14 @@ function RadarPanel() {
<p className="text-[15px] text-text-2">Nichts Besseres in Sicht.</p>
) : (
<ul className="m-0 flex list-none flex-col p-0">
{d.kandidaten.map((k) => <Kandidat key={k.id} k={k} />)}
{d.kandidaten.map((k) => (
<Kandidat
key={k.id}
k={k}
test={d.tests.find((t) => t.kandidat === k.id)}
heute={k.rolle === "hirn" ? hirn : coder}
/>
))}
</ul>
)}
{d && d.tests.length > 0 && (
@@ -223,7 +286,7 @@ export function ModelleSeite() {
</section>
<div className="grid gap-5 md:gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)]">
<NutzungsPanel nutzung={nutzung.data} />
<RadarPanel />
<RadarPanel hirn={hirn?.name} coder={coder?.name} />
</div>
<Panel
titel="Weitere Einträge"