Ampel / ampel (push) Successful in 26s
Ballast raus: - 35 Routen ohne Nutzer entfernt (agent/*, fit, roles, ctx, drafts, groups, routing/policy, system/history, system/self-update, maintenance/reboot, zeitmaschine/inhalt, zeitplan, voice/health|metrics|trace|voices|reference|tts). Von 95 auf 60. - Tote Module geloescht: agent-Router, roles, agent_aktivitaet, metrics_history (samt 10-s-Sampler), voice_metrics, migrate_config, parse_mc2_timeout, scripts/. - Unbenutzte Funktionen und Konstanten entfernt (Modell-Upgrade-Empfehlung, Draft-/Kontext- Setzer, Konsole, PC-Ausfuehrer-Probe, Routing-Policy-Editor ...). Robuster: - Jobs in eigener Prozessgruppe (Abbrechen beendet wirklich alles), Zeitlimit je Job-Art, start_job_exklusiv: zwei Klicks starten kein doppeltes Update mehr; alte Jobs raeumen sich auf. - Update-Pruefung meldet Fehler (pruef_fehler, Lampe "Pruefung unklar") statt "aktuell". - Nach jedem Update sofort neu pruefen (update_stand) statt 10 Minuten alten Stand zeigen. - llama-swap-Config: Sperre (RLock + flock) fuer UI, Radar, Aufraeumen und Hirn-Umstellung. - Hermes-Config: bei Lesefehler nichts schreiben, atomar, mit Sicherung. - Live-Strom und Gateway-Warnung blockieren den Event-Loop nicht mehr (Lucy, OpenChamber). - Gateway antwortet bei Engine-Ausfall im OpenAI-Fehlerformat (502) statt nacktem 500. - Abgestuerzte Waechter-Pruefung wird ein gelber Hinweis statt still zu verschwinden. - Download laedt nur den gewuenschten Quant (vorher bei Fehlen alle Teile aller Varianten), Download-Jobs in Gruppe "download"; HF-Suche kodiert den Suchbegriff. - Herkunftspruefung: schreibende /api-Aufrufe fremder Webseiten werden abgelehnt (keine Anmeldung, User-Entscheid); Skripte, Desktop-Lucy und /v1 unveraendert. - Modellpfade: Eintragen und Loeschen nur innerhalb von MODELS_DIR. - Dienste-Liste fragt keine abgebauten Dienste mehr ab (PC-Ausfuehrer haette 3 s gekostet). - SSE-Fehlerzeilen von /api/voice/chat als gueltiges JSON. - mission-control-2.service: --timeout-graceful-shutdown 3 (Neustart ohne 10-s-Haenger). Tests: 92 gruen (neu: Herkunft, Quant-Auswahl, abgestuerzte Pruefung). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""
|
|
Voll-Zustands-Backup (Hermes-Configs/Secrets + llama-swap config).
|
|
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
|
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
|
"""
|
|
|
|
import subprocess
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
from config import MODELS_DIR
|
|
|
|
BACKUP_DIR = Path(MODELS_DIR) / "mc2-backups"
|
|
SRC_ROOT = Path(__file__).resolve().parents[2]
|
|
BACKUP_SH = SRC_ROOT / "deploy" / "backup.sh"
|
|
|
|
|
|
def _ts(p: Path) -> str:
|
|
"""Zeitstempel aus 'mc2-state-<ts>.tar.gz' (Path.stem ließe '.tar' stehen)."""
|
|
return p.name[len("mc2-state-"):-len(".tar.gz")]
|
|
|
|
|
|
def _latest() -> Path | None:
|
|
if not BACKUP_DIR.exists():
|
|
return None
|
|
snaps = sorted(BACKUP_DIR.glob("mc2-state-*.tar.gz"), reverse=True)
|
|
return snaps[0] if snaps else None
|
|
|
|
|
|
def _components(tarball: Path) -> list[str]:
|
|
"""Top-Level-Einträge im Tarball (zur Anzeige im UI)."""
|
|
try:
|
|
with tarfile.open(tarball, "r:gz") as t:
|
|
top = {m.name.split("/")[1] for m in t.getmembers()
|
|
if m.name.startswith("./") and "/" in m.name[2:]}
|
|
top |= {m.name[2:] for m in t.getmembers()
|
|
if m.name.startswith("./") and "/" not in m.name[2:] and m.isfile()}
|
|
return sorted(x for x in top if x)
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def backup_now() -> dict:
|
|
"""Erstellt einen Voll-Zustands-Snapshot via deploy/backup.sh."""
|
|
try:
|
|
r = subprocess.run(["/bin/bash", str(BACKUP_SH)], capture_output=True, text=True, timeout=180)
|
|
if r.returncode != 0:
|
|
return {"ok": False, "snapshot": "", "files": [], "error": (r.stderr or r.stdout).strip()[-300:]}
|
|
except Exception as exc:
|
|
return {"ok": False, "snapshot": "", "files": [], "error": str(exc)}
|
|
|
|
latest = _latest()
|
|
if not latest:
|
|
return {"ok": False, "snapshot": "", "files": [], "error": "Kein Backup erzeugt"}
|
|
return {
|
|
"ok": True,
|
|
"snapshot": _ts(latest),
|
|
"files": _components(latest),
|
|
"size_mb": round(latest.stat().st_size / 1_000_000, 2),
|
|
}
|
|
|
|
|
|
def list_backups() -> list[dict]:
|
|
if not BACKUP_DIR.exists():
|
|
return []
|
|
out = []
|
|
for p in sorted(BACKUP_DIR.glob("mc2-state-*.tar.gz"), reverse=True):
|
|
st = p.stat()
|
|
out.append({
|
|
"snapshot": _ts(p),
|
|
"file": p.name,
|
|
"size_mb": round(st.st_size / 1_000_000, 2),
|
|
"mtime": st.st_mtime, # für die Sicherungs-Lampe im Cockpit (Alter der letzten)
|
|
})
|
|
return out
|