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.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 |
@@ -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
|
||||||
|
|||||||
@@ -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];
|
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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user