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:
co-authored by
Claude Opus 5.5
parent
c31ef827fc
commit
44f33c31d3
+43
-1
@@ -135,5 +135,47 @@ def nur_suche() -> int:
|
||||
return 0 if any(quellen.values()) else 1
|
||||
|
||||
|
||||
def melde_ergebnis(test: dict) -> None:
|
||||
"""Nach „Jetzt testen“ kommt jedes Ergebnis als Meldung — der Commander hat den Test selbst angestoßen."""
|
||||
if test.get("ergebnis") == "bestanden":
|
||||
melde_bestanden(test)
|
||||
return
|
||||
k = radar.lade_stand()["kandidaten"].get(test.get("kandidat")) or {}
|
||||
text = f"Test von {k.get('name') or test.get('kandidat')}: {str(test.get('zusammenfassung') or test.get('ergebnis'))[:400]}"
|
||||
try:
|
||||
subprocess.run(["bash", str(NOTIFY), "-s", "[Modell-Radar]", text], timeout=120, check=False,
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
log.warning("Radar: Meldung zum Test ging nicht raus.", exc_info=True)
|
||||
|
||||
|
||||
def test_auf_knopfdruck(kid: str) -> int:
|
||||
"""„Jetzt testen“ (seit 25.09.2026, services/radar.test_starten): ein Kandidat, jetzt, mit eigener Frist von
|
||||
TAGTEST_H Stunden. Dieselben Leitplanken wie nachts: nur Warm-Set neben dem Kandidaten, Test-Wächter, Notbremse.
|
||||
Ob der Kandidat tagsüber überhaupt darf (Speicher mit Coder, Sperrzeit), hat test_starten geprüft; hier noch
|
||||
einmal, falls zwischen Knopf und Start Zeit verging."""
|
||||
k = radar.lade_stand()["kandidaten"].get(kid)
|
||||
testbar, grund = radar.jetzt_testbar(k or {})
|
||||
if not k or not testbar:
|
||||
print(f"Kein Test: {grund or 'den Kandidaten gibt es nicht mehr'}.", flush=True)
|
||||
return 1
|
||||
frist = time.time() + radar.TAGTEST_H * 3600
|
||||
signal.signal(signal.SIGTERM, _beim_beenden)
|
||||
bremse = _notbremse(frist)
|
||||
try:
|
||||
test = radar.teste(kid, frist, auf_knopfdruck=True)
|
||||
if test:
|
||||
print(f"{k.get('name')}: {test.get('ergebnis')} – {test.get('zusammenfassung')}", flush=True)
|
||||
melde_ergebnis(test)
|
||||
return 0
|
||||
finally:
|
||||
bremse.cancel()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(nur_suche() if "--nur-suche" in sys.argv[1:] else main())
|
||||
argumente = sys.argv[1:]
|
||||
if "--nur-suche" in argumente:
|
||||
sys.exit(nur_suche())
|
||||
if "--test" in argumente and argumente.index("--test") + 1 < len(argumente):
|
||||
sys.exit(test_auf_knopfdruck(argumente[argumente.index("--test") + 1]))
|
||||
sys.exit(main())
|
||||
|
||||
@@ -5,8 +5,9 @@ Modell-Radar-Schnittstellen (Box-Wart, Umbau 09/2026).
|
||||
POST /api/radar/suche Jetzt suchen (Merkliste + Hugging-Face-Entdeckung) — als Auftrag, antwortet sofort
|
||||
POST /api/radar/{id}/uebernehmen Bestandenen Kandidaten in Betrieb nehmen (Modell-Tausch)
|
||||
POST /api/radar/{id}/verwerfen Kandidaten verwerfen (Dateien weg, nie wieder testen)
|
||||
POST /api/radar/{id}/testen Jetzt testen (seit 25.09.2026; nur wenn er samt Coder in den Speicher passt)
|
||||
|
||||
Dünn: die Logik liegt in services/radar.py. Getestet wird nur nachts (backend/radar_lauf.py).
|
||||
Dünn: die Logik liegt in services/radar.py. Getestet wird nachts (backend/radar_lauf.py) oder auf Knopfdruck.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
@@ -38,6 +39,13 @@ def radar_uebernehmen(kandidat_id: str) -> dict:
|
||||
return _antwort(radar.uebernehmen(kandidat_id))
|
||||
|
||||
|
||||
@router.post("/radar/{kandidat_id}/testen")
|
||||
def radar_testen(kandidat_id: str) -> dict:
|
||||
"""„Jetzt testen“ (seit 25.09.2026): nur, wenn der Kandidat samt Coder neben das Warm-Set passt und nicht in
|
||||
der Nacht-Sperre — sonst 409 mit Grund. Antwortet sofort mit der Auftrags-ID."""
|
||||
return _antwort(radar.test_starten(kandidat_id))
|
||||
|
||||
|
||||
@router.post("/radar/{kandidat_id}/verwerfen")
|
||||
def radar_verwerfen(kandidat_id: str) -> dict:
|
||||
return _antwort(radar.verwerfen(kandidat_id))
|
||||
|
||||
@@ -86,6 +86,10 @@ MIN_PARAMS_B = float(os.environ.get("MC_RADAR_MIN_PARAMS_B", "20"))
|
||||
HIRN_MAX_AKTIV_B = float(os.environ.get("MC_RADAR_HIRN_MAX_AKTIV_B", "12")) # Verdikt: dichte Modelle nie als Hirn
|
||||
ABGELEHNT_TAGE = 30
|
||||
TESTS_MAX = 100
|
||||
# „Jetzt testen“ (User 25.09.2026: „Warum kann ich Modell-Tests nicht selbst anstoßen?“): so lange darf ein Test auf
|
||||
# Knopfdruck dauern, und in dieser Zeit ist er gesperrt (Nachtlauf des Radars, um 03:00 NerdQuiz).
|
||||
TAGTEST_H = float(os.environ.get("MC_RADAR_TAGTEST_H", "2"))
|
||||
KNOPF_GESPERRT = (os.environ.get("MC_RADAR_KNOPF_SPERRE_VON", "00:00"), os.environ.get("MC_RADAR_KNOPF_SPERRE_BIS", "03:30"))
|
||||
|
||||
STORE_PATH = Path(os.environ.get("MC_RADAR_STORE", str(MODELS_DIR / "mc2-radar.json")))
|
||||
BASELINE_PATH = Path(os.environ.get("MC_RADAR_BASELINE", str(MODELS_DIR / "mc2-radar-baseline.json")))
|
||||
@@ -1191,9 +1195,11 @@ def _draft_text(wahl: dict) -> str:
|
||||
f" (13k: {'; '.join(tempi)})." if len(tempi) > 1 else ".")
|
||||
|
||||
|
||||
def teste(kid: str, frist: float) -> dict | None:
|
||||
def teste(kid: str, frist: float, auf_knopfdruck: bool = False) -> dict | None:
|
||||
"""Einen Kandidaten prüfen: Download → Baseline → nur Warm-Set in llama-swap → Kandidat auf :5899 →
|
||||
Urteil. Gibt den Test-Eintrag zurück (None, wenn nur gemessen wurde oder es den Kandidaten nicht gibt)."""
|
||||
Urteil. Gibt den Test-Eintrag zurück (None, wenn nur gemessen wurde oder es den Kandidaten nicht gibt).
|
||||
auf_knopfdruck: gestartet mit „Jetzt testen“ (test_starten) statt im Nachtlauf — nur der Text zum Zeitende
|
||||
unterscheidet sich; die Leitplanken (Warm-Set, Test-Wächter, Frist) sind dieselben."""
|
||||
ps = _pruefstand()
|
||||
k = _lade()["kandidaten"].get(kid)
|
||||
if not k or k.get("status") not in ("neu", "wartet"): # inzwischen verworfen o. Ä.
|
||||
@@ -1226,7 +1232,8 @@ def teste(kid: str, frist: float) -> dict | None:
|
||||
werte["baseline"] = {"modell": (basis.get("werte") or {}).get("modell"), "gemessen": _iso(basis.get("zeit"))}
|
||||
ergebnis, satz = lauf["ergebnis"], lauf["zusammenfassung"] + _draft_text(wahl)
|
||||
except ps.Zeitende:
|
||||
satz = f"Das Nachtfenster endete um {FENSTER_ENDE}, bevor der Test fertig war."
|
||||
satz = (f"Die {TAGTEST_H:g} Stunden für den Test auf Knopfdruck waren um, bevor er fertig war."
|
||||
if auf_knopfdruck else f"Das Nachtfenster endete um {FENSTER_ENDE}, bevor der Test fertig war.")
|
||||
except ps.ServerFehler as e:
|
||||
if "unknown model architecture" in str(e).lower():
|
||||
ergebnis, satz = "durchgefallen", f"Durchgefallen: läuft nicht im offiziellen llama.cpp ({str(e)[:160]})."
|
||||
@@ -1446,8 +1453,59 @@ def _tausche_ein(k: dict) -> dict:
|
||||
return {"modell_id": modell_id, "pfad": str(ziel), "alt": (alt or {}).get("name"), "hinweis": hinweis}
|
||||
|
||||
|
||||
def _oeffentlich(k: dict) -> dict:
|
||||
return {f: k.get(f) for f in KANDIDAT_FELDER}
|
||||
def _oeffentlich(k: dict, zeit: datetime | None = None) -> dict:
|
||||
testbar, grund = jetzt_testbar(k, zeit)
|
||||
return {**{f: k.get(f) for f in KANDIDAT_FELDER}, "testbar_jetzt": testbar, "testbar_grund": grund}
|
||||
|
||||
|
||||
def _in_knopf_sperre(zeit: datetime) -> bool:
|
||||
von, bis = (uhrzeit(*(int(x) for x in t.split(":"))) for t in KNOPF_GESPERRT)
|
||||
uhr = zeit.astimezone(LOCAL_TZ).time()
|
||||
return von <= uhr < bis if von <= bis else (uhr >= von or uhr < bis)
|
||||
|
||||
|
||||
def jetzt_testbar(k: dict, zeit: datetime | None = None) -> tuple[bool, str | None]:
|
||||
"""Darf dieser Kandidat jetzt mit „Jetzt testen“ geprüft werden — und wenn nicht, warum?
|
||||
Tagsüber kann jederzeit der Coder geladen werden (OpenChamber). Der Test-Wächter stoppt den Kandidaten dann zwar
|
||||
(_Nachtwaechter), aber erst nach bis zu zehn Sekunden; so lange stünden Warm-Set, Kandidat UND Coder im Speicher.
|
||||
Also nur, wenn alle drei zusammen unter die Speichergrenze passen. Größere testet das Radar nachts."""
|
||||
if k.get("status") not in ("neu", "wartet"):
|
||||
return False, None
|
||||
bedarf = float(k.get("bedarf_gb") or 0)
|
||||
frei = SPEICHER_GRENZE_GB - WARMSET_GB - CODER_HEUTE_GB
|
||||
if not k.get("passt") or not bedarf:
|
||||
return False, "Ob er in den Speicher passt, ist nicht bekannt — das Radar testet ihn nachts."
|
||||
if bedarf > frei:
|
||||
return False, (f"Zu groß für einen Test tagsüber: braucht ~{_gb(bedarf)} GB, neben Warm-Set und Coder sind "
|
||||
f"{_gb(frei)} GB frei. Das Radar testet ihn nachts.")
|
||||
if _in_knopf_sperre(zeit or jetzt()):
|
||||
return False, (f"Zwischen {KNOPF_GESPERRT[0]} und {KNOPF_GESPERRT[1]} testet das Radar selbst, und um 03:00 "
|
||||
"braucht NerdQuiz das Hirn — danach geht es auf Knopfdruck.")
|
||||
return True, None
|
||||
|
||||
|
||||
def test_starten(kid: str, zeit: datetime | None = None) -> dict:
|
||||
"""„Jetzt testen“: den Test als Auftrag starten (Gruppe „radar“, höchstens einer zugleich; radar_lauf.py --test).
|
||||
Antwortet sofort; den Stand zeigen Auftragskarte und Kandidat („Läuft gerade seit …“)."""
|
||||
stand = _lade()
|
||||
k = stand["kandidaten"].get(kid)
|
||||
if not k:
|
||||
return {"ok": False, "status": 404, "detail": "Diesen Kandidaten gibt es nicht."}
|
||||
testbar, grund = jetzt_testbar(k, zeit)
|
||||
if not testbar:
|
||||
return {"ok": False, "status": 409, "detail": grund or f"{k.get('name')} lässt sich nicht testen "
|
||||
f"(Status: {k.get('status')})."}
|
||||
if _aktiver_lauf(stand):
|
||||
return {"ok": False, "status": 409, "detail": "Es läuft schon ein Test des Radars."}
|
||||
job_id, laeuft = jobengine.start_job_exklusiv(
|
||||
"radar", [sys.executable, str(_LAUF_SKRIPT), "--test", kid], f"Modell-Radar: Test von {k.get('name')}",
|
||||
zeitlimit_s=int(TAGTEST_H * 3600) + 900)
|
||||
if job_id is None:
|
||||
return {"ok": False, "status": 409,
|
||||
"detail": f"Das Radar ist gerade beschäftigt: {(laeuft or {}).get('label') or 'ein Auftrag'}."}
|
||||
return {"ok": True, "job_id": job_id,
|
||||
"text": f"Der Test von {k.get('name')} läuft. Er dauert je nach Download etwa eine halbe bis zwei Stunden; "
|
||||
"das Ergebnis kommt als Meldung."}
|
||||
|
||||
|
||||
def uebernehmen(kid: str) -> dict:
|
||||
@@ -1523,7 +1581,7 @@ def uebersicht(zeit: datetime | None = None) -> dict:
|
||||
kandidaten = sorted(stand["kandidaten"].values(),
|
||||
key=lambda k: (_REIHENFOLGE.get(k.get("status"), 9), 0 if k.get("quelle") == "watchlist" else 1,
|
||||
k.get("rang", 999)))
|
||||
oeffentlich = [_oeffentlich(k) for k in kandidaten]
|
||||
oeffentlich = [_oeffentlich(k, zeit) for k in kandidaten]
|
||||
if (lauf := _aktiver_lauf(stand)): # den laufenden Test in seiner Begründung zeigen (nicht gespeichert)
|
||||
for k in oeffentlich:
|
||||
if k["id"] == lauf.get("kandidat"):
|
||||
|
||||
@@ -336,7 +336,8 @@ def test_uebersicht_hat_die_felder_der_oberflaeche(zustand):
|
||||
radar._speichere(stand)
|
||||
u = radar.uebersicht(_zeit("2026-10-01T12:00:00"))
|
||||
assert set(u) == {"naechster_test", "letzter_test", "kandidaten", "tests"}
|
||||
assert set(u["kandidaten"][0]) == set(radar.KANDIDAT_FELDER)
|
||||
# Dazu seit 25.09.2026, ob „Jetzt testen“ geht und warum nicht.
|
||||
assert set(u["kandidaten"][0]) == set(radar.KANDIDAT_FELDER) | {"testbar_jetzt", "testbar_grund"}
|
||||
assert [k["status"] for k in u["kandidaten"]] == ["wartet", "bestanden"]
|
||||
assert u["naechster_test"] == "2026-10-06T00:30:00+02:00" # die Woche ab dem 29.09. ist belegt
|
||||
assert u["letzter_test"].startswith("2026-09-29T01:40")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
in radar_lauf.py --nur-suche. Vorher lief die Suche synchron in der Anfrage und konnte Minuten dauern."""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import radar_lauf
|
||||
@@ -75,3 +76,54 @@ def test_nur_suche_bei_absturz(monkeypatch, capsys):
|
||||
monkeypatch.setattr(radar_lauf.radar, "suche", kaputt)
|
||||
assert radar_lauf.nur_suche() == 1
|
||||
assert capsys.readouterr().out.strip().endswith("Die Suche ist gescheitert: OSError: Platte voll")
|
||||
|
||||
|
||||
# --- „Jetzt testen“ (seit 25.09.2026) ------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def radarstand(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(radar, "STORE_PATH", tmp_path / "mc2-radar.json")
|
||||
monkeypatch.setattr(radar, "RADAR_DIR", tmp_path / "radar")
|
||||
(tmp_path / "radar").mkdir()
|
||||
stand = radar._leer()
|
||||
# Zahlen vom 25.09.: Ornith (~27 GB mit KV-Cache) passt tagsüber, Flash-Next (84,6 GB) nur nachts.
|
||||
stand["kandidaten"]["ornith"] = {"id": "ornith", "name": "Ornith-1.5-35B-A3B", "rolle": "hirn", "status": "neu",
|
||||
"passt": True, "eng": False, "bedarf_gb": 27.4, "groesse_gb": 22.6}
|
||||
stand["kandidaten"]["flash"] = {"id": "flash", "name": "Qwen3.8-Flash-Next", "rolle": "coder", "status": "wartet",
|
||||
"passt": True, "eng": True, "bedarf_gb": 84.6, "groesse_gb": 82.9}
|
||||
stand["kandidaten"]["alt"] = {"id": "alt", "name": "Alt", "rolle": "hirn", "status": "durchgefallen",
|
||||
"passt": True, "bedarf_gb": 20.0}
|
||||
radar._speichere(stand)
|
||||
return stand
|
||||
|
||||
|
||||
MITTAG = datetime(2026, 9, 25, 13, 0, tzinfo=radar.LOCAL_TZ)
|
||||
NACHT = datetime(2026, 9, 26, 1, 15, tzinfo=radar.LOCAL_TZ)
|
||||
|
||||
|
||||
def test_jetzt_testbar_nur_wenn_auch_der_coder_noch_passt(radarstand):
|
||||
k = radarstand["kandidaten"]
|
||||
assert radar.jetzt_testbar(k["ornith"], MITTAG) == (True, None)
|
||||
testbar, grund = radar.jetzt_testbar(k["flash"], MITTAG)
|
||||
assert not testbar and grund.startswith("Zu groß für einen Test tagsüber: braucht ~84,6 GB")
|
||||
assert radar.jetzt_testbar(k["alt"], MITTAG) == (False, None) # schon beurteilt
|
||||
testbar, grund = radar.jetzt_testbar(k["ornith"], NACHT)
|
||||
assert not testbar and "testet das Radar selbst" in grund
|
||||
oeffentlich = {x["id"]: x for x in radar.uebersicht(MITTAG)["kandidaten"]}
|
||||
assert oeffentlich["ornith"]["testbar_jetzt"] is True and oeffentlich["flash"]["testbar_jetzt"] is False
|
||||
|
||||
|
||||
def test_jetzt_testen_startet_einen_auftrag(radarstand, auftraege, monkeypatch):
|
||||
monkeypatch.setattr(radar, "jetzt", lambda: MITTAG)
|
||||
ergebnis = radar.test_starten("ornith")
|
||||
assert ergebnis["ok"] and ergebnis["job_id"] == "job123"
|
||||
start = auftraege["gestartet"][0]
|
||||
assert start["gruppe"] == "radar" and start["args"][1:] == [str(radar._LAUF_SKRIPT), "--test", "ornith"]
|
||||
assert start["label"] == "Modell-Radar: Test von Ornith-1.5-35B-A3B"
|
||||
zu_gross = radar.test_starten("flash")
|
||||
assert zu_gross["ok"] is False and zu_gross["status"] == 409 and "Zu groß" in zu_gross["detail"]
|
||||
assert radar.test_starten("gibt-es-nicht")["status"] == 404
|
||||
app = FastAPI()
|
||||
app.include_router(radar_router.router)
|
||||
antwort = TestClient(app).post("/api/radar/flash/testen")
|
||||
assert antwort.status_code == 409 and "Zu groß" in antwort.json()["detail"]
|
||||
|
||||
@@ -21,6 +21,13 @@ Merkliste `deploy/radar-watchlist.json`. Zustand: `/srv/models/mc2-radar.json`,
|
||||
- Zwei Rollen: Hirn (Alias `hermes`) und Coder (Alias `coder`), jede mit Bild-Zwilling (`vision` bzw. `coder-bild`).
|
||||
- Tests nur nachts 00:30–02:30, höchstens ein neuer Kandidat pro Woche. Um 03:00 braucht der NerdQuiz-Nachtlauf das
|
||||
Hirn.
|
||||
- **Jetzt testen** (seit 25.09.2026, User-Wunsch): Auf der Modelle-Seite hat jeder wartende Kandidat den Knopf, wenn er
|
||||
samt heutigem Coder neben das Warm-Set passt (Bedarf + 29 GB Coder ≤ 88 GB, also ~59 GB) und es nicht zwischen 00:00
|
||||
und 03:30 ist. Grund: Tagsüber kann jederzeit der Coder laden; der Test-Wächter stoppt den Kandidaten dann, aber
|
||||
erst nach bis zu zehn Sekunden — so lange müssen alle drei in den Speicher passen. Sonst steht der Grund beim
|
||||
Kandidaten, und er wartet auf die Nacht. Der Test läuft als Auftrag (`radar_lauf.py --test <id>`, Gruppe „radar“),
|
||||
höchstens zwei Stunden, mit denselben Leitplanken wie nachts; jedes Ergebnis kommt als Meldung. Er zählt für die
|
||||
Wochengrenze mit (`erster_start`). `POST /api/radar/{id}/testen`, 409 mit Grund, wenn es nicht geht.
|
||||
- Nur was neben das Warm-Set passt: 27 GB Warm-Set plus Kandidat mit KV-Cache ≤ 115 GB, gerechnet mit dem Kontext
|
||||
aus dem Betrieb (131 072; passt das nicht, stufenweise kleiner, nie unter 32 768). Ab 90 % des Budgets heißt es
|
||||
„passt knapp".
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{i as e,n as t,r as n,s as r,t as i}from"./dist-D4CVSex9.js";import{$t as a,Ht as o,I as s,Nt as c,kt as l,rt as u}from"./index-DzdbFvQV.js";import{t as d}from"./button-D8rvET0Z.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-BReEHJc5.js";var _=t(),v=r(e(),1),y=n();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=u(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:t}){let n=a(),r=s(),[u,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await o(`/api/maintenance/restart`,{service:e.unit});t.ok?l(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):l(`fehler`,`${e.name} ließ sich nicht starten: ${c(t)}`),n.invalidateQueries({queryKey:[`dienste`]})}catch(e){l(`fehler`,e.message)}finally{w(null)}}}let E;return E=r.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):r.isError&&!r.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,r.error.message]}):r.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:r.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:i(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(d,{variant:u===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":u===e.unit,onClick:()=>_(u===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(d,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(d,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),u===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&t(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste};
|
||||
import{i as e,n as t,r as n,s as r,t as i}from"./dist-D4CVSex9.js";import{At as a,I as o,Pt as s,Ut as c,en as l,rt as u}from"./index-21ZM2s1S.js";import{t as d}from"./button-D8rvET0Z.js";import{a as f,i as p,n as m,r as h,t as g}from"./sheet-CgwkrC7e.js";var _=t(),v=r(e(),1),y=n();function b(e){let t=(0,_.c)(4),{unit:n}=e,r=u(n),i;if(r.isPending)i=`Wird gelesen …`;else if(r.isError)i=`Das Protokoll lässt sich gerade nicht lesen: ${r.error.message}`;else if(r.data.ok===!1)i=r.data.err||r.data.text||`Das Protokoll lässt sich nicht lesen.`;else{let e;t[0]===r.data.text?e=t[1]:(e=r.data.text?.trim()||`Noch keine Einträge.`,t[0]=r.data.text,t[1]=e),i=e}let a;return t[2]===i?a=t[3]:(a=(0,y.jsx)(`pre`,{className:`ziffern m-0 max-h-[50vh] w-full basis-full overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap break-all text-text-2`,children:i}),t[2]=i,t[3]=a),a}function x({offen:e,onSchliessen:t}){let n=l(),r=o(),[u,_]=(0,v.useState)(null),[x,S]=(0,v.useState)(null),[C,w]=(0,v.useState)(null);async function T(e){if(!C){S(null),w(e.unit);try{let t=await c(`/api/maintenance/restart`,{service:e.unit});t.ok?a(`erfolg`,`${e.name} ${e.schlaeft?`wird geweckt`:`startet neu`}.`):a(`fehler`,`${e.name} ließ sich nicht starten: ${s(t)}`),n.invalidateQueries({queryKey:[`dienste`]})}catch(e){a(`fehler`,e.message)}finally{w(null)}}}let E;return E=r.isPending?(0,y.jsx)(`p`,{role:`status`,className:`px-4 text-[15px] text-text-2`,children:`Dienste werden gelesen …`}):r.isError&&!r.data?(0,y.jsxs)(`p`,{role:`alert`,className:`px-4 text-[15px] text-rot-text`,children:[`Die Dienste lassen sich gerade nicht lesen: `,r.error.message]}):r.data?.services.length?(0,y.jsx)(`ul`,{className:`m-0 flex list-none flex-col px-4 pb-4`,children:r.data.services.map(e=>(0,y.jsxs)(`li`,{className:`flex flex-wrap items-center justify-between gap-2 border-t border-linie py-2.5`,children:[(0,y.jsxs)(`span`,{className:`flex min-w-0 items-center gap-2.5`,children:[(0,y.jsx)(`span`,{className:i(`size-2.5 shrink-0 rounded-full`,e.ok?`bg-gruen`:e.schlaeft?`bg-text-3`:`bg-rot`),"aria-hidden":!0}),(0,y.jsxs)(`span`,{className:`min-w-0`,children:[(0,y.jsx)(`span`,{className:`block text-[15px]`,children:e.name}),(0,y.jsxs)(`span`,{className:`ziffern text-xs text-text-3`,children:[e.unit,` · `,e.ok?`läuft`:e.schlaeft?`schläft (abgeschaltet)`:`antwortet nicht`]})]})]}),(0,y.jsxs)(`span`,{className:`flex flex-wrap gap-2`,children:[(0,y.jsx)(d,{variant:u===e.unit?`info`:`ghost`,size:`sm`,"aria-expanded":u===e.unit,onClick:()=>_(u===e.unit?null:e.unit),children:`Protokoll`}),x===e.unit?(0,y.jsxs)(y.Fragment,{children:[(0,y.jsx)(d,{variant:`gefahr`,size:`sm`,onClick:()=>T(e),children:e.schlaeft?`Wirklich wecken?`:`Wirklich neu starten?`}),(0,y.jsx)(d,{variant:`ghost`,size:`sm`,onClick:()=>S(null),children:`Nein`})]}):(0,y.jsx)(d,{variant:`outline`,size:`sm`,disabled:C!==null,onClick:()=>S(e.unit),children:C===e.unit?`Startet …`:e.schlaeft?`Wecken`:`Neu starten`})]}),u===e.unit&&(0,y.jsx)(b,{unit:e.unit})]},e.unit))}):(0,y.jsx)(`p`,{className:`px-4 text-[15px] text-text-2`,children:`Keine Dienste gefunden.`}),(0,y.jsx)(g,{open:e,onOpenChange:e=>!e&&t(),children:(0,y.jsxs)(m,{side:`right`,className:`gap-3 overflow-y-auto border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[(0,y.jsxs)(p,{children:[(0,y.jsx)(f,{className:`schild text-lg`,children:`Dienste und Protokolle`}),(0,y.jsx)(h,{className:`text-text-2`,children:`Welcher Dienst läuft, was er zuletzt geschrieben hat.`})]}),E]})})}export{x as Dienste};
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{g as n,h as r}from"./index-DzdbFvQV.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-BReEHJc5.js";var l=e(),u=t();function d(e){let t=(0,l.c)(9),{onDienste:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(r,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=m):m=t[2];let h;t[3]!==d||t[4]!==f?(h=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-6 border-linie bg-[#0c0f12] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[m,(0,u.jsx)(n,{onDienste:d,onGewaehlt:f})]}),t[3]=d,t[4]=f,t[5]=h):h=t[5];let g;return t[6]!==p||t[7]!==h?(g=(0,u.jsx)(c,{open:!0,onOpenChange:p,children:h}),t[6]=p,t[7]=h,t[8]=g):g=t[8],g}export{d as MenueSchublade};
|
||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{g as n,h as r}from"./index-21ZM2s1S.js";import{a as i,i as a,n as o,r as s,t as c}from"./sheet-CgwkrC7e.js";var l=e(),u=t();function d(e){let t=(0,l.c)(9),{onDienste:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,u.jsxs)(a,{className:`p-0`,children:[(0,u.jsx)(i,{asChild:!0,children:(0,u.jsx)(`div`,{children:(0,u.jsx)(r,{})})}),(0,u.jsx)(s,{className:`sr-only`,children:`Bereiche und Seiten`})]}),t[2]=m):m=t[2];let h;t[3]!==d||t[4]!==f?(h=(0,u.jsxs)(o,{side:`left`,className:`flex flex-col gap-6 border-linie bg-[#0c0f12] px-4 pt-5 pb-[calc(1.25rem+env(safe-area-inset-bottom))] data-[side=left]:w-[300px]`,children:[m,(0,u.jsx)(n,{onDienste:d,onGewaehlt:f})]}),t[3]=d,t[4]=f,t[5]=h):h=t[5];let g;return t[6]!==p||t[7]!==h?(g=(0,u.jsx)(c,{open:!0,onOpenChange:p,children:h}),t[6]=p,t[7]=h,t[8]=g):g=t[8],g}export{d as MenueSchublade};
|
||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-BReEHJc5.js";var s=e(),c=t();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster};
|
||||
import{n as e,r as t}from"./dist-D4CVSex9.js";import{a as n,i as r,n as i,r as a,t as o}from"./sheet-CgwkrC7e.js";var s=e(),c=t();function l(e){let t=(0,s.c)(14),{offen:l,titel:u,text:d,onSchliessen:f}=e,p;t[0]===f?p=t[1]:(p=e=>!e&&f(),t[0]=f,t[1]=p);let m;t[2]===Symbol.for(`react.memo_cache_sentinel`)?(m=(0,c.jsx)(n,{className:`schild text-lg`,children:`Protokoll`}),t[2]=m):m=t[2];let h;t[3]===u?h=t[4]:(h=(0,c.jsxs)(r,{children:[m,(0,c.jsx)(a,{className:`text-text-2`,children:u})]}),t[3]=u,t[4]=h);let g;t[5]===d?g=t[6]:(g=(0,c.jsx)(`pre`,{className:`ziffern mx-4 mb-4 flex-1 overflow-auto rounded-lg border border-linie bg-[#0b0d0f] p-3 text-xs leading-relaxed whitespace-pre-wrap text-text-2`,children:d}),t[5]=d,t[6]=g);let _;t[7]!==h||t[8]!==g?(_=(0,c.jsxs)(i,{side:`right`,className:`border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl`,children:[h,g]}),t[7]=h,t[8]=g,t[9]=_):_=t[9];let v;return t[10]!==l||t[11]!==p||t[12]!==_?(v=(0,c.jsx)(o,{open:l,onOpenChange:p,children:_}),t[10]=l,t[11]=p,t[12]=_,t[13]=v):v=t[13],v}export{l as Protokollfenster};
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{Jt as e}from"./index-DzdbFvQV.js";var t={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};t.node;var n=e(t);export{n as t};
|
||||
import{Yt as e}from"./index-21ZM2s1S.js";var t={name:`chevron-right`,size:24,node:[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]};t.node;var n=e(t);export{n as t};
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
||||
import{Jt as e}from"./index-DzdbFvQV.js";var t={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};t.node;var n=e(t);export{n as t};
|
||||
import{Yt as e}from"./index-21ZM2s1S.js";var t={name:`search`,size:24,node:[[`path`,{d:`m21 21-4.34-4.34`,key:`14j7rj`}],[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}]]};t.node;var n=e(t);export{n as t};
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{i as e,n as t,r as n,t as r}from"./dist-D4CVSex9.js";import{Ut as i}from"./index-DzdbFvQV.js";import{t as a}from"./button-D8rvET0Z.js";import{a as o,i as s,n as c,o as l,r as u,s as d,t as f}from"./dist-C31gG1ts.js";var p=t();e();var m=n();function h(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(f,{"data-slot":`sheet`,...n}),t[2]=n,t[3]=r),r}function g(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(l,{"data-slot":`sheet-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,o,s,l,d;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4],d=t[5]):({className:o,children:n,side:l,showCloseButton:d,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l,t[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===o?y=t[8]:(y=r(`fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10`,o),t[7]=o,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==s||t[13]!==f||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[n,b]})]}),t[11]=n,t[12]=s,t[13]=f,t[14]=y,t[15]=b,t[16]=x):x=t[16],x}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-0.5 p-4`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`sheet-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base font-medium text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{b as a,y as i,v as n,x as r,h as t};
|
||||
import{i as e,n as t,r as n,t as r}from"./dist-D4CVSex9.js";import{Wt as i}from"./index-21ZM2s1S.js";import{t as a}from"./button-D8rvET0Z.js";import{a as o,i as s,n as c,o as l,r as u,s as d,t as f}from"./dist-CPF3acEI.js";var p=t();e();var m=n();function h(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(f,{"data-slot":`sheet`,...n}),t[2]=n,t[3]=r),r}function g(e){let t=(0,p.c)(4),n;t[0]===e?n=t[1]:({...n}=e,t[0]=e,t[1]=n);let r;return t[2]===n?r=t[3]:(r=(0,m.jsx)(l,{"data-slot":`sheet-portal`,...n}),t[2]=n,t[3]=r),r}function _(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0`,n),t[3]=n,t[4]=a);let s;return t[5]!==i||t[6]!==a?(s=(0,m.jsx)(o,{"data-slot":`sheet-overlay`,className:a,...i}),t[5]=i,t[6]=a,t[7]=s):s=t[7],s}function v(e){let t=(0,p.c)(17),n,o,s,l,d;t[0]===e?(n=t[1],o=t[2],s=t[3],l=t[4],d=t[5]):({className:o,children:n,side:l,showCloseButton:d,...s}=e,t[0]=e,t[1]=n,t[2]=o,t[3]=s,t[4]=l,t[5]=d);let f=l===void 0?`right`:l,h=d===void 0||d,v;t[6]===Symbol.for(`react.memo_cache_sentinel`)?(v=(0,m.jsx)(_,{}),t[6]=v):v=t[6];let y;t[7]===o?y=t[8]:(y=r(`fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10`,o),t[7]=o,t[8]=y);let b;t[9]===h?b=t[10]:(b=h&&(0,m.jsx)(c,{"data-slot":`sheet-close`,asChild:!0,children:(0,m.jsxs)(a,{variant:`ghost`,className:`absolute top-3 right-3`,size:`icon-sm`,children:[(0,m.jsx)(i,{}),(0,m.jsx)(`span`,{className:`sr-only`,children:`Schließen`})]})}),t[9]=h,t[10]=b);let x;return t[11]!==n||t[12]!==s||t[13]!==f||t[14]!==y||t[15]!==b?(x=(0,m.jsxs)(g,{children:[v,(0,m.jsxs)(u,{"data-slot":`sheet-content`,"data-side":f,className:y,...s,children:[n,b]})]}),t[11]=n,t[12]=s,t[13]=f,t[14]=y,t[15]=b,t[16]=x):x=t[16],x}function y(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`flex flex-col gap-0.5 p-4`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(`div`,{"data-slot":`sheet-header`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function b(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`font-heading text-base font-medium text-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(d,{"data-slot":`sheet-title`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}function x(e){let t=(0,p.c)(8),n,i;t[0]===e?(n=t[1],i=t[2]):({className:n,...i}=e,t[0]=e,t[1]=n,t[2]=i);let a;t[3]===n?a=t[4]:(a=r(`text-sm text-muted-foreground`,n),t[3]=n,t[4]=a);let o;return t[5]!==i||t[6]!==a?(o=(0,m.jsx)(s,{"data-slot":`sheet-description`,className:a,...i}),t[5]=i,t[6]=a,t[7]=o):o=t[7],o}export{b as a,y as i,v as n,x as r,h as t};
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<title>Homelab Orchestrator</title>
|
||||
<script type="module" crossorigin src="/assets/index-DzdbFvQV.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-21ZM2s1S.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/dist-D4CVSex9.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useSelector-uUwrspuX.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useMatch-CiyhmkMV.js">
|
||||
|
||||
@@ -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"), {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user