From b0c9549a2957874421f295a5a006a0ea9070f5f0 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Thu, 24 Sep 2026 15:17:36 +0200 Subject: [PATCH] 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 --- backend/services/waechter.py | 33 ++++++++++++++++++++++++++++++--- backend/tests/test_waechter.py | 25 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/backend/services/waechter.py b/backend/services/waechter.py index 55683c6..9cba63f 100644 --- a/backend/services/waechter.py +++ b/backend/services/waechter.py @@ -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_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"))) +# „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 # 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. @@ -305,6 +308,24 @@ def _errors_log_ende(max_bytes: int = 400_000) -> list[str]: 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]: befunde: list[Befund] = [] jobs = _hermes_jobs() @@ -340,13 +361,15 @@ def pruefe_hermes_jobs() -> list[Befund]: if alter > 26 * 3600: continue # nur der aktuelle Lauf zählt 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( - id=f"job:{jid}:werkzeug", stufe="gelb", + id=befund_id, stufe="gelb", titel=f"Job „{name}“: {anzahl} Werkzeugfehler im letzten Lauf", text=beispiel or "Einzelne Werkzeuge meldeten Fehler, der Lauf selbst kam durch.", 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)) return befunde @@ -595,6 +618,10 @@ def fuehre_aktion_aus(hinweis_id: str, aktion_id: str) -> dict: if not update_verlauf.freigeben(aktion["baustein"]): return {"ok": False, "detail": "Der Baustein war schon freigegeben."} 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": try: r = subprocess.run(["hermes", "cron", "run", aktion["job"]], diff --git a/backend/tests/test_waechter.py b/backend/tests/test_waechter.py index eafcbb6..5ef97b1 100644 --- a/backend/tests/test_waechter.py +++ b/backend/tests/test_waechter.py @@ -139,3 +139,28 @@ def test_abgestuerzte_pruefung_wird_ein_gelber_befund(monkeypatch, tmp_path): hinweise = waechter._stand["hinweise"] assert "pruefung:pruefe_hermes_jobs" in hinweise 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"]