box-wart: Hirn gezielt pruefen, Laden meldet das echte Ergebnis, Radar-Suche als Auftrag

- Waechter (pruefe_kern): prueft jetzt gezielt das Modell mit der Rolle hermes und seinen
  Zustand in llama-swap (/running, v257 listet auch ladende Modelle), statt das Hirn fuer
  bereit zu halten, sobald irgendein Modell laeuft. Ein Ladevorgang ist kein Ausfall, aber
  nur bis 10 Minuten (MC_WAECHTER_HIRN_LADEN_S), sonst bliebe ein immer neu startendes Hirn
  unbemerkt. Sonst die ueblichen Takte (FAIL_AFTER); Knopf "Protokoll des Motors".
- llamaswap: modell_zustaende() und hirn_zustand(); lade_modell() meldet ok false mit
  deutschem Grund und der Meldung der Engine, "laedt noch" nach der Wartezeit ist kein Fehler,
  ein geladenes Modell ohne Chat (embed) zaehlt als geladen. POST /api/models/{id}/load
  reicht das Ergebnis durch (Router duenn).
- Radar "Jetzt suchen": startet die Suche als Auftrag (Job-Engine, Gruppe radar, hoechstens
  einer, radar_lauf.py --nur-suche) und antwortet sofort mit der Auftrags-ID. Die Ansicht
  zeigt "Sucht ..." bis der Auftrag fertig ist, liest danach das Radar neu und nennt eine
  gescheiterte Suche; die Auftragskarte zeigt den Stand.
- Tests: Hirn-Zustaende und Frist, Laden gegen eine llama-swap-Attrappe, Radar-Auftrag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 20:39:01 +02:00
co-authored by Claude Opus 5.5
parent 40d2840b16
commit 93f6613146
11 changed files with 462 additions and 35 deletions
+3 -2
View File
@@ -250,10 +250,11 @@ export const useRadarAktion = () =>
},
)
/** „Jetzt suchen“: Die Box startet die Suche als Auftrag (Gruppe „radar“) und antwortet sofort. */
export const useRadarSuche = () =>
useAktion(() => post<AktionsErgebnis>("/api/radar/suche"), {
erfolg: () => "Radar sucht nach neuen Modellen.",
neuLaden: ["radar"],
erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Das Radar sucht jetzt nach neuen Modellen.",
neuLaden: ["jobs"],
})
/** Wer liefert die Seite aus, und gibt es die zweite Instanz? (ändert sich nur mit einem Deploy) */
+27 -4
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { useLocation } from "@tanstack/react-router"
import { Search } from "lucide-react"
import { Auftraege } from "@/components/cockpit/Auftraege"
@@ -10,10 +11,11 @@ import {
useAufraeumen, useAufraeumenLoeschen, useJobs, useModellLaden, useModelle, useNutzung, useRadar, useRadarAktion,
useRadarSuche, useStart,
} from "@/lib/abfragen"
import { messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige"
import { laeuftNoch, messwert, RADAR_STATUS_TEXT, rolleVon } from "@/lib/anzeige"
import { ApiFehler } from "@/lib/api"
import { useLetzteMetrik } from "@/lib/strom"
import type { AufraeumKandidat, Modell, Nutzung, RadarKandidat, RadarTest } from "@/lib/typen"
import { letzteMeldung } from "@/lib/wartung"
import { flugplanZeit, gb, gbText, zahl } from "@/lib/zeit"
import { ModellSuche } from "./ModellSuche"
@@ -243,6 +245,21 @@ function Kandidat({ k, test, heute }: { k: RadarKandidat; test: RadarTest | unde
function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string | undefined }) {
const radar = useRadar()
const suche = useRadarSuche()
const jobs = useJobs()
const qc = useQueryClient()
// „Jetzt suchen“ läuft als Auftrag (Gruppe „radar“); der jüngste sagt, ob gerade gesucht wird. Bis die Liste
// der Aufträge den eben gestarteten kennt, gilt er als laufend (sonst sprang der Knopf kurz zurück).
const liste = jobs.data?.jobs
const auftrag = (liste ?? []).filter((j) => j.group === "radar").at(-1)
const gestartet = suche.data?.job_id
const nochNichtGelistet = !!gestartet && !!liste && !liste.some((j) => j.id === gestartet)
const sucht = suche.isPending || nochNichtGelistet || (auftrag !== undefined && laeuftNoch(auftrag))
// Ist die Suche eben fertig geworden, das Radar gleich neu lesen (sonst erst nach fünf Minuten).
const suchteEben = useRef(sucht)
useEffect(() => {
if (suchteEben.current && !sucht) qc.invalidateQueries({ queryKey: ["radar"] })
suchteEben.current = sucht
}, [sucht, qc])
if (radar.isError && !radar.data) {
// 404 heißt: Diese Box hat kein Radar. Alles andere ist ein Fehler, kein fehlendes Radar.
const fehlt = radar.error instanceof ApiFehler && radar.error.status === 404
@@ -263,15 +280,21 @@ function RadarPanel({ hirn, coder }: { hirn: string | undefined; coder: string |
id="radar"
className="scroll-mt-4"
rechts={
<Button variant="info" size="sm" onClick={() => suche.mutate(undefined)} disabled={suche.isPending}>
{suche.isPending ? "Sucht …" : "Jetzt suchen"}
<Button variant="info" size="sm" onClick={() => suche.mutate(undefined)} disabled={sucht}>
{sucht ? "Sucht …" : "Jetzt suchen"}
</Button>
}
>
<p className="-mt-2 text-sm text-text-2">
Testet nachts zwischen 00:30 und 02:30 selbst, höchstens einen Kandidaten pro Woche.
{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>
{!sucht && auftrag?.state === "failed" && (
<p role="alert" className="text-sm text-rot-text">
Die letzte Suche ist gescheitert: {letzteMeldung(auftrag) ?? "ohne Meldung"}
</p>
)}
{!d ? (
<p className="text-[15px] text-text-2">Wird gelesen …</p>
) : d.kandidaten.length === 0 ? (