diff --git a/.gitignore b/.gitignore index 5bc8ed8..7f3e47d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *$py.class .venv/ upload.token +update-addon.env venv/ ENV/ .env diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c0562..90ca51a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -ID: DOC_000001 | Version: 0.2.1 | Status: Final +ID: DOC_000001 | Version: 0.2.2 | Status: Final By: Codex (GPT-5) # Projekt-Logbuch (Changelog) @@ -76,6 +76,12 @@ By: Codex (GPT-5) | 15.01.2026 | 🏗️ Planning | ID: US_000048/TASK_000057 Release-Upload automatisieren. By: Codex (GPT-5) | | 15.01.2026 | ⚙️ Code | ID: Release-Upload Script hinzugefuegt (tar.gz + curl). By: Codex (GPT-5) | | 15.01.2026 | ⚙️ Code | ID: Upload-Script toleriert fehlende Leserechte fuer /etc/skd/*. By: Codex (GPT-5) | +| 15.01.2026 | 🏗️ Planning | ID: US_000049/TASK_000058 deployment.env fuer lokale Uploads. By: Codex (GPT-5) | +| 15.01.2026 | ⚙️ Code | ID: deployment.env.example hinzugefuegt und Scripts angepasst. By: Codex (GPT-5) | +| 15.01.2026 | ⚙️ Code | ID: deployment.env als lokale Quelle fuer Enrollment/Upload Scripts. By: Codex (GPT-5) | +| 15.01.2026 | ⚙️ Code | ID: EPIC_000013 System Telemetry Endpoint und UI umgesetzt. By: Codex (GPT-5) | +| 15.01.2026 | 📝 Req | ID: EPIC_000009 Update Webservice als erledigt markiert. By: Codex (GPT-5) | +| 15.01.2026 | 🚀 Release | ID: VERSION auf 0.2.2 erhoeht. By: Codex (GPT-5) | | 15.01.2026 | ⚙️ Code | ID: Makefile restart-Target hinzugefuegt. By: Codex (GPT-5) | --- diff --git a/README.md b/README.md index 8d4afdc..62b64ba 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ID: README_000001 | Version: 0.2.1 | Status: Draft +ID: README_000001 | Version: 0.2.2 | Status: Draft By: Codex (GPT-5) # Safe Kiddo Daemon diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/backend/app.py b/backend/app.py index 8890d99..bc489f7 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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) diff --git a/backend/models.py b/backend/models.py index afc8c69..08f86e4 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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 diff --git a/backend/system_metrics.py b/backend/system_metrics.py new file mode 100644 index 0000000..83ab7f7 --- /dev/null +++ b/backend/system_metrics.py @@ -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, + } diff --git a/backend/templates/index.html b/backend/templates/index.html index 733ca73..dec71ff 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -95,6 +95,30 @@ + + +