"""Zeitmaschine — Snapshots ansehen und per Klick zu einem Stand zurückkehren. Die Maschinerie existiert komplett (deploy/backup.sh Timer 03:30, Off-Box-Kopie, deploy/restore.sh mit Pre-Restore-Sicherung); hier kommt nur der letzte Meter dazu: Liste + Inhalt in der UI und ein Restore-Start als EIGENE systemd-Unit — restore.sh stoppt mission-control-2 selbst, als Kind des Backends stürbe der Lauf mittendrin.""" import os import re import subprocess import time from pathlib import Path from fastapi import APIRouter, HTTPException from pydantic import BaseModel from services import backup as backup_svc router = APIRouter(prefix="/api") _REPO_ROOT = Path(__file__).resolve().parents[2] RESTORE_SH = _REPO_ROOT / "deploy" / "restore.sh" _FILE_RX = re.compile(r"^mc2-state-[0-9T:_-]+\.tar\.gz$") class RestoreIn(BaseModel): file: str @router.get("/zeitmaschine") def list_snapshots() -> dict: return {"available": os.name == "posix", "backups": backup_svc.list_backups()} @router.get("/zeitmaschine/inhalt") def snapshot_contents(file: str) -> dict: """Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …).""" if not _FILE_RX.match(file): raise HTTPException(400, "Ungültiger Snapshot-Name.") p = backup_svc.BACKUP_DIR / file if not p.is_file(): raise HTTPException(404, "Snapshot nicht gefunden.") return {"file": file, "components": backup_svc.snapshot_components(p)} @router.post("/zeitmaschine/restore") def restore(body: RestoreIn) -> dict: """Wiederherstellung starten (detached). restore.sh macht VORHER selbst ein Sicherheits-Backup des aktuellen Zustands — der Schritt ist also reversibel.""" if os.name != "posix": raise HTTPException(400, "Wiederherstellen geht nur auf der Box.") if not _FILE_RX.match(body.file): raise HTTPException(400, "Ungültiger Snapshot-Name.") if not (backup_svc.BACKUP_DIR / body.file).is_file(): raise HTTPException(404, "Snapshot nicht gefunden.") try: from services import announce announce.add(f"Zeitmaschine: Wiederherstellung von {body.file} gestartet — " "die Dienste starten gleich neu.", "[Zeitmaschine]", "zeitmaschine", "silent") except Exception: pass unit = f"mc2-restore-{int(time.time())}" r = subprocess.run( ["systemd-run", "--user", "--collect", f"--unit={unit}", "/bin/bash", str(RESTORE_SH), "--yes", body.file], capture_output=True, text=True, timeout=20) if r.returncode != 0: raise HTTPException(500, f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}") return {"ok": True, "unit": unit}