Files
mission-control-v2/backend/routers/system.py
T
HitonabiandClaude Opus 5.5 cf01a5bb85 dienste: eigene Seite /box/dienste mit Tabelle wie die Geräte im Homelab
Die Schublade „Dienste und Protokolle" war eine schlichte Liste (Punkt, Name, „läuft",
zwei Knöpfe). Jetzt eine Seite im Bereich KI-Box, gebaut wie die Geräte-Tabelle im
Homelab-Cockpit: je Dienst Zustand in Worten (läuft, schläft, gestoppt, gescheitert,
antwortet nicht), CPU, Speicher, „Läuft seit" und Neustarts; Probleme oben. „Protokoll"
klappt unter der Zeile auf (das Neueste unten im Blick), Neu starten / Starten / Wecken
fragen vorher nach.

Entscheidungen:
- Seite statt Schublade: die Tabelle braucht die Breite, und der Eintrag steht jetzt wie
  die anderen Seiten in der Seitenleiste. Schublade samt onDienste-Weg entfernt.
- Speicher = MemoryCurrent als Zahl mit Einheit; der Balken zeigt den echten Anteil am
  Arbeitsspeicher der Box, keine erfundenen Prozente. Bei der Engine zählt der
  Grafikspeicher (GTT) mit — dort liegen die Modelle, und systemd rechnet ihn keinem
  Dienst zu (25.09.: 4,3 GB laut systemd, 27,6 GB Modelle).
- CPU = Anteil an allen Kernen aus CPUUsageNSec zwischen zwei Abfragen (die Seite fragt
  alle 10 s). Fehlt eine brauchbare Probe, misst die Box einmal 0,5 s nach. Ruhiges Blau
  ohne Warnfarben: ein Dienst hat keine Grenze, was zählt, zeigt der Zustand.
- Neustarts = NRestarts (automatische Neustarts nach Absturz), am Handy nur, wenn es
  welche gab; mehr als 0 rückt den Dienst nach oben.
- Backend: Liste und Kennzahlen in services/dienste.py (ein systemctl show je Bereich,
  auf Windows harmlos leer), Router dünn. Die Felder der MCP-Werkzeuge bleiben;
  waechter.dienst_zustand war nur für die alte Liste da und ist weg.
- Tabelle ab xl, Knöpfe ab 1400 px nebeneinander, darunter Karten mit zwei bzw. vier
  Spalten; bis 320 px keine waagrechte Scrollleiste.

Tests: backend/tests/test_dienste.py (echte systemctl-show-Ausgabe der Box),
frontend lib/dienste.test.ts und views/Dienste.test.tsx.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:28:57 +02:00

73 lines
2.5 KiB
Python

"""System-Endpoints: Live-Status, Dienste-Liste, Sicherung, Neustart, Token-Verbrauch.
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
from fastapi import APIRouter
from pydantic import BaseModel
from services import backup as backup_svc
from services import dienste, maintenance
from services.llamaswap import 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")
@router.get("/system/status")
def status() -> dict:
return system_status()
@router.get("/system/services")
def services() -> dict:
"""Alle Dienste der Box mit Erreichbarkeit, Zustand und Kennzahlen (Speicher, CPU, Laufzeit,
Neustarts) — die EINE Quelle für die Seite „Dienste und Protokolle“ und die MCP-Werkzeuge.
Logik in services/dienste.py; `scope`: user|system (Restart-Weg), `unit`: echter systemd-Name."""
return dienste.liste()
@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()}
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.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)