Files
mission-control-v2/backend/services/zeitplan.py
T
HitonabiandClaude Opus 5.5 12dadfe6ef umbau(boxwart): Backend auf Box-Wart umgestellt – Waechter, Modell-Nutzung, Zeitplan
MC2 wird Updater, Waechter und Modell-Radar (Konzept „MC2 als Box-Wart“, 23.09.2026).

- Neuer Waechter (services/waechter.py) loest sentry.py ab: Dienste, Timer-Laeufe,
  Hermes-Jobs samt Werkzeugfehlern, Kern-HTTP-Proben, Platte. Abgestuerzte Dienste
  startet er selbst neu (max. 2/h), rote Hinweise gehen an Telegram und Lucy.
  Laeuft im mc2-steward; waehrend eines Updates haelt er still.
- Neue Schnittstellen (routers/boxwart.py): /api/start, /api/hinweise (+ Aktionen),
  /api/modelle/nutzung, /api/zeitplan.
- Modell-Nutzung aus dem llama-swap-Journal (wer fragt wie oft, 24 h je Stunde).
- Entfernt: Ideen, Wissen, Chronik, Skills, Verbinden, Konsolen-Proxy, /api/events;
  Lucys Werkzeug idee_notieren; box_status nennt jetzt die offenen Hinweise.
- Behoben: projekte-sync ueberspringt leere Gitea-Repos (lief seit 07.09. stuendlich rot);
  Motor-Version kam aus dem verwaisten /opt/llamacpp statt /opt/llamacpp-vulkan.
- Erste Backend-Tests (8) fuer Waechter und Modell-Nutzung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:16:18 +02:00

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