feat: add system metrics dashboard with live CPU, RAM, and disk usage monitoring plus service status overview

Add system_metrics() function to read CPU usage from /proc/stat with delta calculation, memory stats from /proc/meminfo, and disk usage for System/Daten/Filme/Serien mounts with device deduplication. Add /api/system endpoint returning metrics JSON with error handling.

Add dashboard view as default tab with hero section, 4-card overview grid showing movie/series/missing/download counts, system
This commit is contained in:
nessi committed 2026-08-13 16:38:19 +02:00
1 parent 3d4fb52fa3
commit 5912b7078c
2 files changed
+65 -5

No files matched your search

+53
View File
@@ -4,6 +4,8 @@ import os
import sqlite3
import subprocess
import threading
import time
import shutil
import re
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
@@ -31,6 +33,8 @@ app = Flask(__name__)
executor = ThreadPoolExecutor(max_workers=int(os.environ.get("SCAN_WORKERS", "2")))
jobs = {"radarr": {"state": "idle", "total": 0, "done": 0, "error": None}, "sonarr": {"state": "idle", "total": 0, "done": 0, "error": None}}
job_lock = threading.Lock()
system_lock = threading.Lock()
previous_cpu = None
LANG_NAMES = {"de":"Deutsch", "deu":"Deutsch", "ger":"Deutsch", "en":"Englisch", "eng":"Englisch", "ja":"Japanisch", "jpn":"Japanisch", "ko":"Koreanisch", "kor":"Koreanisch", "fr":"Französisch", "fra":"Französisch", "fre":"Französisch", "es":"Spanisch", "spa":"Spanisch", "it":"Italienisch", "ita":"Italienisch", "ru":"Russisch", "rus":"Russisch", "zh":"Chinesisch", "zho":"Chinesisch", "chi":"Chinesisch", "und":"Unbekannt", "":"Unbekannt"}
FLAGS = {"Deutsch":"🇩🇪", "Englisch":"🇬🇧", "Japanisch":"🇯🇵", "Koreanisch":"🇰🇷", "Französisch":"🇫🇷", "Spanisch":"🇪🇸", "Italienisch":"🇮🇹", "Russisch":"🇷🇺", "Chinesisch":"🇨🇳", "Unbekannt":"❓"}
@@ -77,6 +81,47 @@ def format_bytes(size):
return f"{value:.1f} {unit}"
value /= 1024
def cpu_usage_percent():
global previous_cpu
try:
fields = Path("/proc/stat").read_text().splitlines()[0].split()[1:]
values = [int(value) for value in fields]
idle = values[3] + (values[4] if len(values) > 4 else 0)
total = sum(values)
with system_lock:
current = (total, idle)
if previous_cpu is None:
previous_cpu = current
return 0.0
total_delta = total - previous_cpu[0]
idle_delta = idle - previous_cpu[1]
previous_cpu = current
return round(max(0.0, min(100.0, (1 - idle_delta / total_delta) * 100)), 1) if total_delta else 0.0
except (OSError, ValueError, IndexError):
return 0.0
def system_metrics():
memory = {line.split(":", 1)[0]: int(line.split()[1]) * 1024 for line in Path("/proc/meminfo").read_text().splitlines() if ":" in line}
total_memory = memory.get("MemTotal", 0)
available_memory = memory.get("MemAvailable", memory.get("MemFree", 0))
used_memory = max(0, total_memory - available_memory)
mount_paths = [("System", "/"), ("Daten", "/data"), ("Filme", "/media/filme"), ("Serien", "/media/serien")]
disks, seen_devices = [], set()
for label, path in mount_paths:
try:
target = Path(path)
if not target.exists():
continue
device = target.stat().st_dev
if device in seen_devices:
continue
seen_devices.add(device)
usage = shutil.disk_usage(target)
disks.append({"label": label, "path": path, "total": format_bytes(usage.total), "used": format_bytes(usage.used), "free": format_bytes(usage.free), "used_bytes": usage.used, "total_bytes": usage.total, "percent": round(usage.used / usage.total * 100, 1) if usage.total else 0})
except OSError:
continue
return {"cpu_percent": cpu_usage_percent(), "memory": {"used": format_bytes(used_memory), "total": format_bytes(total_memory), "percent": round(used_memory / total_memory * 100, 1) if total_memory else 0}, "disks": disks, "timestamp": time.time()}
def release_languages(name):
value = f" {name.lower().replace('.', ' ').replace('-', ' ')} "
found = []
@@ -292,6 +337,14 @@ def downloads():
log.exception("SABnzbd konnte nicht abgefragt werden")
return jsonify({"items": [], "queue_status": "Fehler", "speed": "—", "paused": False, "active_count": 0, "history_count": 0, "remaining": "—", "error": str(exc)}), 502
@app.get("/api/system")
def system():
try:
return jsonify(system_metrics())
except Exception as exc:
log.exception("Systemmetriken konnten nicht gelesen werden")
return jsonify({"error": str(exc), "cpu_percent": 0, "memory": {}, "disks": []}), 500
@app.get("/health")
def health(): return jsonify({"ok":True, "radarr_url":RADARR_URL, "sonarr_enabled":bool(SONARR_URL and SONARR_API_KEY), "sab_enabled":bool(SAB_URL and SAB_API_KEY)})