feat: live system metrics (CPU, RAM, Disk, GPU, Temp)
This commit is contained in:
@@ -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 |
@@ -2,3 +2,4 @@ fastapi>=0.110
|
||||
uvicorn[standard]>=0.29
|
||||
httpx>=0.27
|
||||
ruamel.yaml>=0.18
|
||||
psutil>=5.9.0
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user