Welle 3 · Lucys Freiheiten je Aktionsart, Rückfragen mit „ja 7“, Lucy untersucht rote Hinweise, Wochenbericht

- services/lucy_autonomie.py: Stufe je Aktionsart (nur lesen, erst fragen, selbst handeln), Start „mehr Freiheit“;
  Meldung danach (POST /api/lucy/gehandelt)
- services/rueckfragen.py: nummerierte Rückfragen, nur Knöpfe der Oberfläche (ERLAUBT), „ja“ führt genau die
  gespeicherte Aktion aus, 24 h gültig, hinfällig wenn der Hinweis weg ist, Nachprüfung danach
- notify.sh -k: Antworttasten (Antworttastatur über den Direktweg, ohne Eingriff in Hermes)
- services/ermittlung.py: Wächter meldet rote Hinweise an Lucy; Knopf wird Rückfrage, Lucy sieht über den
  api_server nach und schickt Ursache und Weg als eine Nachricht
- services/wochenbericht.py + mc2-wochenbericht.timer (So 07:30): feste Sätze, Absatz vom Modell, offene
  App-Updates als Rückfragen
- MCP: Werkzeuge fragen die Freiheiten (auf_bitte), neu: freiheiten, rueckfragen, rueckfrage_beantworten,
  sicherheitsupdates_einspielen, snapshot_zurueckspielen, snapshots, geraet_logs, dienst_logs
