"""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 from services.llamaswap import list_models @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 # Map model IDs and aliases to their respective roles for pricing resolution role_map = {} try: for m in list_models(): role_map[m["name"].lower()] = m.get("role") for alias in m.get("aliases", []): role_map[alias.lower()] = m.get("role") except Exception: pass # Dynamic pricing tiers based on model class in June 2026 PRICING = { "heavy": (15.0, 75.0), "coder": (3.0, 15.0), "hermes": (1.0, 5.0), "fast": (0.15, 0.60), "scout": (0.15, 0.60), "vision": (0.15, 0.60), "reasoning": (0.15, 0.60), } modeled_p = 0 modeled_c = 0 saved_usd = 0.0 models_data = stats.get("models") or {} for m_name, m_tokens in models_data.items(): mp = m_tokens.get("prompt", 0) mc = m_tokens.get("completion", 0) modeled_p += mp modeled_c += mc role = role_map.get(m_name, m_name) rate_in, rate_out = PRICING.get(role, (0.15, 0.60)) saved_usd += (mp * rate_in + mc * rate_out) / 1_000_000.0 # Baseline/legacy tokens calculated at premium rates ($15.00 / $75.00) # to preserve historical savings value prior to model-specific logging baseline_p = max(0, p - modeled_p) baseline_c = max(0, c - modeled_c) saved_usd += (baseline_p * 15.0 + baseline_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) }