release: 0.2.2

This commit is contained in:
2026-01-15 17:24:54 +01:00
parent fb096f7897
commit 835244b7e0
134 changed files with 563 additions and 168 deletions

View File

@ -28,11 +28,13 @@ from backend.models import (
UpdateLogEntry,
UpdateServiceStatus,
UpdateStatus,
SystemMetrics,
UserStatus,
)
from backend.oidc import OIDCClient, OIDCError
from backend.settings import Settings, get_settings
from backend import update
from backend import system_metrics
logging.basicConfig(
level=logging.INFO,
@ -321,6 +323,12 @@ def update_service_status(settings: Settings = Depends(get_settings)) -> UpdateS
return UpdateServiceStatus(**data)
@app.get("/system/metrics", response_model=SystemMetrics, dependencies=[Depends(get_current_admin)])
def system_metrics_status() -> SystemMetrics:
data = system_metrics.get_system_metrics()
return SystemMetrics(**data)
@app.get("/", response_class=HTMLResponse)
@app.get("/login", response_class=HTMLResponse)
@app.get("/dashboard", response_class=HTMLResponse)

View File

@ -82,3 +82,14 @@ class UpdateServiceStatus(BaseModel):
error: Optional[str] = None
checked_url: str
environment: str
class SystemMetrics(BaseModel):
cpu_percent: float
ram_total_mb: float
ram_used_percent: float
gpu_vram_total_mb: Optional[float] = None
gpu_vram_used_percent: Optional[float] = None
gpu_present: bool = False
net_rx_mbps: float
net_tx_mbps: float

139
backend/system_metrics.py Normal file
View File

@ -0,0 +1,139 @@
import time
import shutil
import subprocess
from typing import Any, Dict, Optional, Tuple
_last_cpu: Optional[Tuple[float, float]] = None
_last_net: Optional[Tuple[float, float, float]] = None
def _read_cpu_times() -> Tuple[float, float]:
with open("/proc/stat", "r", encoding="utf-8") as handle:
line = handle.readline()
parts = line.strip().split()
if not parts or parts[0] != "cpu":
return 0.0, 0.0
values = [float(p) for p in parts[1:]]
total = sum(values)
idle = values[3] if len(values) > 3 else 0.0
return total, idle
def _cpu_percent() -> float:
global _last_cpu
total, idle = _read_cpu_times()
if _last_cpu is None:
_last_cpu = (total, idle)
return 0.0
last_total, last_idle = _last_cpu
_last_cpu = (total, idle)
delta_total = total - last_total
delta_idle = idle - last_idle
if delta_total <= 0:
return 0.0
return max(0.0, min(100.0, (delta_total - delta_idle) / delta_total * 100.0))
def _read_meminfo() -> Dict[str, float]:
data: Dict[str, float] = {}
with open("/proc/meminfo", "r", encoding="utf-8") as handle:
for line in handle:
key, value = line.split(":", 1)
parts = value.strip().split()
if not parts:
continue
data[key] = float(parts[0])
return data
def _read_net_bytes() -> Tuple[float, float]:
rx_total = 0.0
tx_total = 0.0
with open("/proc/net/dev", "r", encoding="utf-8") as handle:
for line in handle:
if ":" not in line:
continue
iface, stats = line.split(":", 1)
iface = iface.strip()
if iface == "lo":
continue
fields = stats.split()
if len(fields) < 16:
continue
rx_total += float(fields[0])
tx_total += float(fields[8])
return rx_total, tx_total
def _net_mbps() -> Tuple[float, float]:
global _last_net
now = time.time()
rx, tx = _read_net_bytes()
if _last_net is None:
_last_net = (now, rx, tx)
return 0.0, 0.0
last_time, last_rx, last_tx = _last_net
_last_net = (now, rx, tx)
delta_t = now - last_time
if delta_t <= 0:
return 0.0, 0.0
rx_mbps = (rx - last_rx) * 8.0 / (delta_t * 1_000_000.0)
tx_mbps = (tx - last_tx) * 8.0 / (delta_t * 1_000_000.0)
return max(0.0, rx_mbps), max(0.0, tx_mbps)
def _gpu_metrics() -> Dict[str, Any]:
if not shutil.which("nvidia-smi"):
return {
"gpu_vram_total_mb": None,
"gpu_vram_used_percent": None,
"gpu_present": False,
}
try:
output = subprocess.check_output(
[
"nvidia-smi",
"--query-gpu=memory.total,memory.used",
"--format=csv,noheader,nounits",
],
text=True,
).strip()
if not output:
raise ValueError("empty nvidia-smi output")
total_str, used_str = output.split(",", 1)
total_mb = float(total_str.strip())
used_mb = float(used_str.strip())
used_percent = 0.0 if total_mb == 0 else used_mb / total_mb * 100.0
return {
"gpu_vram_total_mb": total_mb,
"gpu_vram_used_percent": used_percent,
"gpu_present": True,
}
except Exception:
return {
"gpu_vram_total_mb": None,
"gpu_vram_used_percent": None,
"gpu_present": False,
}
def get_system_metrics() -> Dict[str, Any]:
meminfo = _read_meminfo()
total_kb = meminfo.get("MemTotal", 0.0)
available_kb = meminfo.get("MemAvailable", 0.0)
used_kb = max(0.0, total_kb - available_kb)
ram_total_mb = total_kb / 1024.0
ram_used_percent = 0.0 if total_kb == 0 else used_kb / total_kb * 100.0
cpu_percent = _cpu_percent()
rx_mbps, tx_mbps = _net_mbps()
gpu = _gpu_metrics()
return {
"cpu_percent": cpu_percent,
"ram_total_mb": ram_total_mb,
"ram_used_percent": ram_used_percent,
"net_rx_mbps": rx_mbps,
"net_tx_mbps": tx_mbps,
**gpu,
}

View File

@ -95,6 +95,30 @@
</div>
</div>
<!-- System Information Section -->
<section id="systemSection" class="panel-section hidden">
<h3><i data-lucide="monitor"></i> System Information</h3>
<div class="metrics" id="systemMetrics">
<div class="metric-card" id="metricCpu">
<div class="label"><i data-lucide="cpu"></i> CPU</div>
<div class="value">-</div>
</div>
<div class="metric-card" id="metricRam">
<div class="label"><i data-lucide="memory-stick"></i> RAM</div>
<div class="value">-</div>
</div>
<div class="metric-card" id="metricGpu">
<div class="label"><i data-lucide="monitor"></i> GPU</div>
<div class="value">-</div>
</div>
<div class="metric-card" id="metricNet">
<div class="label"><i data-lucide="network"></i> Netzwerk</div>
<div class="value">-</div>
</div>
</div>
<div class="text-muted" style="font-size: 0.75rem; margin-top: 0.5rem;">Aktualisierung alle 5 Sekunden.</div>
</section>
<!-- User Management Section -->
<section id="userSection" class="panel-section hidden">
<h3><i data-lucide="users"></i> Nutzerverwaltung</h3>
@ -326,14 +350,24 @@
showApp();
document.getElementById('userSection').classList.remove('hidden');
document.getElementById('updateSection').classList.remove('hidden');
document.getElementById('systemSection').classList.remove('hidden');
await refreshUsers();
await refreshUpdateStatus();
await refreshSystemMetrics();
if (!window.systemMetricsTimer) {
window.systemMetricsTimer = setInterval(refreshSystemMetrics, 5000);
}
return true;
} catch (err) {
document.getElementById('currentUser').textContent = 'Nicht angemeldet';
showLanding();
document.getElementById('userSection').classList.add('hidden');
document.getElementById('updateSection').classList.add('hidden');
document.getElementById('systemSection').classList.add('hidden');
if (window.systemMetricsTimer) {
clearInterval(window.systemMetricsTimer);
window.systemMetricsTimer = null;
}
return false;
}
}
@ -513,6 +547,29 @@
}
}
async function refreshSystemMetrics() {
try {
const data = await api('/system/metrics');
document.querySelector('#metricCpu .value').textContent = `${data.cpu_percent.toFixed(1)}%`;
const ramGb = data.ram_total_mb / 1024.0;
document.querySelector('#metricRam .value').textContent = `${data.ram_used_percent.toFixed(1)}% (${ramGb.toFixed(1)} GB)`;
if (data.gpu_present && data.gpu_vram_total_mb) {
const gpuGb = data.gpu_vram_total_mb / 1024.0;
const gpuPct = data.gpu_vram_used_percent ?? 0;
document.querySelector('#metricGpu .value').textContent = `${gpuPct.toFixed(1)}% (${gpuGb.toFixed(1)} GB)`;
} else {
document.querySelector('#metricGpu .value').textContent = 'Nicht verfuegbar';
}
document.querySelector('#metricNet .value').textContent = `${data.net_rx_mbps.toFixed(1)} / ${data.net_tx_mbps.toFixed(1)} Mbps`;
lucide.createIcons();
} catch (err) {
document.querySelector('#metricCpu .value').textContent = 'Fehler';
document.querySelector('#metricRam .value').textContent = 'Fehler';
document.querySelector('#metricGpu .value').textContent = 'Fehler';
document.querySelector('#metricNet .value').textContent = 'Fehler';
}
}
// Event listeners
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();