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:
1 parent
3d4fb52fa3
commit
5912b7078c
2 files changed
+65
-5
No files matched your search
@@ -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)})
|
||||
|
||||
|
||||
Reference in new issue
Block a user