diff --git a/app.py b/app.py index 92b7395..859afdf 100644 --- a/app.py +++ b/app.py @@ -20,13 +20,14 @@ from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles -from routers import jobs, maintenance, models +from routers import jobs, maintenance, models, system app = FastAPI(title="Mission Control") app.include_router(models.router) app.include_router(jobs.router) app.include_router(maintenance.router) +app.include_router(system.router) _STATIC = Path(__file__).parent / "static" diff --git a/docs/mission-control-overview.png b/docs/mission-control-overview.png new file mode 100644 index 0000000..1d112e9 Binary files /dev/null and b/docs/mission-control-overview.png differ diff --git a/requirements.txt b/requirements.txt index 284c855..e04c6be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ fastapi>=0.110 uvicorn[standard]>=0.29 httpx>=0.27 ruamel.yaml>=0.18 +psutil>=5.9.0 diff --git a/routers/system.py b/routers/system.py new file mode 100644 index 0000000..6e03ca7 --- /dev/null +++ b/routers/system.py @@ -0,0 +1,91 @@ +""" +System-Router: Liefert Live-Auslastung (CPU, RAM, Disk, GPU, Temp). +Greift lokal auf psutil und sysfs zu. +""" + +from pathlib import Path +import psutil + +from fastapi import APIRouter, Depends +from auth import auth + +router = APIRouter(prefix="/api/system", dependencies=[Depends(auth)]) + +def _read_sysfs_int(path: Path) -> int | None: + try: + if path.exists(): + return int(path.read_text().strip()) + except Exception: + pass + return None + +def _get_gpu_mem(): + # Suche in /sys/class/drm/ nach card*, die mem_info_* hat + drm_dir = Path("/sys/class/drm") + if not drm_dir.exists(): + return None + + for card in drm_dir.glob("card*"): + dev_dir = card / "device" + vram_total = _read_sysfs_int(dev_dir / "mem_info_vram_total") + if vram_total is not None: + return { + "vram": { + "used": _read_sysfs_int(dev_dir / "mem_info_vram_used") or 0, + "total": vram_total, + }, + "gtt": { + "used": _read_sysfs_int(dev_dir / "mem_info_gtt_used") or 0, + "total": _read_sysfs_int(dev_dir / "mem_info_gtt_total") or 0, + } + } + return None + +def _get_temperatures(): + temps = {"cpu": None, "gpu": None} + hwmon_dir = Path("/sys/class/hwmon") + if not hwmon_dir.exists(): + return temps + + for hw in hwmon_dir.glob("hwmon*"): + try: + name = (hw / "name").read_text().strip() + temp_val = _read_sysfs_int(hw / "temp1_input") + if temp_val is not None: + if name == "k10temp": + temps["cpu"] = temp_val / 1000.0 + elif name == "amdgpu": + temps["gpu"] = temp_val / 1000.0 + except Exception: + continue + return temps + +@router.get("/status") +def system_status(): + temps = _get_temperatures() + + cpu_percent = psutil.cpu_percent(interval=None) + ram = psutil.virtual_memory() + disk = psutil.disk_usage("/") + + return { + "cpu": { + "percent": cpu_percent, + "temp": temps["cpu"] + }, + "ram": { + "used": ram.used, + "total": ram.total, + "percent": ram.percent + }, + "disk": { + "used": disk.used, + "total": disk.total, + "percent": disk.percent + }, + "gpu": _get_gpu_mem() or { + "vram": {"used": 0, "total": 0}, + "gtt": {"used": 0, "total": 0} + }, + "gpu_temp": temps["gpu"] + } diff --git a/static/js/main.js b/static/js/main.js index 75cabf5..978bdce 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -13,6 +13,7 @@ import jobs from "./panels/jobs.js"; const panels = [overview, models, maintenance, jobs]; let lastJobs = []; +let lastSystem = null; // ---- Topbar / Alert aus dem Status ableiten ---- function applyStatus(s) { @@ -40,6 +41,11 @@ function applyJobs(jobs) { for (const p of panels) p.onJobs?.(lastJobs); } +function applySystem(sys) { + lastSystem = sys; + for (const p of panels) p.onSystem?.(sys); +} + function showAlert(html, warn) { const a = $("#alert"); a.className = "alert" + (warn ? " warn" : ""); @@ -58,6 +64,11 @@ async function pollJobs() { catch { /* still */ } } +async function pollSystem() { + try { applySystem(await api("/api/system/status")); } + catch { /* still */ } +} + // ---- Boot ---- function bootToken() { const i = $("#token"); @@ -76,6 +87,8 @@ document.addEventListener("mc:refresh", pollStatus); pollStatus(); pollJobs(); +pollSystem(); setInterval(tickClock, 1000); setInterval(pollStatus, 3000); setInterval(pollJobs, 1500); +setInterval(pollSystem, 3000); diff --git a/static/js/panels/overview.js b/static/js/panels/overview.js index 96510e6..5fdf88c 100644 --- a/static/js/panels/overview.js +++ b/static/js/panels/overview.js @@ -5,6 +5,7 @@ import { $, icon, esc } from "../core/ui.js"; let S = null; // letzter Status let J = []; // letzte Job-Liste +let SYS = null; // letzte System-Auslastung const RUNNING = new Set(["running", "ready", "loading", "starting"]); @@ -52,6 +53,9 @@ function kpi(cls, title, ic, value, sub) { function renderKpis() { const c = counts(); + const sysV = SYS ? `${SYS.cpu.percent.toFixed(0)}% CPU` : "n/a"; + const sysS = SYS ? `${SYS.ram.percent.toFixed(0)}% RAM, ${SYS.gpu_temp ? SYS.gpu_temp.toFixed(0)+'°C' : SYS.cpu.temp ? SYS.cpu.temp.toFixed(0)+'°C' : '–'}` : "bald · Live-Auslastung"; + $("#kpis").innerHTML = kpi(c.swap ? "green" : "red", "llama-swap", "swap", c.swap ? "Online" : "Offline", "Transport-Status") + @@ -59,7 +63,7 @@ function renderKpis() { `${c.running}/${c.total}`, "aktiv / gesamt") + kpi("purple", "Jobs", "layers", c.jobsRun, "laufend") + kpi(c.jobsErr ? "red" : "muted", "Fehler", "alert", c.jobsErr, "in der Aktivität") + - kpi("muted", "System-Last", "gauge", "n/a", "bald · Live-Auslastung"); + kpi(SYS ? "blue" : "muted", "System-Last", "gauge", sysV, sysS); } function kvRow(k, v, cls = "") { @@ -68,6 +72,16 @@ function kvRow(k, v, cls = "") { function renderHealth() { const c = counts(); + + let sysRow = kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na"); + if (SYS) { + const gb = b => (b / 1024 / 1024 / 1024).toFixed(1); + const ram = `${gb(SYS.ram.used)}GB / ${gb(SYS.ram.total)}GB`; + const gpu = (SYS.gpu && SYS.gpu.vram.total) ? `${gb(SYS.gpu.vram.used + SYS.gpu.gtt.used)}GB / ${gb(SYS.gpu.vram.total + SYS.gpu.gtt.total)}GB` : "–"; + const disk = `${SYS.disk.percent.toFixed(0)}%`; + sysRow = kvRow("Auslastung (RAM/GPU/Disk)", `${ram} | ${gpu} | ${disk}`); + } + $("#health").innerHTML = `