Welle 2 · Befundliste mit Ablauf: neu, gesehen, 24 h stumm; Puffer gegen Flackern
Jeder Hinweis hat jetzt einen Zustand (neu, gesehen, stumm bis …) mit Knöpfen in beiden Cockpits; das Homelab-Cockpit nutzt dieselbe Liste wie die KI-Box (Knöpfe gehen an den Homelab-Teil, neue Route). Stumm heißt: keine Telegram-Erinnerungen, unten in der Liste, keine Warnleuchte — verschärft er sich von gelb auf rot, gilt stumm nicht mehr. Ein Hinweis ist erst erledigt, wenn sein Befund drei Takte am Stück fehlt: Kurze Wackler bringen kein „Erledigt“ und gleich danach keinen neuen Alarm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
45f11bc3dc
commit
995a0f34ee
@@ -14,7 +14,10 @@ gesehen. Dieser Wächter schaut deshalb breiter hin:
|
||||
• Probe monatliche Probe-Wiederherstellung der Sicherung (rot oder zu alt = gelb)
|
||||
|
||||
Jeder Befund wird zum Hinweis mit Stufe (rot = jetzt, gelb = bei Gelegenheit), Beginn und
|
||||
Knöpfen. Einfaches behebt er selbst (User-Entscheid 23.09.): Ein ABGESTÜRZTER Dienst
|
||||
Knöpfen. Seit 25.09.2026 (Ausbauplan Welle 2, Punkt 10) hat jeder Hinweis einen Zustand: neu, gesehen oder stumm bis …
|
||||
(24 Stunden, je Hinweis per Knopf; verschärft er sich von gelb auf rot, gilt stumm nicht mehr). Stumm heißt: keine
|
||||
Telegram-Erinnerungen, in der Liste unten. Und ein Hinweis ist erst erledigt, wenn sein Befund ERLEDIGT_NACH Takte am
|
||||
Stück fehlt — ein kurzer Wackler bringt kein „Erledigt“ und gleich danach keinen neuen Alarm. Einfaches behebt er selbst (User-Entscheid 23.09.): Ein ABGESTÜRZTER Dienst
|
||||
(ActiveState=failed) wird neu gestartet, höchstens AUTO_MAX_PRO_STUNDE-mal pro Stunde. Ein
|
||||
bewusst gestoppter Dienst (inactive) bleibt aus und wird nur gemeldet. Rote Hinweise gehen
|
||||
zusätzlich an Telegram und in Lucys Briefkasten.
|
||||
@@ -67,6 +70,12 @@ STORE_PATH = Path(os.environ.get("MC_WAECHTER_STORE", str(DATEN_DIR / "mc2-waech
|
||||
# „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(DATEN_DIR / "mc2-quittiert.json")))
|
||||
# Zustand je Hinweis (seit 25.09.2026): gesehen / stumm bis … Schreibt nur MC2 (Knöpfe), der Steward liest.
|
||||
ZUSTAND_PATH = Path(os.environ.get("MC_WAECHTER_ZUSTAND", str(DATEN_DIR / "mc2-hinweis-zustand.json")))
|
||||
STUMM_S = int(os.environ.get("MC_WAECHTER_STUMM_S", str(24 * 3600)))
|
||||
# So viele Takte am Stück muss ein Befund fehlen, bis sein Hinweis erledigt ist (Puffer gegen Flackern).
|
||||
ERLEDIGT_NACH = int(os.environ.get("MC_WAECHTER_ERLEDIGT_NACH", "3"))
|
||||
STUFE_RANG = {"gelb": 1, "rot": 2}
|
||||
# Die andere Instanz darf einen Neustart lang schweigen (Sonntags-Update der Box, Container-Update
|
||||
# auf dem Proxmox-PC), bevor daraus ein roter Hinweis wird.
|
||||
PARTNER_TAKTE = int(os.environ.get("MC_WAECHTER_PARTNER_TAKTE", "5"))
|
||||
@@ -717,23 +726,27 @@ def takt() -> None:
|
||||
hinweise: dict = _stand["hinweise"]
|
||||
kandidaten: dict = _stand["kandidaten"]
|
||||
aktuell = {b.id for b in befunde}
|
||||
zustaende = hinweis_zustaende()
|
||||
|
||||
for b in befunde:
|
||||
k = kandidaten.setdefault(b.id, {"takte": 0, "erstmals": jetzt})
|
||||
k["takte"] += 1
|
||||
if not update and b.auto:
|
||||
_selbst_beheben(b, jetzt)
|
||||
if not (b.sofort or k["takte"] >= (b.nach_takten or FAIL_AFTER)):
|
||||
# Ein Hinweis, der noch im Puffer steht (Befund kurz weg und wieder da), läuft einfach weiter.
|
||||
if not (b.sofort or k["takte"] >= (b.nach_takten or FAIL_AFTER) or b.id in hinweise):
|
||||
continue
|
||||
h = hinweise.get(b.id)
|
||||
neu = h is None
|
||||
h = h or {"id": b.id, "seit": k["erstmals"], "gemeldet": 0.0}
|
||||
h.update(stufe=b.stufe, titel=b.titel, text=b.text, quelle=b.quelle,
|
||||
aktionen=b.aktionen, zuletzt=jetzt, takte=k["takte"])
|
||||
h.pop("fehlt", None)
|
||||
hinweise[b.id] = h
|
||||
if neu:
|
||||
_verlauf("neu", b.id, b.titel, b.stufe)
|
||||
if b.stufe == "rot" and (neu or jetzt - h.get("gemeldet", 0.0) >= REMIND_S):
|
||||
if b.stufe == "rot" and (neu or jetzt - h.get("gemeldet", 0.0) >= REMIND_S) \
|
||||
and not ist_stumm(zustaende.get(b.id), b.stufe, jetzt):
|
||||
vorsatz = "" if neu else "Immer noch: "
|
||||
_telegram(BETREFF_PROBLEM, f"{vorsatz}{b.titel}. {b.text}")
|
||||
h["gemeldet"] = jetzt
|
||||
@@ -745,7 +758,11 @@ def takt() -> None:
|
||||
continue
|
||||
if update and hid.startswith(("dienst:", "kern:")):
|
||||
continue
|
||||
hinweise[hid]["fehlt"] = hinweise[hid].get("fehlt", 0) + 1
|
||||
if hinweise[hid]["fehlt"] < ERLEDIGT_NACH:
|
||||
continue
|
||||
h = hinweise.pop(hid)
|
||||
h.pop("fehlt", None)
|
||||
_verlauf("erledigt", hid, h.get("titel", hid), h.get("stufe"))
|
||||
if h.get("stufe") == "rot" and h.get("gemeldet"):
|
||||
_telegram(BETREFF_OK, f"Erledigt: {h.get('titel', hid)}.")
|
||||
@@ -772,6 +789,51 @@ async def waechter_loop() -> None:
|
||||
await asyncio.sleep(INTERVAL)
|
||||
|
||||
|
||||
# --- Zustand je Hinweis: gesehen, stumm (seit 25.09.2026) --------------------------------
|
||||
|
||||
def hinweis_zustaende() -> dict[str, dict]:
|
||||
try:
|
||||
daten = json.loads(ZUSTAND_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
return daten if isinstance(daten, dict) else {}
|
||||
|
||||
|
||||
def ist_stumm(eintrag: dict | None, stufe: str | None, jetzt: float) -> bool:
|
||||
"""Stumm, solange die Frist läuft und der Hinweis sich nicht verschärft hat (gelb stumm geschaltet, jetzt rot:
|
||||
nicht mehr stumm — Kritisches meldet sich trotzdem)."""
|
||||
if not eintrag or float(eintrag.get("stumm_bis") or 0) <= jetzt:
|
||||
return False
|
||||
return STUFE_RANG.get(stufe or "", 0) <= STUFE_RANG.get(str(eintrag.get("stumm_stufe")), 0)
|
||||
|
||||
|
||||
def hinweis_zustand_setzen(hinweis: dict, was: str, jetzt: float | None = None) -> dict:
|
||||
"""Die Knöpfe „Gesehen“, „24 h stumm“, „Wieder melden“ (MC2-Prozess)."""
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
daten = hinweis_zustaende()
|
||||
eintrag = dict(daten.get(hinweis["id"]) or {})
|
||||
if was == "gesehen":
|
||||
eintrag["gesehen"] = hinweis.get("seit")
|
||||
text = "Als gesehen markiert."
|
||||
elif was == "stumm":
|
||||
eintrag.update(stumm_bis=jetzt + STUMM_S, stumm_stufe=hinweis.get("stufe"), gesehen=hinweis.get("seit"))
|
||||
text = (f"Stumm für {STUMM_S // 3600} Stunden: keine Erinnerungen. Wird er schlimmer (rot), "
|
||||
"meldet er sich trotzdem.")
|
||||
else:
|
||||
eintrag.pop("stumm_bis", None)
|
||||
eintrag.pop("stumm_stufe", None)
|
||||
text = "Meldet sich wieder."
|
||||
eintrag["geaendert"] = jetzt
|
||||
daten[hinweis["id"]] = eintrag
|
||||
# Einträge ohne laufende Frist, die eine Woche nicht angefasst wurden, fallen weg.
|
||||
daten = {k: v for k, v in daten.items()
|
||||
if float(v.get("stumm_bis") or 0) > jetzt or jetzt - float(v.get("geaendert") or 0) < 7 * 86400}
|
||||
tmp = ZUSTAND_PATH.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
os.replace(tmp, ZUSTAND_PATH)
|
||||
return {"ok": True, "text": text}
|
||||
|
||||
|
||||
# --- Lesen und Knöpfe (MC2-Prozess) ------------------------------------------------
|
||||
|
||||
def lese_stand() -> dict:
|
||||
@@ -780,10 +842,19 @@ def lese_stand() -> dict:
|
||||
daten = json.loads(STORE_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
daten = {}
|
||||
hinweise = sorted((daten.get("hinweise") or {}).values(),
|
||||
key=lambda h: (h.get("stufe") != "rot", h.get("seit", 0)))
|
||||
jetzt = time.time()
|
||||
zustaende = hinweis_zustaende()
|
||||
hinweise = list((daten.get("hinweise") or {}).values())
|
||||
for h in hinweise:
|
||||
h.pop("gemeldet", None)
|
||||
h.pop("fehlt", None)
|
||||
z = zustaende.get(h.get("id")) or {}
|
||||
if ist_stumm(z, h.get("stufe"), jetzt):
|
||||
h["zustand"], h["stumm_bis"] = "stumm", z["stumm_bis"]
|
||||
else:
|
||||
h["zustand"] = "gesehen" if z.get("gesehen") == h.get("seit") else "neu"
|
||||
# Rot vor gelb, stumm ganz unten, sonst nach Beginn.
|
||||
hinweise.sort(key=lambda h: (h["zustand"] == "stumm", h.get("stufe") != "rot", h.get("seit", 0)))
|
||||
return {
|
||||
"hinweise": hinweise,
|
||||
"verlauf": list(reversed((daten.get("verlauf") or [])[-50:])),
|
||||
@@ -805,6 +876,8 @@ def fuehre_aktion_aus(hinweis_id: str, aktion_id: str) -> dict:
|
||||
hinweis = next((h for h in stand["hinweise"] if h.get("id") == hinweis_id), None)
|
||||
if hinweis is None:
|
||||
return {"ok": False, "detail": "Diesen Hinweis gibt es nicht mehr."}
|
||||
if aktion_id in ("gesehen", "stumm", "laut"): # gibt es bei jedem Hinweis (seit 25.09.2026)
|
||||
return hinweis_zustand_setzen(hinweis, aktion_id)
|
||||
aktion = next((a for a in hinweis.get("aktionen", []) if a.get("id") == aktion_id), None)
|
||||
if aktion is None:
|
||||
return {"ok": False, "detail": "Diese Aktion gehört nicht zu dem Hinweis."}
|
||||
|
||||
Reference in New Issue
Block a user