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.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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

+1
View File
@@ -2,3 +2,4 @@ fastapi>=0.110
uvicorn[standard]>=0.29
httpx>=0.27
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];
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);
+18 -3
View File
@@ -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)}<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 =
kpi(c.swap ? "green" : "red", "llama-swap", "swap",
c.swap ? "Online" : "Offline", "Transport-Status") +
@@ -59,7 +63,7 @@ function renderKpis() {
`${c.running}<small>/${c.total}</small>`, "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 = `
<div class="card-h"><h3>Stack-Gesundheit</h3>
<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("Jobs (laufend)", c.jobsRun)}
${kvRow("Fehler", c.jobsErr, c.jobsErr ? "bad" : "")}
${kvRow("Auslastung (RAM/GPU/Disk)", "folgt", "na")}
${sysRow}
</div>`;
}
@@ -113,5 +127,6 @@ function renderAll() { renderHero(); renderKpis(); renderHealth(); renderModels(
function mount() { renderAll(); }
function onStatus(s) { S = s; renderAll(); }
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 };