feat: live system metrics (CPU, RAM, Disk, GPU, Temp)

This commit is contained in:
Hitonabi
2026-06-20 21:19:58 +02:00
parent 6c0ba255fe
commit 08eab95178
6 changed files with 125 additions and 4 deletions
+2 -1
View File
@@ -20,13 +20,14 @@ from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from routers import jobs, maintenance, models from routers import jobs, maintenance, models, system
app = FastAPI(title="Mission Control") app = FastAPI(title="Mission Control")
app.include_router(models.router) app.include_router(models.router)
app.include_router(jobs.router) app.include_router(jobs.router)
app.include_router(maintenance.router) app.include_router(maintenance.router)
app.include_router(system.router)
_STATIC = Path(__file__).parent / "static" _STATIC = Path(__file__).parent / "static"
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

+1
View File
@@ -2,3 +2,4 @@ fastapi>=0.110
uvicorn[standard]>=0.29 uvicorn[standard]>=0.29
httpx>=0.27 httpx>=0.27
ruamel.yaml>=0.18 ruamel.yaml>=0.18
psutil>=5.9.0
+91
View File
@@ -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"]
}
+13
View File
@@ -13,6 +13,7 @@ import jobs from "./panels/jobs.js";
const panels = [overview, models, maintenance, jobs]; const panels = [overview, models, maintenance, jobs];
let lastJobs = []; let lastJobs = [];
let lastSystem = null;
// ---- Topbar / Alert aus dem Status ableiten ---- // ---- Topbar / Alert aus dem Status ableiten ----
function applyStatus(s) { function applyStatus(s) {
@@ -40,6 +41,11 @@ function applyJobs(jobs) {
for (const p of panels) p.onJobs?.(lastJobs); 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) { function showAlert(html, warn) {
const a = $("#alert"); const a = $("#alert");
a.className = "alert" + (warn ? " warn" : ""); a.className = "alert" + (warn ? " warn" : "");
@@ -58,6 +64,11 @@ async function pollJobs() {
catch { /* still */ } catch { /* still */ }
} }
async function pollSystem() {
try { applySystem(await api("/api/system/status")); }
catch { /* still */ }
}
// ---- Boot ---- // ---- Boot ----
function bootToken() { function bootToken() {
const i = $("#token"); const i = $("#token");
@@ -76,6 +87,8 @@ document.addEventListener("mc:refresh", pollStatus);
pollStatus(); pollStatus();
pollJobs(); pollJobs();
pollSystem();
setInterval(tickClock, 1000); setInterval(tickClock, 1000);
setInterval(pollStatus, 3000); setInterval(pollStatus, 3000);
setInterval(pollJobs, 1500); setInterval(pollJobs, 1500);
setInterval(pollSystem, 3000);
+18 -3
View File
@@ -5,6 +5,7 @@ import { $, icon, esc } from "../core/ui.js";
let S = null; // letzter Status let S = null; // letzter Status
let J = []; // letzte Job-Liste let J = []; // letzte Job-Liste
let SYS = null; // letzte System-Auslastung
const RUNNING = new Set(["running", "ready", "loading", "starting"]); const RUNNING = new Set(["running", "ready", "loading", "starting"]);
@@ -52,6 +53,9 @@ function kpi(cls, title, ic, value, sub) {
function renderKpis() { function renderKpis() {
const c = counts(); const c = counts();
const sysV = SYS ? `${SYS.cpu.percent.toFixed(0)}<small>% CPU</small>` : "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 = $("#kpis").innerHTML =
kpi(c.swap ? "green" : "red", "llama-swap", "swap", kpi(c.swap ? "green" : "red", "llama-swap", "swap",
c.swap ? "Online" : "Offline", "Transport-Status") + c.swap ? "Online" : "Offline", "Transport-Status") +
@@ -59,7 +63,7 @@ function renderKpis() {
`${c.running}<small>/${c.total}</small>`, "aktiv / gesamt") + `${c.running}<small>/${c.total}</small>`, "aktiv / gesamt") +
kpi("purple", "Jobs", "layers", c.jobsRun, "laufend") + kpi("purple", "Jobs", "layers", c.jobsRun, "laufend") +
kpi(c.jobsErr ? "red" : "muted", "Fehler", "alert", c.jobsErr, "in der Aktivität") + 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 = "") { function kvRow(k, v, cls = "") {
@@ -68,6 +72,16 @@ function kvRow(k, v, cls = "") {
function renderHealth() { function renderHealth() {
const c = counts(); 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 = ` $("#health").innerHTML = `
<div class="card-h"><h3>Stack-Gesundheit</h3> <div class="card-h"><h3>Stack-Gesundheit</h3>
<span class="meta ${c.swap ? "ok" : ""}">${c.swap ? "Connected" : "Offline"}</span></div> <span class="meta ${c.swap ? "ok" : ""}">${c.swap ? "Connected" : "Offline"}</span></div>
@@ -77,7 +91,7 @@ function renderHealth() {
${kvRow("Aktiv", c.running, c.running ? "ok" : "")} ${kvRow("Aktiv", c.running, c.running ? "ok" : "")}
${kvRow("Jobs (laufend)", c.jobsRun)} ${kvRow("Jobs (laufend)", c.jobsRun)}
${kvRow("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")} ${kvRow("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
${kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na")} ${sysRow}
</div>`; </div>`;
} }
@@ -113,5 +127,6 @@ function renderAll() { renderHero(); renderKpis(); renderHealth(); renderModels(
function mount() { renderAll(); } function mount() { renderAll(); }
function onStatus(s) { S = s; renderAll(); } function onStatus(s) { S = s; renderAll(); }
function onJobs(jobs) { J = jobs || []; renderHero(); renderKpis(); renderHealth(); } function onJobs(jobs) { J = jobs || []; renderHero(); renderKpis(); renderHealth(); }
function onSystem(sys) { SYS = sys; renderKpis(); renderHealth(); }
export default { id: "overview", mount, onStatus, onJobs }; export default { id: "overview", mount, onStatus, onJobs, onSystem };