""" 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. """ 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() if __name__ == "__main__": sys.exit(main())