services/ablaeufe.py: je systemd-Timer der Box der erwartete Takt plus Karenz (LastTriggerUSec über --timestamp=unix), Hermes-Cron-Jobs mit verpasstem next_run_at, der Runner der Gitea-Ampel (Admin-API, Token aus ~/.git-credentials, nie ausgegeben). Überfällig oder gestoppt → Hinweis nach fünf Takten, die Sonntags-Updates rot. GET /api/system/ablaeufe; Panel „Zeitpläne“ unter den Diensten. Tests fragen Gitea nie echt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.8 KiB
Python
79 lines
2.8 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 ablaeufe, dienste, maintenance
|
|
from services import backup as backup_svc
|
|
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.get("/system/ablaeufe")
|
|
def ablaeufe_liste() -> dict:
|
|
"""Die geplanten Abläufe der Box mit letztem und nächstem Lauf — der Totmannschalter (services/ablaeufe.py)."""
|
|
return {"ablaeufe": ablaeufe.ablaeufe()}
|
|
|
|
|
|
@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)
|