waechter: Werkzeugfehler eines Jobs bis zum naechsten Lauf ausblenden

Heute stand ein schon behobener web_extract-Fehler des News-Jobs bis zum naechsten Lauf
(morgen 07:00) im Cockpit. Neuer Knopf "Ausblenden bis zum naechsten Lauf": MC2 merkt sich
den Lauf in /srv/models/mc2-quittiert.json, der Waechter blendet genau diesen Lauf aus. Hat
der naechste Lauf wieder Fehler, erscheint der Hinweis erneut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 15:17:36 +02:00
co-authored by Claude Opus 5.5
parent 4cd856bd34
commit b0c9549a29
2 changed files with 55 additions and 3 deletions
+30 -3
View File
@@ -58,6 +58,9 @@ AUTO_MAX_PRO_STUNDE = int(os.environ.get("MC_WAECHTER_AUTO_MAX", "2"))
DISK_ROT_PCT = float(os.environ.get("MC_WAECHTER_DISK_ROT", "90")) DISK_ROT_PCT = float(os.environ.get("MC_WAECHTER_DISK_ROT", "90"))
DISK_GELB_PCT = float(os.environ.get("MC_WAECHTER_DISK_GELB", "80")) DISK_GELB_PCT = float(os.environ.get("MC_WAECHTER_DISK_GELB", "80"))
STORE_PATH = Path(os.environ.get("MC_WAECHTER_STORE", str(MODELS_DIR / "mc2-waechter.json"))) STORE_PATH = Path(os.environ.get("MC_WAECHTER_STORE", str(MODELS_DIR / "mc2-waechter.json")))
# „Ausblenden bis zum nächsten Lauf“ (24.09.2026): Hinweis-ID → Start des Laufs, der ausgeblendet ist.
# Schreibt nur MC2 (Knopf), der Steward liest. Ein neuer Lauf macht den Eintrag wirkungslos.
QUITTIERT_PATH = Path(os.environ.get("MC_WAECHTER_QUITTIERT", str(MODELS_DIR / "mc2-quittiert.json")))
VERLAUF_MAX = 200 VERLAUF_MAX = 200
# Trockenlauf (Probelauf neben dem echten Betrieb): prüft und führt Hinweise, meldet aber # Trockenlauf (Probelauf neben dem echten Betrieb): prüft und führt Hinweise, meldet aber
# nichts an Telegram/Lucy und repariert nichts selbst — das macht weiter der echte Wächter. # nichts an Telegram/Lucy und repariert nichts selbst — das macht weiter der echte Wächter.
@@ -305,6 +308,24 @@ def _errors_log_ende(max_bytes: int = 400_000) -> list[str]:
return [] return []
def _quittiert() -> dict[str, str]:
try:
daten = json.loads(QUITTIERT_PATH.read_text(encoding="utf-8"))
return daten if isinstance(daten, dict) else {}
except (OSError, ValueError):
return {}
def quittieren(hinweis_id: str, lauf: str) -> None:
"""Hinweis bis zum nächsten Lauf ausblenden (atomar schreiben, alte Einträge begrenzen)."""
daten = _quittiert()
daten[hinweis_id] = lauf
daten = dict(list(daten.items())[-50:])
tmp = QUITTIERT_PATH.with_suffix(".json.tmp")
tmp.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8")
os.replace(tmp, QUITTIERT_PATH)
def pruefe_hermes_jobs() -> list[Befund]: def pruefe_hermes_jobs() -> list[Befund]:
befunde: list[Befund] = [] befunde: list[Befund] = []
jobs = _hermes_jobs() jobs = _hermes_jobs()
@@ -340,13 +361,15 @@ def pruefe_hermes_jobs() -> list[Befund]:
if alter > 26 * 3600: if alter > 26 * 3600:
continue # nur der aktuelle Lauf zählt continue # nur der aktuelle Lauf zählt
anzahl, beispiel = werkzeugfehler_im_lauf(log_zeilen, jid, lauf["started_at"]) anzahl, beispiel = werkzeugfehler_im_lauf(log_zeilen, jid, lauf["started_at"])
if anzahl: befund_id = f"job:{jid}:werkzeug"
if anzahl and _quittiert().get(befund_id) != lauf["started_at"]:
befunde.append(Befund( befunde.append(Befund(
id=f"job:{jid}:werkzeug", stufe="gelb", id=befund_id, stufe="gelb",
titel=f"Job „{name}“: {anzahl} Werkzeugfehler im letzten Lauf", titel=f"Job „{name}“: {anzahl} Werkzeugfehler im letzten Lauf",
text=beispiel or "Einzelne Werkzeuge meldeten Fehler, der Lauf selbst kam durch.", text=beispiel or "Einzelne Werkzeuge meldeten Fehler, der Lauf selbst kam durch.",
quelle=f"hermes-cron:{jid}", quelle=f"hermes-cron:{jid}",
aktionen=[_aktion("protokoll", "Protokoll", job=jid)], aktionen=[_aktion("protokoll", "Protokoll", job=jid),
_aktion("ausblenden", "Ausblenden bis zum nächsten Lauf", lauf=lauf["started_at"])],
sofort=True)) sofort=True))
return befunde return befunde
@@ -595,6 +618,10 @@ def fuehre_aktion_aus(hinweis_id: str, aktion_id: str) -> dict:
if not update_verlauf.freigeben(aktion["baustein"]): if not update_verlauf.freigeben(aktion["baustein"]):
return {"ok": False, "detail": "Der Baustein war schon freigegeben."} return {"ok": False, "detail": "Der Baustein war schon freigegeben."}
return {"ok": True, "text": update_verlauf.FREIGEGEBEN_TEXT} return {"ok": True, "text": update_verlauf.FREIGEGEBEN_TEXT}
if aktion_id == "ausblenden":
quittieren(hinweis_id, str(aktion.get("lauf", "")))
return {"ok": True, "text": "Ausgeblendet. Der Hinweis verschwindet in spätestens einer Minute "
"und kommt nur wieder, wenn der nächste Lauf erneut Fehler hat."}
if aktion_id == "job-wiederholen": if aktion_id == "job-wiederholen":
try: try:
r = subprocess.run(["hermes", "cron", "run", aktion["job"]], r = subprocess.run(["hermes", "cron", "run", aktion["job"]],
+25
View File
@@ -139,3 +139,28 @@ def test_abgestuerzte_pruefung_wird_ein_gelber_befund(monkeypatch, tmp_path):
hinweise = waechter._stand["hinweise"] hinweise = waechter._stand["hinweise"]
assert "pruefung:pruefe_hermes_jobs" in hinweise assert "pruefung:pruefe_hermes_jobs" in hinweise
assert "neues Format" in hinweise["pruefung:pruefe_hermes_jobs"]["text"] assert "neues Format" in hinweise["pruefung:pruefe_hermes_jobs"]["text"]
def test_ausblenden_gilt_bis_zum_naechsten_lauf(monkeypatch, tmp_path):
"""24.09.2026: Ein behobener Werkzeugfehler (z. B. web_extract im News-Job) soll sich
ausblenden lassen — kommt aber wieder, sobald ein neuer Lauf erneut Fehler hat."""
from datetime import datetime, timedelta
monkeypatch.setattr(waechter, "QUITTIERT_PATH", tmp_path / "mc2-quittiert.json")
heute = (datetime.now() - timedelta(hours=2)).isoformat(timespec="seconds")
lauf = {"started_at": heute}
monkeypatch.setattr(waechter, "_hermes_jobs", lambda: [{"id": "news", "name": "Daily News Report",
"enabled": True, "last_status": "ok"}])
monkeypatch.setattr(waechter, "_letzter_lauf", lambda jid: lauf)
monkeypatch.setattr(waechter, "_errors_log_ende", lambda: [])
monkeypatch.setattr(waechter, "werkzeugfehler_im_lauf", lambda zeilen, jid, start: (2, "web_extract: Fehler"))
befunde = waechter.pruefe_hermes_jobs()
assert [b.id for b in befunde] == ["job:news:werkzeug"]
assert any(a["id"] == "ausblenden" and a["lauf"] == heute for a in befunde[0].aktionen)
waechter.quittieren("job:news:werkzeug", heute)
assert waechter.pruefe_hermes_jobs() == []
lauf["started_at"] = (datetime.now() - timedelta(minutes=5)).isoformat(timespec="seconds")
assert [b.id for b in waechter.pruefe_hermes_jobs()] == ["job:news:werkzeug"]