einstellungen: Zeitfenster, Radar-Schalter, Telegram-Test, Software-Stand und Altreste
Einstellungen, die etwas einstellen (services/einstellungen.py, /api/einstellungen): - Update-Zeitfenster: Wochentag und Uhrzeit des Update-Laufs als Drop-in mc2-autoupdate.timer.d/zeitfenster.conf. Gegen die Nachhol-Falle von Persistent=true (24.09.): Timer vor dem daemon-reload stoppen, Stempel auf jetzt, dann starten. Bei Fehler zurück auf das alte Drop-in. Während eines Update-Laufs abgelehnt. - Neustart nur im eingestellten Fenster (deploy/wartungsfenster.sh, drei Stunden ab der vollen Stunde des Beginns; Standard So 04:00-06:59). - Radar an/aus: disable --now bzw. enable + Stempel + start (kein Nachholen). deploy.sh respektiert den Schalter und startet den Radar-Timer nur mit Stempel; das Zeitfenster-Drop-in fasst er nie an. - Wächter: schläft der Timer eines Timer-Dienstes, ist ein alter Fehlschlag kein Befund mehr (der Dienst selbst ist static und schläft nie). - Telegram-Test je Instanz (/api/einstellungen/telegram-test und /api/homelab/einstellungen/telegram-test) über notify.sh -d; Ergebnis und Grund aus dem Melde-Log. notify.sh schreibt jetzt auch den Grund des gescheiterten Zweitwegs in den FALLBACK-Eintrag. - Einstellungsdatei mc2-einstellungen.json im Datenordner. Software-Stand: deploy.sh und homelab/ausrollen.sh schreiben nach dem Umschalten mc2-stand.json (deploy/stand-schreiben.py); /api/instanz liefert den Stand, die Einstellungen zeigen beide Instanzen und warnen bei Abweichung. Ohne Datei aus git, sonst unbekannt. Aufräumen: Altreste neben dem Betrieb aus einer festen, auf der Box geprüften Liste (Größe, Hinweis, nur was da und nicht in Gebrauch ist). Löschen nur per Kennung und Klick mit Rückfrage; Worktrees mit git worktree prune, Alt-Units mit daemon-reload, /opt/llamacpp notfalls mit sudo -n. Tests: pytest mit einem systemd-Abbild samt Nachhol-Regel, echtem notify.sh (Ersatz-Hermes, Tmp-Zugang, lokale Bot-API), Shell-Funktionen des Wartungsfensters und von deploy.sh; vitest für die Rückfragen der Schublade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40d2840b16
commit
3d8df1c6fd
@@ -11,10 +11,18 @@ Gelöscht wird nur auf Knopfdruck des Nutzers (mit Rückfrage in der Oberfläche
|
||||
(User-Entscheid 23.09.: „nach der Umstellung löschen“ — die Entscheidung je Modell bleibt beim Nutzer).
|
||||
Ein Eintrag wird aus der Config genommen, danach verschwinden nur Ordner, auf die NICHTS mehr zeigt:
|
||||
Die Bild-Zwillinge teilen sich ihre Gewichte mit Hirn und Coder, geteilte Dateien bleiben immer liegen.
|
||||
|
||||
Seit 24.09.2026 zusätzlich: Altreste neben dem Betrieb (Test-Ordner, alte Umgebungen, Worktrees, Alt-Units) aus
|
||||
einer festen Liste — siehe ALTRESTE unten.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
@@ -144,3 +152,228 @@ def loeschen(art: str, name: str) -> dict:
|
||||
except (OSError, ValueError) as e:
|
||||
return {"ok": False, "detail": f"Löschen ging nicht: {e}"}
|
||||
return {"ok": True, "text": f"{name} gelöscht, {frei / 1e9:.1f} GB frei."}
|
||||
|
||||
|
||||
# --- Altreste neben dem Betrieb (seit 24.09.2026) ---------------------------------------------------------------
|
||||
# Feste Liste aus dem Prüfbericht vom 24.09.2026, am selben Abend auf der Box nachgemessen (du) und gegengeprüft:
|
||||
# Kein laufender Dienst, keine eingetragene Unit, kein Cron- oder Hermes-Job, kein aktiver Skill und keine
|
||||
# Hermes-Einstellung nennt einen dieser Pfade. Angezeigt wird nur, was noch da ist; gelöscht wird nur auf Knopfdruck
|
||||
# mit Rückfrage und nur ein Eintrag dieser Liste — die Anfrage nennt eine Kennung, nie einen Pfad.
|
||||
#
|
||||
# Bewusst NICHT in der Liste, weil nicht klar tot: ~/.voice (die Spracherkennung schläft nur), ~/.cache (Modelle
|
||||
# von Lucys Stimme), ~/.opencode (steht in ~/.bashrc), ~/.hermes/kanban.db (Hermes öffnet sie noch), Archive
|
||||
# (profiles-archive, archiv-aufraeumen, wissens-vault), ~/.hermes/state-snapshots, der PBS-Weg, die Rückweg-Kopien
|
||||
# des Updaters (/opt/llamacpp-vulkan.bak, llama-swap.bak) und Skripte, die ein aktiver Skill noch nennt
|
||||
# (ampel-waechter.sh, night-cron-wrapper.sh, pc_path_lookup.*).
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Altrest:
|
||||
id: str
|
||||
name: str
|
||||
pfade: tuple[str, ...] # "~/…" (Home des Dienst-Nutzers) oder absolut
|
||||
hinweis: str
|
||||
art: str = "ordner" # "ordner" | "worktree" (danach git worktree prune) | "unit" (danach daemon-reload)
|
||||
sperre: tuple[str, ...] = () # User-Units, die weder laufen noch für den Autostart eingetragen sein dürfen
|
||||
sudo: bool = False # Elternordner gehört root (/opt): den geleerten Ordner mit sudo -n entfernen
|
||||
|
||||
|
||||
_UNITS = "~/.config/systemd/user/"
|
||||
_STEMPEL = "~/.local/share/systemd/timers/stamp-"
|
||||
_SKRIPTE = "~/.hermes/scripts/"
|
||||
|
||||
ALTRESTE: tuple[Altrest, ...] = (
|
||||
Altrest("zonos2-test", "TTS-Test Zonos2", ("~/zonos2-test",),
|
||||
"Test einer anderen Sprachausgabe vom 04.09. Lucys Stimme ist pocket-tts (~/.lucy-stimme); "
|
||||
"nichts ruft diesen Ordner auf."),
|
||||
Altrest("rocm-build", "Alter ROCm-Build von llama.cpp", ("/opt/llamacpp",),
|
||||
"Motor aus der ROCm-Zeit (Juni). Seit dem Vulkan-Verdikt läuft er aus /opt/llamacpp-vulkan, und "
|
||||
"llama-swap verweist nicht mehr hierher. Zurück zu ROCm ginge danach nur mit einem neuen Download.",
|
||||
sudo=True),
|
||||
Altrest("mem0", "mem0-Umgebung", ("~/.mem0",),
|
||||
"Gedächtnis-Sidecar, am 27.08. abgelöst: Hermes führt das Gedächtnis selbst. Nur die abgeschaltete "
|
||||
"Unit mem0-service verweist noch darauf.", sperre=("mem0-service.service",)),
|
||||
Altrest("kanban", "Kanban-Arbeitsordner", ("~/.hermes/kanban",),
|
||||
"Arbeitsordner, Anhänge und Protokolle der Kanban-Aufgaben aus Juli und August. Kanban ist in Hermes "
|
||||
"abgeschaltet; die kleine Datenbank kanban.db bleibt liegen, weil Hermes sie noch öffnet."),
|
||||
Altrest("alte-venvs", "Alte Python-Umgebungen", ("~/.venv-audit", "~/.venv-lint", "~/.litellm-venv"),
|
||||
"Umgebungen für den früheren venv-Audit, Lint-Versuche und LiteLLM (der Gateway ist seit Juli "
|
||||
"eingebaut). Kein Dienst und kein Job nutzt sie."),
|
||||
Altrest("audiocpp-test", "audio.cpp-Test", ("~/audiocpp-test",),
|
||||
"Test von audio.cpp mit Vulkan vom 04.09., nie in Betrieb gegangen."),
|
||||
Altrest("avatar", "Avatar in der Oberfläche", ("~/mission-control-v2/frontend/dist/avatar.vrm",),
|
||||
"VRM-Avatar vom 25.07., nie im Repo. Die Oberfläche lädt ihn nicht (kein Abruf in 30 Tagen), "
|
||||
"Lucy-Desktop bringt einen eigenen mit."),
|
||||
Altrest("worktrees", "Alte Git-Arbeitskopien",
|
||||
("~/.hermes/worktrees/orch-graph-verknuepfer", "~/.hermes/worktrees/orch-graph-verknuepfer-work",
|
||||
"~/.hermes/worktrees/wartung-remove-openrouter", "~/projekte/mc2-referenz",
|
||||
"~/mission-control-v2/.worktrees/t_b7b426dc"),
|
||||
"Arbeitskopien alter Aufgaben aus Juli und August, weit hinter main. Ihre Zweige bleiben im Box-Repo "
|
||||
"erhalten; verloren gingen nur Änderungen, die dort nie committet wurden.", art="worktree"),
|
||||
Altrest("test-reste", "Test-Reste im Home-Ordner",
|
||||
("~/v2test", "~/bench_ab", "~/stimm_varianten", "~/stimm_moods", "~/emotion_test", "~/english_probe",
|
||||
"~/zed-web-search-mcp", "~/cron_digest.py", "~/mc2-träum-duplikatscheck.patch"),
|
||||
"Hörproben der Stimm-Tests vom 09. bis 12.07., ein MCP-Server der abgelösten Zed-Bahn, ein Skript des "
|
||||
"alten Morgen-Digests und ein Patch aus der mem0-Zeit. Lucys Stimme nutzt ihre Referenz in "
|
||||
"~/.lucy-stimme."),
|
||||
Altrest("hermes-fremd", "Fremde Dateien im Hermes-Quellbaum",
|
||||
("~/.hermes/hermes-agent/hermes_cli/mc2_kanban_reaper.py",
|
||||
"~/.hermes/hermes-agent/hermes_cli/web_dist.bak-rootbuild"),
|
||||
"Ein alter Kanban-Aufräumer und eine Sicherung der Web-Oberfläche, beide nicht von Hermes. Nichts lädt "
|
||||
"sie; Hermes selbst bleibt unverändert."),
|
||||
Altrest("hermes-skripte", "Alte Skripte in ~/.hermes/scripts",
|
||||
tuple(_SKRIPTE + s for s in (
|
||||
"blogwatcher-logger.py", "briefing-date.sh", "fremdblick.sh", "gitea-repo-create.sh",
|
||||
"hermes-release-radar-feed.sh", "news-melden.sh.bak-20260923", "radar-feed.sh", "restore-probe.sh",
|
||||
"selbstkritik-feed.sh", "venv-audit.sh", "worker.sh")),
|
||||
"Skripte früherer Cron-Jobs (Werkstatt, Ampel, alte Radare). Kein Job, kein Cron und kein aktiver "
|
||||
"Skill ruft sie auf; deploy.sh legt nur die heutigen Skripte dorthin."),
|
||||
Altrest("mc2-memory", "Plugin mc2-memory", ("~/.hermes/plugins/mc2-memory",),
|
||||
"Gedächtnis-Plugin aus der mem0-Zeit, in Hermes nicht aktiviert und nicht mehr im Repo."),
|
||||
Altrest("pinned-version", "Veraltete PINNED_VERSION", ("~/.hermes/PINNED_VERSION",),
|
||||
"Sagt noch „v0.17.0“ und führt in die Irre: Festgehaltene Versionen stehen in /srv/models/mc2-pins.json."),
|
||||
Altrest("alt-units", "Abgeschaltete Alt-Units",
|
||||
tuple(_UNITS + u for u in (
|
||||
"mem0-service.service", "mem0-service.service.bak-174750", "mc2-morgen-digest.service",
|
||||
"mc2-morgen-digest.timer", "mc2-selfsmoke.service", "mc2-selfsmoke.timer",
|
||||
"hermes-dashboard.service", "lucy-stimme.service.bak-20260904"))
|
||||
+ (_STEMPEL + "mc2-morgen-digest.timer", _STEMPEL + "mc2-selfsmoke.timer"),
|
||||
"Units früherer Dienste (mem0, Morgen-Digest, Selbsttest, altes Hermes-Dashboard) samt zwei Sicherungen "
|
||||
"davon, alle abgeschaltet; ihre Skripte gibt es nicht mehr. Danach liest systemd die Units neu ein.",
|
||||
art="unit",
|
||||
sperre=("mem0-service.service", "mc2-morgen-digest.service", "mc2-morgen-digest.timer",
|
||||
"mc2-selfsmoke.service", "mc2-selfsmoke.timer", "hermes-dashboard.service")),
|
||||
)
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
ALTRESTE_CACHE_S = 300 # Größen messen dauert (os.walk über ~20 000 Dateien); sie ändern sich selten
|
||||
_altreste_lock = threading.Lock()
|
||||
_altreste_cache: dict = {"ts": 0.0, "liste": None}
|
||||
|
||||
|
||||
def _home() -> Path:
|
||||
return Path(os.environ.get("MC_ALTRESTE_HOME", str(Path.home())))
|
||||
|
||||
|
||||
def _pfad(roh: str) -> Path:
|
||||
return _home() / roh[2:] if roh.startswith("~/") else Path(roh)
|
||||
|
||||
|
||||
def _belegt(pfad: Path) -> int:
|
||||
"""Bytes einer Datei oder eines Ordners, ohne Verknüpfungen zu folgen (ein venv zeigt sonst aufs System-Python)."""
|
||||
try:
|
||||
st = pfad.lstat()
|
||||
except OSError:
|
||||
return 0
|
||||
if not stat.S_ISDIR(st.st_mode):
|
||||
return st.st_size
|
||||
summe = 0
|
||||
for wurzel, _, dateien in os.walk(pfad):
|
||||
for datei in dateien:
|
||||
try:
|
||||
summe += os.lstat(os.path.join(wurzel, datei)).st_size
|
||||
except OSError:
|
||||
pass
|
||||
return summe
|
||||
|
||||
|
||||
def _menge(n: int) -> str:
|
||||
"""Größe in Worten, dezimal wie die Modell-Kandidaten: „7,7 GB“, „323 MB“, „44 KB“."""
|
||||
if n >= 1e9:
|
||||
return f"{n / 1e9:.1f} GB".replace(".", ",")
|
||||
if n >= 1e6:
|
||||
return f"{n / 1e6:.0f} MB"
|
||||
if n >= 1e3:
|
||||
return f"{n / 1e3:.0f} KB"
|
||||
return "unter 1 KB"
|
||||
|
||||
|
||||
def _befehl(args: list[str]) -> tuple[bool, str]:
|
||||
"""Befehle (systemctl, git, sudo) über eine Schicht, die Tests ersetzen. Ohne das Programm: (False, Grund)."""
|
||||
try:
|
||||
r = subprocess.run(args, capture_output=True, text=True, timeout=60)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
return False, f"{exc.__class__.__name__}: {exc}"
|
||||
return r.returncode == 0, ((r.stdout if r.returncode == 0 else (r.stderr or r.stdout)) or "").strip()[:300]
|
||||
|
||||
|
||||
def _vorhanden(a: Altrest) -> list[tuple[str, Path]]:
|
||||
return [(roh, p) for roh in a.pfade if (p := _pfad(roh)).exists() or p.is_symlink()]
|
||||
|
||||
|
||||
def _sperrgrund(a: Altrest) -> str | None:
|
||||
"""Läuft eine der Units noch oder steht sie im Autostart, wird der Rest gebraucht — dann nicht anbieten."""
|
||||
for unit in a.sperre:
|
||||
ok, ausgabe = _befehl(["systemctl", "--user", "show", unit, "-p", "ActiveState,UnitFileState"])
|
||||
z = dict(zeile.split("=", 1) for zeile in ausgabe.splitlines() if "=" in zeile) if ok else {}
|
||||
if z.get("ActiveState") in ("active", "activating", "reloading"):
|
||||
return f"{unit} läuft gerade, {a.name} wird also noch gebraucht."
|
||||
if z.get("UnitFileState") in ("enabled", "enabled-runtime", "linked", "linked-runtime"):
|
||||
return f"{unit} steht im Autostart, {a.name} wird also noch gebraucht."
|
||||
return None
|
||||
|
||||
|
||||
def altreste(frisch: bool = False) -> list[dict]:
|
||||
"""Die Altreste der festen Liste, die noch da und nicht in Gebrauch sind — größte zuerst. Löscht nichts."""
|
||||
with _altreste_lock:
|
||||
if not frisch and _altreste_cache["liste"] is not None \
|
||||
and time.time() - _altreste_cache["ts"] < ALTRESTE_CACHE_S:
|
||||
return [dict(e) for e in _altreste_cache["liste"]]
|
||||
liste = []
|
||||
for a in ALTRESTE:
|
||||
da = _vorhanden(a)
|
||||
if not da or _sperrgrund(a):
|
||||
continue
|
||||
groesse = sum(_belegt(p) for _, p in da)
|
||||
liste.append({"id": a.id, "name": a.name, "pfade": [roh for roh, _ in da], "groesse_bytes": groesse,
|
||||
"groesse": _menge(groesse), "hinweis": a.hinweis})
|
||||
liste.sort(key=lambda e: -e["groesse_bytes"])
|
||||
with _altreste_lock:
|
||||
_altreste_cache.update(ts=time.time(), liste=liste)
|
||||
return [dict(e) for e in liste]
|
||||
|
||||
|
||||
def _entfernen(pfad: Path, a: Altrest) -> None:
|
||||
if pfad.is_symlink() or not pfad.is_dir():
|
||||
pfad.unlink() # eine Verknüpfung selbst, nie ihr Ziel
|
||||
return
|
||||
try:
|
||||
shutil.rmtree(pfad)
|
||||
except PermissionError:
|
||||
if not a.sudo:
|
||||
raise
|
||||
# /opt gehört root: Den Inhalt darf der Dienst-Nutzer löschen, den geleerten Ordner selbst nur mit sudo.
|
||||
ok, grund = _befehl(["sudo", "-n", "rm", "-rf", "--", str(pfad)])
|
||||
if not ok:
|
||||
raise PermissionError(f"sudo -n rm ging nicht ({grund})") from None
|
||||
|
||||
|
||||
def altrest_loeschen(kennung: str) -> dict:
|
||||
"""Knopf „Löschen“ eines Altrests: nur Einträge der festen Liste, nur was da ist, nur wenn nichts es braucht."""
|
||||
a = next((x for x in ALTRESTE if x.id == kennung), None)
|
||||
if a is None:
|
||||
return {"ok": False, "detail": "Diesen Altrest kennt die Liste nicht."}
|
||||
if not AUF_DER_BOX:
|
||||
return {"ok": False, "detail": "Löschen geht nur auf der Box."}
|
||||
da = _vorhanden(a)
|
||||
if not da:
|
||||
return {"ok": False, "detail": f"{a.name}: schon weg."}
|
||||
if (grund := _sperrgrund(a)):
|
||||
return {"ok": False, "detail": grund}
|
||||
frei, fehler = 0, []
|
||||
for roh, pfad in da:
|
||||
groesse = _belegt(pfad)
|
||||
try:
|
||||
_entfernen(pfad, a)
|
||||
frei += groesse
|
||||
except OSError as exc:
|
||||
fehler.append(f"{roh} ({exc.strerror or exc})")
|
||||
if a.art == "worktree": # Git vergisst die gelöschten Arbeitskopien, die Zweige bleiben im Repo
|
||||
_befehl(["git", "-C", str(REPO), "worktree", "prune"])
|
||||
elif a.art == "unit":
|
||||
_befehl(["systemctl", "--user", "daemon-reload"])
|
||||
with _altreste_lock:
|
||||
_altreste_cache.update(ts=0.0, liste=None)
|
||||
if fehler:
|
||||
return {"ok": False, "detail": f"{a.name} nur zum Teil gelöscht ({_menge(frei)} frei). Geblieben: "
|
||||
+ "; ".join(fehler)}
|
||||
return {"ok": True, "text": f"{a.name} gelöscht, {_menge(frei)} frei."}
|
||||
|
||||
Reference in New Issue
Block a user