Files
mission-control-v2/backend/routers/system.py
T
Hitonabi d0688a7b64 Wartung (B10): Restart-Allowlist vereinheitlichen
Der ServicesCard-"Anzeige- statt Unit-Name"-Bug ist behoben (Frontend nutzt
durchgehend x.unit). Der echte Rest-Fehler lag in der ZWEITEN Restart-Wahrheit:
/api/system/restart (system.py) + das MCP-Tool restart_service hatten eine
veraltete Allowlist ohne llama-swap (Engine) und hermes-terminal, dafuer mit
Geist-Eintrag hermes-webui. Ein Engine-Neustart per Sprache/MCP schlug daher mit
"nicht erlaubt" fehl.

Fix: /api/system/restart delegiert jetzt an services.maintenance.restart_service
(EINE Allowlist-Wahrheit, kennt System- via sudo -n UND User-Dienste, wird auch
von der UI genutzt). MCP-Tool-Docstring auf die echten Dienste korrigiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 19:45:34 +02:00

131 lines
4.7 KiB
Python

"""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 logging
import os
import subprocess
from fastapi import APIRouter
from pydantic import BaseModel
import httpx
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, VOICE_SERVICE_URL
from services import backup as backup_svc
from services import maintenance
from services.agent import agent_status
from services.gateway import gateway_reachable
from services.llamaswap import engine_reachable, list_models
from services.pricing import compute_savings
from services.system import system_status
from services.token_stats import get_stats
log = logging.getLogger(__name__)
router = APIRouter(prefix="/api")
# 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()
def _mem0_reachable() -> bool:
try:
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
except Exception:
return False
def _voice_reachable() -> bool:
try:
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
except Exception:
return False
@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)", "unit": "llama-swap", "url": LLAMA_SWAP_URL, "ok": engine_reachable()},
{"name": "Gateway (integriert)", "unit": "mission-control-2", "url": gw_url, "ok": gateway_reachable()},
{"name": "Hermes-Gateway", "unit": "hermes-gateway", "url": HERMES_API_URL, "ok": a["gateway_reachable"]},
{"name": "Hermes-Terminal", "unit": "hermes-terminal", "url": a["terminal_url"], "ok": a["terminal_reachable"]},
{"name": "Mem0 (Gedächtnis)", "unit": "mem0-service", "url": MEM0_SERVICE_URL, "ok": _mem0_reachable()},
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL, "ok": _voice_reachable()},
],
"links": {
"engine_ui": f"{LLAMA_SWAP_URL}/ui",
"gateway": gw_url,
"hermes_terminal": a["terminal_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:
"""Dienst neustarten. Delegiert an den Wartungs-Service (EINE Allowlist-Wahrheit):
der kennt System-Dienste (llama-swap, via sudo -n) UND User-Dienste und wird auch von
der UI (/api/maintenance/restart) genutzt. Vorher lag hier eine zweite, veraltete Liste
(ohne llama-swap/hermes-terminal, mit Geist-Eintrag hermes-webui) → Engine-Neustart per
Sprache/MCP schlug fehl."""
return maintenance.restart_service(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}
@router.get("/system/token-stats")
def token_stats() -> dict:
"""Token-Verbrauch + Cloud-Ersparnis. Logik im pricing-Service (SSoT)."""
# Rolle je Modell/Alias (lowercase) für die Tarif-Auflösung auflösen.
role_map: dict[str, str | None] = {}
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:
log.warning("token_stats: list_models fehlgeschlagen, Tarife per Name", exc_info=True)
return compute_savings(get_stats(), role_map)