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>
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""
|
|
Flugplan der Box (Box-Wart, Umbau 09/2026): was lief heute, was kommt als Nächstes.
|
|
|
|
Quellen: die Hermes-Cron-Jobs (jobs.json: nächster/letzter Lauf, Status), die systemd-
|
|
Timer (Sicherung) und die Modell-Nutzung (NerdQuiz-Nachtlauf, erkannt an den nächtlichen
|
|
Anfragen). Alles nur lesend.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from datetime import datetime, timedelta
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from services import modell_nutzung, waechter
|
|
|
|
TIMER = {"mc2-backup.timer": "Sicherung", "mc2-radar.timer": "Modell-Radar"}
|
|
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
|
|
|
|
|
|
def _parse_iso(wert) -> datetime | None:
|
|
"""ISO-Zeit; ohne Offset gilt MC_LOCAL_TZ (Projektregel, nie raten)."""
|
|
if not wert:
|
|
return None
|
|
try:
|
|
dt = datetime.fromisoformat(str(wert).replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
return dt if dt.tzinfo else dt.replace(tzinfo=LOCAL_TZ)
|
|
|
|
|
|
def _timer() -> list[dict]:
|
|
"""systemd-Timer als JSON (systemd ≥ 256). Ohne systemd: leer."""
|
|
try:
|
|
out = subprocess.run(["systemctl", "--user", "list-timers", "--all", "--no-pager", "--output=json"],
|
|
capture_output=True, text=True, timeout=10).stdout
|
|
daten = json.loads(out or "[]")
|
|
except Exception:
|
|
return []
|
|
ergebnis = []
|
|
for t in daten if isinstance(daten, list) else []:
|
|
name = TIMER.get(t.get("unit", ""))
|
|
if not name:
|
|
continue
|
|
def _us(v):
|
|
try:
|
|
return datetime.fromtimestamp(int(v) / 1_000_000, LOCAL_TZ) if v else None
|
|
except (TypeError, ValueError):
|
|
return None
|
|
ergebnis.append({"name": name, "naechster": _us(t.get("next")), "letzter": _us(t.get("last"))})
|
|
return ergebnis
|
|
|
|
|
|
def _nerdquiz_nacht(nutzung: dict) -> dict | None:
|
|
"""Der NerdQuiz-Nachtlauf, erkannt an Anfragen zwischen 01 und 06 Uhr in den letzten 24 h."""
|
|
stunden = [s for s in nutzung.get("letzte_24h", []) if "01" <= s["stunde"] <= "06"]
|
|
anfragen = sum(v for s in stunden for n, v in s["je_absender"].items() if "NerdQuiz" in n)
|
|
if not anfragen:
|
|
return None
|
|
erste = next((s["stunde"] for s in stunden
|
|
if any("NerdQuiz" in n for n in s["je_absender"])), "03")
|
|
heute = datetime.now(LOCAL_TZ).replace(hour=int(erste), minute=0, second=0, microsecond=0)
|
|
return {"zeit": heute, "titel": "NerdQuiz-Nachtlauf", "text": f"{anfragen} Anfragen ans Hirn",
|
|
"status": "erledigt"}
|
|
|
|
|
|
def flugplan() -> dict:
|
|
"""Heute Gelaufenes und die nächsten geplanten Läufe, je zeitlich sortiert."""
|
|
jetzt = datetime.now(LOCAL_TZ)
|
|
heute_start = jetzt.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
gelaufen: list[dict] = []
|
|
geplant: list[dict] = []
|
|
|
|
for job in waechter._hermes_jobs():
|
|
if not job.get("enabled", True):
|
|
continue
|
|
name = str(job.get("name", "Job"))
|
|
letzter, naechster = _parse_iso(job.get("last_run_at")), _parse_iso(job.get("next_run_at"))
|
|
if letzter and letzter >= heute_start:
|
|
ok = job.get("last_status") in (None, "ok", "success")
|
|
gelaufen.append({"zeit": letzter, "titel": name, "text": "",
|
|
"status": "erledigt" if ok else "fehler"})
|
|
if naechster and naechster > jetzt:
|
|
geplant.append({"zeit": naechster, "titel": name, "text": "", "status": "geplant"})
|
|
|
|
for t in _timer():
|
|
if t["letzter"] and t["letzter"] >= heute_start:
|
|
gelaufen.append({"zeit": t["letzter"], "titel": t["name"], "text": "", "status": "erledigt"})
|
|
if t["naechster"] and t["naechster"] > jetzt:
|
|
geplant.append({"zeit": t["naechster"], "titel": t["name"], "text": "", "status": "geplant"})
|
|
|
|
if (nq := _nerdquiz_nacht(modell_nutzung.nutzung())):
|
|
gelaufen.append(nq)
|
|
|
|
def _aus(eintraege: list[dict]) -> list[dict]:
|
|
return [{**e, "zeit": e["zeit"].isoformat()} for e in sorted(eintraege, key=lambda e: e["zeit"])]
|
|
|
|
grenze = jetzt + timedelta(days=8)
|
|
return {"gelaufen": _aus(gelaufen), "geplant": _aus([g for g in geplant if g["zeit"] <= grenze])}
|