Auftragsbuch/Chronik/Wissen/Zeitmaschine: Backend + Annahme-Runner (Ein-Klick-Gate)
Der letzte Meter des propose-only-Kreislaufs: Vorschlags-Branches (Werkstatt/ Orchestrator) und Traum-Skill-Kandidaten werden als API sichtbar; Annehmen laeuft als detached systemd-Unit (Merge im Worktree -> Push main -> Deploy -> Health -> Auto-Revert bei Rot). Dazu: Chronik-Endpoint (Announce-Store als Timeline), Wissens-Vault-Reader (read-only, Traversal-Guard), Zeitmaschine (Snapshots + detached Restore) und Morgenlage-Spiegel im Chef-Gutachter-Feed (priority=silent). python-multipart explizit in requirements (voice braucht es, war implizit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user