feat(2.0): Phase 2 — System/OS + Connect

System: services/system.py (psutil CPU/RAM/Disk, sysfs GPU/Temp guarded),
routers/system.py GET status + restart (Whitelist) + self-update (systemd-
USER, sudo-frei). Connect: services/connect.py (Cline/OpenCode/Zed/Continue/
Claude Code/Memory-MCP → Gateway model:auto, LAN-IP-Override), routers/
connect.py. Frontend: SystemView (Metrik-Bars) + ConnectView (Tool-Tabs,
Copy, IP-Override).

Lokal verifiziert: Backend-Smoke + Frontend-Build + Browser (System-Bars,
Connect-Snippets). Docs aktualisiert (README/STATUS/Plan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 08:08:43 +02:00
parent cff3f0b1a8
commit 6c8b6d81fe
17 changed files with 617 additions and 150 deletions
+105
View File
@@ -0,0 +1,105 @@
"""
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
(`model: auto`, LiteLLM :4000) + den **Shared-Memory-MCP** (MC :9000).
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
"""
import json
DEFAULT_HOST = "192.168.178.151"
GATEWAY_PORT = 4000
MC_PORT = 9000
# Modelle, die der Gateway anbietet (model:auto = Standard).
GATEWAY_MODELS = ["auto", "fast", "heavy", "coder", "vision"]
def _gw(host: str) -> str:
return f"http://{host}:{GATEWAY_PORT}/v1"
def build_snippets(host: str = DEFAULT_HOST,
mcp_script_path: str = r"C:\\Users\\TobisPC\\mission-control-v2\\mcp\\mcp_memory.py",
mcp_python: str = "python") -> dict:
gw = _gw(host)
mc_url = f"http://{host}:{MC_PORT}"
cline = json.dumps({
"apiProvider": "openai",
"openAiBaseUrl": gw,
"openAiApiKey": "local",
"openAiModelId": "auto",
}, indent=2)
opencode = json.dumps({
"providers": {
"litellm": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bosgame Gateway",
"options": {"baseURL": gw, "apiKey": "local"},
"models": {m: {"name": m} for m in GATEWAY_MODELS},
}
}
}, indent=2)
zed = json.dumps({
"language_models": {
"openai_compatible": {
"bosgame": {
"api_url": gw,
"available_models": [
{"name": m, "display_name": m, "max_tokens": 131072,
"capabilities": {"tools": True}} for m in GATEWAY_MODELS
],
}
}
}
}, indent=2)
cont = json.dumps({
"models": [
{"title": f"Bosgame / {m}", "provider": "openai", "model": m,
"apiBase": gw, "apiKey": "local"} for m in ("auto", "coder", "heavy")
]
}, indent=2)
# Claude Code: Anthropic-Format → LiteLLM kann /v1/messages anbieten.
claude_code = (
f'# Claude Code gegen den lokalen Gateway (Anthropic-kompatibel via LiteLLM):\n'
f'export ANTHROPIC_BASE_URL="http://{host}:{GATEWAY_PORT}"\n'
f'export ANTHROPIC_API_KEY="local"\n'
f'export ANTHROPIC_MODEL="auto"\n'
f'# (LiteLLM muss den Anthropic-/v1/messages-Endpunkt aktiviert haben — auf der Box prüfen.)'
)
memory_mcp = json.dumps({
"mcpServers": {
"mission-control-memory": {
"command": mcp_python,
"args": [mcp_script_path],
"env": {"MC_URL": mc_url},
}
}
}, indent=2)
return {
"host": host,
"gateway_url": gw,
"tools": {
"cline": {"label": "Cline (VS Code)", "lang": "json", "snippet": cline,
"note": "OpenAI-Provider → Gateway. Modell 'auto' (schnell, eskaliert bei Bedarf)."},
"opencode": {"label": "OpenCode", "lang": "jsonc", "snippet": opencode,
"note": "Datei opencode.jsonc, Key 'providers'."},
"zed": {"label": "Zed", "lang": "json", "snippet": zed,
"note": "settings.json → language_models.openai_compatible."},
"continue": {"label": "Continue", "lang": "json", "snippet": cont,
"note": "~/.continue/config.json."},
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
"note": "Anthropic-Format über LiteLLM /v1/messages."},
"memory_mcp": {"label": "Shared Memory (MCP)", "lang": "json", "snippet": memory_mcp,
"note": "Für jedes MCP-fähige Tool. mcp_memory.py muss lokal liegen."},
},
}
+72
View File
@@ -0,0 +1,72 @@
"""
System/OS-Metriken für die Box (Bosgame / Strix Halo).
CPU/RAM/Disk via psutil (plattformübergreifend). GPU-Auslastung/VRAM/Temperatur
via sysfs (amdgpu) — nur Linux; auf anderen Plattformen None (amd-smi fehlt auf
der Box, daher sysfs). Verschachtelte Struktur wie v1 (cpu.percent, ram.used Bytes).
"""
import glob
import os
import psutil
from config import MODELS_DIR
def _read_int(path: str) -> int | None:
try:
with open(path) as f:
return int(f.read().strip())
except Exception:
return None
def _gpu_sysfs() -> dict | None:
"""AMD-GPU-Auslastung + VRAM via sysfs (Linux). None, wenn nicht vorhanden."""
base = "/sys/class/drm/card0/device"
if not os.path.isdir(base):
return None
busy = _read_int(f"{base}/gpu_busy_percent")
vram_used = _read_int(f"{base}/mem_info_vram_used")
vram_total = _read_int(f"{base}/mem_info_vram_total")
if busy is None and vram_used is None:
return None
return {"busy_percent": busy, "vram_used": vram_used, "vram_total": vram_total}
def _temps() -> dict | None:
"""CPU/GPU-Temperatur via hwmon (Linux). None bei Fehlen."""
out: dict = {}
for hw in glob.glob("/sys/class/hwmon/hwmon*"):
name = ""
try:
with open(f"{hw}/name") as f:
name = f.read().strip()
except Exception:
continue
t = _read_int(f"{hw}/temp1_input")
if t is None:
continue
c = round(t / 1000.0, 1)
if name in ("k10temp", "zenpower", "coretemp"):
out["cpu"] = c
elif name in ("amdgpu", "edge"):
out["gpu"] = c
return out or None
def system_status() -> dict:
vm = psutil.virtual_memory()
try:
du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd())
disk = {"total": du.total, "used": du.used, "percent": du.percent}
except Exception:
disk = None
return {
"cpu": {"percent": psutil.cpu_percent(interval=0.1), "cores": psutil.cpu_count()},
"ram": {"total": vm.total, "used": vm.used, "percent": vm.percent},
"gpu": _gpu_sysfs(),
"temp": _temps(),
"disk": disk,
}