Ampel / ampel (push) Successful in 26s
- Waechter und Dienste-Liste kennen "schlaeft": abgeschaltete Units (disable --now) sind kein Befund mehr; die Oberflaeche zeigt sie grau mit Knopf "Wecken" (fuer die Android-App spaeter). - Updates am Sonntag laufen wieder ueber mc2-autoupdate.timer (jobs/sonntags-update.sh) statt als Hermes-Cron, der sich beim Hermes-Update selbst neu startete (20.09.: 180 s, Fehlschlag). Flugplan und Waechter kennen den Timer. - 10 abgeloeste Skills (Werkstatt, Kanban, Orchestrator, Trend-Radar ...) aus dem Repo; deploy.sh verschiebt sie in ~/.hermes/skills-archiv statt sie weiter zu Lucy zu kopieren. - Embedding und Reranker schlafen: raus aus brains und Warm-Set, ttl 300, warmup.sh waermt sie nicht mehr vor. deploy.sh spielt Timer und Warm-Set-Drop-in selbst aus. - Dienste-Namen: "Box-Wart (MC2)", "Hermes-Dashboard", "Spracherkennung (Desktop-Lucy)". - Frontend neu gebaut (npm ci, lokale Pakete waren vom 28.08.). Tests: 87 gruen (neu: schlafende Dienste), Frontend Lint/Tests/Build gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.2 KiB
Python
101 lines
4.2 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",
|
|
"mc2-morgenmeldung.timer": "Morgenmeldung", "mc2-autoupdate.timer": "Updates am Sonntag"}
|
|
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])}
|