""" Nächtlicher Lauf des Modell-Radars (Box-Wart, 09/2026) — gestartet von mc2-radar.timer um 00:30. Warum ein eigener Einstieg: Der Test lädt ein bis zu ~85 GB großes Modell neben Lucys Hirn. Das darf nur nachts passieren und muss um 02:30 sicher vorbei sein, weil um 03:00 der NerdQuiz- Nachtlauf das Hirn braucht. Ablauf: 1. Suchen — höchstens einmal am Tag (Merkliste + Entdeckung). 2. Offene Urteile nachholen („getestet“: damals fehlte die Vergleichsmessung). 3. Testen — nur im Fenster 00:30–02:30 und höchstens ein neuer Kandidat pro Woche. Die Messungen prüfen die Frist selbst. Hängt trotzdem etwas, zieht fünf Minuten nach der Frist die Notbremse: Kandidaten-Server und Download werden gestoppt, der Prozess endet hart. systemd (RuntimeMaxSec) ist die letzte Sicherung. Ist nichts fällig, endet der Lauf sofort mit Code 0. Mit --nur-suche (seit 24.09.2026) nur Schritt 1, jederzeit: So startet der Knopf „Jetzt suchen“ die Suche als Auftrag (services/radar.suche_starten). Getestet wird dabei nichts. """ import logging import os import signal import subprocess import sys import threading import time from pathlib import Path from services import radar logging.basicConfig( level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(), format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", ) log = logging.getLogger("radar_lauf") NOTBREMSE_S = int(os.environ.get("MC_RADAR_NOTBREMSE_S", "300")) def _beim_beenden(signum, _frame) -> None: """SIGTERM (systemd stop, RuntimeMaxSec) → normal auslaufen, damit der Kandidaten-Server gestoppt wird.""" raise SystemExit(f"Signal {signum}") def _notbremse(frist: float) -> threading.Timer: def ziehen() -> None: log.error("Radar: Notbremse – %s s nach der Frist läuft noch etwas, der Lauf wird hart beendet.", NOTBREMSE_S) try: radar.notstopp() finally: os._exit(3) bremse = threading.Timer(max(frist - time.time(), 0) + NOTBREMSE_S, ziehen) bremse.daemon = True bremse.start() return bremse NOTIFY = Path(__file__).resolve().parents[1] / "deploy" / "notify.sh" def melde_bestanden(test: dict) -> None: """Ein bestandener Kandidat wartet auf eine Entscheidung — einmal Bescheid geben, sonst liegt er unbemerkt herum (Lehre der Pins vom September). notify.sh sammelt Nachtmeldungen für den Morgen.""" k = radar.lade_stand()["kandidaten"].get(test.get("kandidat")) or {} rolle = "das Hirn" if test.get("rolle") == "hirn" else "den Coder" text = (f"{k.get('name') or test.get('kandidat')} hat den Nachttest für {rolle} bestanden. " f"{str(test.get('zusammenfassung') or '')[:300]} " "In MC2 unter Modelle kannst du ihn übernehmen oder verwerfen.") 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 über den bestandenen Kandidaten ging nicht raus.", exc_info=True) def main() -> int: beginn = radar.jetzt() if radar.suche_faellig(beginn): try: ergebnis = radar.suche() log.info("Radar: Suche – %s neu, %s warten", len(ergebnis.get("neu") or []), ergebnis.get("wartend")) except Exception: log.warning("Radar: Suche fehlgeschlagen", exc_info=True) else: log.info("Radar: heute schon gesucht.") if not radar.im_fenster(radar.jetzt()): log.info("Radar: außerhalb des Test-Fensters %s–%s, nichts zu testen.", radar.FENSTER_START, radar.FENSTER_ENDE) return 0 frist = radar.fenster_ende(radar.jetzt()).timestamp() signal.signal(signal.SIGTERM, _beim_beenden) bremse = _notbremse(frist) try: if (nachgeholt := radar.nachurteilen(frist)): log.info("Radar: %s offene Urteile nachgeholt.", nachgeholt) plan = radar.plane_test(radar.lade_stand(), radar.jetzt()) if not plan.get("kandidat"): log.info("Radar: nichts fällig (%s).", plan.get("grund")) return 0 log.info("Radar: teste %s bis spätestens %s.", plan.get("grund"), radar.FENSTER_ENDE) test = radar.teste(plan["kandidat"], frist) if test: log.info("Radar: %s – %s", test.get("ergebnis"), test.get("zusammenfassung")) if test.get("ergebnis") == "bestanden": melde_bestanden(test) return 0 finally: bremse.cancel() def nur_suche() -> int: """„Jetzt suchen“ aus der Oberfläche: nur die Suche, kein Test. Die letzte Zeile sagt das Ergebnis in Worten — die Auftragskarte zeigt sie. Code 1, wenn die Suche scheitert oder keine Quelle erreichbar war.""" # Das Protokoll des Auftrags zeigt die Oberfläche: ohne eine Zeile je Hugging-Face-Abruf (samt signierter # Download-Adressen), nur die Schritte des Radars und das Ergebnis. logging.getLogger("httpx").setLevel(logging.WARNING) try: ergebnis = radar.suche() except Exception as exc: log.exception("Radar: Suche fehlgeschlagen") print(f"Die Suche ist gescheitert: {exc.__class__.__name__}: {exc}", flush=True) return 1 neu = ergebnis.get("neu") or [] quellen = ergebnis.get("quellen") or {} teile = [f"{len(neu)} neu ({', '.join(neu)})" if neu else "nichts Neues", f"{ergebnis.get('wartend', 0)} warten auf den Nachttest"] if not quellen.get("discover", True): teile.append("Hugging Face war nicht erreichbar") if not quellen.get("watchlist", True): teile.append("die Merkliste war nicht lesbar") print(f"Suche fertig: {', '.join(teile)}.", flush=True) return 0 if any(quellen.values()) else 1 if __name__ == "__main__": sys.exit(nur_suche() if "--nur-suche" in sys.argv[1:] else main())