- Ausführer-Aktion journal (nur lesend), POST /api/homelab/ziele/{id}/sicherheitsupdates
- Läufe des Homelabs mit flock (Oberfläche und Steward schreiben beide)
- Skill homelab-orchestrator, SOUL-Abschnitt als Kopie in deploy/nur-box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 19:12:50 +02:00
co-authored by Claude Opus 5.5
parent dd23f3fb4c
commit 12cefb427a
37 changed files with 2649 additions and 169 deletions
+149
View File
@@ -0,0 +1,149 @@
"""Wochenbericht in Lucys Stimme (Ausbauplan Welle 3, Punkt 16, 25.09.2026): feste Sätze aus den Zahlen beider Teile,
ein kurzer Absatz vom Modell obenauf, offene App-Updates als Rückfragen mit Antworttasten."""
import json
from datetime import datetime
import pytest
from kern import einstellungen as einstellungen_mod
from kern.zeit import LOCAL_TZ
from services import rueckfragen, wochenbericht
JETZT = datetime(2026, 9, 27, 7, 30, tzinfo=LOCAL_TZ).timestamp() # Sonntag
TAG = 86400
FAKTEN = {
"homelab_laeufe": [
{"status": "fertig", "ergebnis": "eingespielt", "name": "Gitea", "baustein": "app", "ende": JETZT - TAG},
{"status": "fertig", "ergebnis": "eingespielt", "name": "Proxmox-Host", "baustein": "sicherheit",
"ende": JETZT - 4 * 3600},
{"status": "fertig", "ergebnis": "zurueckgerollt", "name": "NetBird", "baustein": "os", "ende": JETZT - 2 * TAG},
{"status": "fertig", "ergebnis": "eingespielt", "name": "Alt", "baustein": "app", "ende": JETZT - 9 * TAG},
{"status": "fertig", "ergebnis": "eingespielt", "name": "Probe", "baustein": "probe", "ende": JETZT - TAG},
{"status": "laeuft", "name": "Vaultwarden", "baustein": "os", "ende": None},
],
"box_laeufe": [
{"start": datetime(2026, 9, 27, 4, 30, tzinfo=LOCAL_TZ).isoformat(),
"zeilen": [{"baustein": "Motor", "ergebnis": "eingespielt"}, {"baustein": "Hermes", "ergebnis": "festgehalten"},
{"baustein": "llama-swap", "ergebnis": "aktuell"}]},
{"start": "2026-09-13T04:30:00+02:00", "zeilen": [{"baustein": "Hermes", "ergebnis": "fehler"}]},
],
"ziele_box": [{"id": "ki-box", "name": "KI-Box", "bausteine": [
{"id": "os", "name": "Betriebssystem", "zustand": "neu", "kurz": "5 Pakete", "aktion": {"pfad": "/api/x"}}]}],
"ziele_homelab": [
{"id": "ct-104", "name": "Gitea", "bausteine": [
{"id": "app", "name": "Gitea", "zustand": "neu", "verfuegbar": "1.27.4",
"aktion": {"pfad": "/api/homelab/ziele/ct-104/update"}},
{"id": "os", "name": "Pakete", "zustand": "neu", "sicherheit": 2, "aktion": {"pfad": "/api/homelab/ziele/ct-104/os-update"}}]},
{"id": "ct-110", "name": "Home Assistant", "bausteine": [
{"id": "app", "name": "Home Assistant", "zustand": "neu", "verfuegbar": "2026.10", "aktion": None,
"grund": "eigene Oberfläche"}]},
],
"hinweise_box": [],
"hinweise_homelab": [{"stufe": "gelb", "titel": "Platte von AdGuard Home zu 84 % voll"}],
"speicher": {"pools": [{"speicher": "local-lvm", "anteil": 0.52, "vorhersage": {"tage": 40.0}}],
"systemplatte": {"anteil": 0.31, "vorhersage": {"tage": None}},
"nas": [{"problem": "NAS antwortet nicht."}],
"sicherungen": {"stufe": "gelb", "fehlend": [{"name": "Arcane"}],
"geraete": [{"name": "Gitea", "probe": {"ok": True}},
{"name": "NetBird", "probe": {"ok": False}}]}},
"box_platte": {"percent": 61.0, "vorhersage": {"tage": 10.0}},
"zertifikate": {"zertifikate": [{"name": "mc.example", "stufe": "gelb", "problem": "wird nicht erneuert, noch "
"2 Tage gültig"},
{"name": "pve", "stufe": "ok"}]},
"docker": {"verfuegbar": True, "stacks": [{"container": [
{"name": "immich", "luecken": {"kritisch": 3}}, {"name": "paperless", "luecken": {"kritisch": 0}},
{"name": "redis", "luecken": None}]}]},
"radar": [{"name": "Qwen4-30B", "status": "bestanden"}, {"name": "X", "status": "wartet"},
{"name": "Y", "status": "neu"}],
"rueckfragen": {"zuletzt": [{"zeit": JETZT - TAG, "antwort": {"ja": True}}, {"zeit": JETZT - TAG, "antwort": None},
{"zeit": JETZT - 10 * TAG, "antwort": {"ja": True}}]},
"lucy": [{"zeit": "2026-09-26T10:00:00", "werkzeug": "dienst_neustarten", "stufe": "selbst", "ergebnis": "ok"},
{"zeit": "2026-09-26T11:00:00", "werkzeug": "update_starten", "stufe": "auf_bitte", "ergebnis": "ok"},
{"zeit": "2026-09-26T12:00:00", "werkzeug": "update_starten", "stufe": "fragen", "ergebnis": "Rückfrage 3"},
{"zeit": "2026-09-26T13:00:00", "werkzeug": "x", "stufe": "selbst", "ergebnis": "Nicht ausgeführt: nein"},
{"zeit": "2026-09-10T10:00:00", "werkzeug": "dienst_neustarten", "stufe": "selbst", "ergebnis": "ok"}],
}
@pytest.fixture
def daten(tmp_path, monkeypatch):
monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path))
einstellungen_mod.einstellungen.cache_clear()
yield tmp_path
einstellungen_mod.einstellungen.cache_clear()
def test_feste_saetze():
assert wochenbericht.zeilen(FAKTEN, JETZT) == [
"Eingespielt: Gitea (App), Proxmox-Host (Sicherheitsupdates) und KI-Box Motor.",
"Nicht glatt gelaufen: NetBird (Pakete) zurückgerollt und KI-Box Hermes festgehalten.",
"Offen: Gitea 1.27.4 und Home Assistant 2026.10; Paket-Updates auf 2 Geräten, davon 2 Sicherheitsupdates.",
"Hinweise: gelb: Platte von AdGuard Home zu 84 % voll.",
("Speicher: Pool local-lvm 52 % (voll in etwa 6 Wochen); Host-Systemplatte 31 %; NAS: NAS antwortet nicht; "
"KI-Box 61 % (voll in etwa 10 Tagen)."),
"Sicherungen: Stufe gelb; zu alt oder fehlend: Arcane; Probe-Wiederherstellung gescheitert: NetBird.",
"Zertifikate: mc.example: wird nicht erneuert, noch 2 Tage gültig.",
"Lücken-Scan: immich (3 kritisch).",
"Modell-Radar: bestanden und wartet auf deine Entscheidung: Qwen4-30B; 2 Kandidaten warten auf den Test.",
"Lucy: 1× selbst gehandelt; 1× auf deine Bitte; 2 Rückfragen beantwortet oder abgelaufen, davon 1× ja.",
]
def test_leere_woche():
zeilen = wochenbericht.zeilen({}, JETZT)
assert zeilen == ["Eingespielt: nichts.", "Offen: nichts.", "Hinweise: keine.", "Speicher: keine Angaben.",
"Sicherungen: keine Angaben.", "Zertifikate: keine Angaben."]
def test_bericht_mit_rueckfragen(daten, monkeypatch):
gesendet = []
monkeypatch.setattr(wochenbericht, "fakten", lambda jetzt=None: FAKTEN)
monkeypatch.setattr(wochenbericht, "prosa", lambda zeilen: "Commander, eine ruhige Woche.")
monkeypatch.setattr(wochenbericht.announce, "add", lambda text, **k: gesendet.append(("briefkasten", k)))
monkeypatch.setattr(wochenbericht.announce, "notify_telegram",
lambda betreff, text, dringend=False, knoepfe=None: gesendet.append((betreff, text, knoepfe)))
assert wochenbericht.senden(JETZT) == {"ok": True, "text": "Der Wochenbericht ist unterwegs."}
briefkasten, telegram = gesendet
assert briefkasten == ("briefkasten", {"subject": "[Wochenbericht]", "source": "wochenbericht",
"priority": "silent"})
betreff, text, knoepfe = telegram
assert betreff == "[Wochenbericht]" and knoepfe == ["ja 1", "ja 2"]
assert text.startswith("Commander, eine ruhige Woche.\n\nDie Woche in Stichpunkten:\n• Eingespielt: Gitea (App)")
assert text.endswith("Auf Zuruf:\n• Gitea 1.27.4 einspielen — „ja 1“\n"
"• alles auf einmal („Alle aktualisieren“) — „ja 2“")
offen = rueckfragen.liste(jetzt=JETZT)["offen"]
assert [(f["pfad"], f["von"]) for f in offen] == [("/api/partner/homelab/alle", "wochenbericht"),
("/api/partner/homelab/ziele/ct-104/update", "wochenbericht")]
assert json.loads((daten / "wochenbericht.json").read_text(encoding="utf-8"))["zeit"] == JETZT
assert wochenbericht.stand(JETZT)["zuletzt"]["text"] == text
def test_ohne_modell_ein_fester_satz(daten, monkeypatch):
monkeypatch.setattr(wochenbericht, "fakten", lambda jetzt=None: {})
monkeypatch.setattr(wochenbericht, "prosa", lambda zeilen: None)
bericht = wochenbericht.bauen(JETZT)
assert bericht["text"].startswith("Commander, hier ist dein Wochenbericht.\n\nDie Woche in Stichpunkten:")
assert bericht["knoepfe"] == [] and "Auf Zuruf" not in bericht["text"]
def test_naechster_termin():
samstag = datetime(2026, 9, 26, 12, 0, tzinfo=LOCAL_TZ).timestamp()
sonntag_acht = datetime(2026, 9, 27, 8, 0, tzinfo=LOCAL_TZ).timestamp()
assert wochenbericht.naechster(samstag) == JETZT
assert wochenbericht.naechster(sonntag_acht) == datetime(2026, 10, 4, 7, 30, tzinfo=LOCAL_TZ).timestamp()
def test_prosa_ohne_denkpause(monkeypatch):
class Antwort:
status_code = 200
@staticmethod
def json():
return {"choices": [{"message": {"content": "<think>hm</think>Commander, alles ruhig."}}]}
gesendet = {}
monkeypatch.setattr(wochenbericht.httpx, "post", lambda url, timeout, json: gesendet.update(json) or Antwort())
assert wochenbericht.prosa(["Eingespielt: nichts."]) == "Commander, alles ruhig."
assert gesendet["model"] == "fast" and gesendet["chat_template_kwargs"] == {"enable_thinking": False}
assert "Eingespielt: nichts." in gesendet["messages"][0]["content"]