"""System-Endpoints: Live-Status + Wartung (Restart/Self-Update — auf der Box). Wartung läuft als systemd-USER-Dienst → KEIN sudo/Passwort (Nordstern). Lokal (Windows) schlagen die Shell-Befehle harmlos fehl und werden als Fehler zurückgegeben statt zu crashen. """ import os import subprocess from fastapi import APIRouter, HTTPException from pydantic import BaseModel from config import GATEWAY_URL, HERMES_API_URL, HERMES_WEBUI_URL, LLAMA_SWAP_URL from services import backup as backup_svc from services.agent import agent_status from services.gateway import gateway_reachable from services.llamaswap import engine_reachable from services.system import system_status router = APIRouter(prefix="/api") # Nur diese User-Dienste dürfen neugestartet werden. ALLOWED_SERVICES = {"mission-control-2", "hermes-gateway", "hermes-webui"} # Quelle für Self-Update (auf der Box ~/mission-control-v2). SOURCE_DIR = os.path.expanduser(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2")) @router.get("/system/status") def status() -> dict: return system_status() @router.get("/system/services") def services() -> dict: """Aggregierte Erreichbarkeit aller Stack-Dienste (für die Health-Anzeige).""" a = agent_status() gw_url = f"{GATEWAY_URL}/v1" return { "services": [ {"name": "Engine (llama-swap)", "url": LLAMA_SWAP_URL, "ok": engine_reachable()}, {"name": "Gateway (integriert)", "url": gw_url, "ok": gateway_reachable()}, {"name": "Hermes-Gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]}, {"name": "Hermes-WebUI", "url": HERMES_WEBUI_URL, "ok": a["webui_reachable"]}, ], "links": { "engine_ui": f"{LLAMA_SWAP_URL}/ui", "gateway": gw_url, "hermes_webui": HERMES_WEBUI_URL, }, } @router.post("/system/backup") def backup() -> dict: return backup_svc.backup_now() @router.get("/system/backups") def backups() -> dict: return {"backups": backup_svc.list_backups()} def _run(cmd: list[str], cwd: str | None = None) -> dict: try: p = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=180) return {"ok": p.returncode == 0, "code": p.returncode, "out": (p.stdout or "")[-2000:], "err": (p.stderr or "")[-2000:]} except Exception as exc: # noqa: BLE001 return {"ok": False, "code": -1, "out": "", "err": str(exc)} class RestartReq(BaseModel): service: str @router.post("/system/restart") def restart(req: RestartReq) -> dict: if req.service not in ALLOWED_SERVICES: raise HTTPException(400, f"Dienst '{req.service}' nicht erlaubt.") return _run(["systemctl", "--user", "restart", req.service]) @router.post("/system/self-update") def self_update() -> dict: """git pull (Source) → venv-Deps → Dienst-Restart. Auf der Box; lokal Fehler.""" pull = _run(["git", "fetch", "--all"], cwd=SOURCE_DIR) reset = _run(["git", "reset", "--hard", "origin/main"], cwd=SOURCE_DIR) restart_res = _run(["systemctl", "--user", "restart", "mission-control-2"]) return {"pull": pull, "reset": reset, "restart": restart_res} from services.token_stats import get_stats @router.get("/system/token-stats") def token_stats() -> dict: stats = get_stats() p = stats.get("prompt_tokens", 0) c = stats.get("completion_tokens", 0) total = p + c # Juni 2026 API-Preise (deutlich gestiegen): # Premium-Modelle (Claude 4 / GPT-5): 15,00 $ / 1M Input-Tokens und 75,00 $ / 1M Output-Tokens saved_usd = (p * 15.0 + c * 75.0) / 1_000_000.0 saved_eur = saved_usd * 0.92 # 1 USD = 0.92 EUR return { "prompt_tokens": p, "completion_tokens": c, "total_tokens": total, "saved_usd": round(saved_usd, 2), "saved_eur": round(saved_eur, 2) }