""" 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])}