From d69f82d995fb65613ddbcd451575f234395ad651 Mon Sep 17 00:00:00 2001 From: nessi Date: Sat, 15 Aug 2026 12:28:14 +0200 Subject: [PATCH] feat: add service status monitoring to dashboard with parallel health checks for 14 services Add SERVICE_DEFINITIONS list with Radarr, Sonarr, SABnzbd, Jellyfin, Seerr, Prowlarr, Easynews as Indexer, qBittorrent, Bazarr, Lingarr, LibreTranslate, Sonarr-Prowlarr Proxy, Radarr-Prowlarr Proxy, and Dashboard. Add check_service() to perform HTTP/API health checks with configurable timeout. Add refresh_services() to run checks in parallel using service_check_executor ThreadPoolExecutor with 16 workers. --- .env.example | 15 +++++++++ README.md | 35 ++++++++++++++++++++ app.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 ++ templates/index.html | 3 +- 5 files changed, 132 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 3bef3c4..32517cb 100644 --- a/.env.example +++ b/.env.example @@ -15,5 +15,20 @@ FILM_MEDIA_VOLUME=/nesflix/filme # SAB_URL=http://sabnzbd:8080 # SAB_API_KEY=CHANGE_ME +# Dienststatus im Dashboard +JELLYFIN_URL=http://jellyfin:8096 +SEERR_URL=http://seerr:5055 +PROWLARR_URL=http://prowlarr:9696 +EASYNEWS_INDEXER_URL=http://easynews-as-indexer:8081 +# qBittorrent läuft außerhalb des Compose-Stacks: URL hier anpassen +# QBITTORRENT_URL=http://host.docker.internal:8080 +BAZARR_URL=http://bazarr:6767 +LINGARR_URL=http://lingarr:9876 +LIBRETRANSLATE_URL=http://libretranslate:5000 +SONARR_PROWLARR_PROXY_URL=http://sonarr-prowlarr-proxy:8080 +RADARR_PROWLARR_PROXY_URL=http://radarr-prowlarr-proxy:8080 +SERVICE_CHECK_TIMEOUT=3 +SERVICE_REFRESH_SECONDS=30 + DB_PATH=/data/cache.db LOG_LEVEL=INFO diff --git a/README.md b/README.md index ba51e1a..b96b541 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,41 @@ volumes: - /nesflix/filme:/media/filme:ro ``` +## Dienststatus + +Der Dashboard-Tab prüft zusätzlich Jellyfin, Seerr, Prowlarr, Easynews as Indexer, +qBittorrent, Bazarr, Lingarr, LibreTranslate sowie die Sonarr-/Radarr-Prowlarr-Proxies. +Die Prüfungen laufen parallel im Hintergrund und blockieren die Seite nicht. + +Die Standardnamen funktionieren, wenn die Container im selben Docker-Netzwerk liegen. +Für qBittorrent muss `QBITTORRENT_URL` auf eine aus dem Dashboard-Container erreichbare +URL zeigen, zum Beispiel `http://host.docker.internal:8080` oder die Host-IP. + +```env +JELLYFIN_URL=http://jellyfin:8096 +SEERR_URL=http://seerr:5055 +PROWLARR_URL=http://prowlarr:9696 +EASYNEWS_INDEXER_URL=http://easynews-as-indexer:8081 +QBITTORRENT_URL=http://HOST_ODER_IP:8080 +BAZARR_URL=http://bazarr:6767 +LINGARR_URL=http://lingarr:9876 +LIBRETRANSLATE_URL=http://libretranslate:5000 +SONARR_PROWLARR_PROXY_URL=http://sonarr-prowlarr-proxy:8080 +RADARR_PROWLARR_PROXY_URL=http://radarr-prowlarr-proxy:8080 +SERVICE_CHECK_TIMEOUT=3 +SERVICE_REFRESH_SECONDS=30 +``` + +Für die bereits vorhandenen Container sind keine zusätzlichen Compose-Services nötig; +sie müssen im gemeinsamen `media-stack_default`-Netzwerk erreichbar sein. Falls +qBittorrent nicht über `host.docker.internal` erreichbar ist, kann unter Linux je nach +Docker-Konfiguration dieser Eintrag ergänzt werden: + +```yaml +extra_hosts: + - "host.docker.internal:host-gateway" +``` + Ein Radarr-Pfad wie: `/data/filme/Avatar (2009)/Avatar.mkv` diff --git a/app.py b/app.py index 04ec523..844be48 100644 --- a/app.py +++ b/app.py @@ -24,6 +24,18 @@ SONARR_MEDIA_PATH = os.environ.get("SONARR_MEDIA_PATH", "/data/serien").rstrip(" LOCAL_SERIES_PATH = os.environ.get("LOCAL_SERIES_PATH", "/media/serien").rstrip("/") SAB_URL = os.environ.get("SAB_URL", "").rstrip("/") SAB_API_KEY = os.environ.get("SAB_API_KEY", "") +JELLYFIN_URL = os.environ.get("JELLYFIN_URL", "http://jellyfin:8096").rstrip("/") +SEERR_URL = os.environ.get("SEERR_URL", "http://seerr:5055").rstrip("/") +PROWLARR_URL = os.environ.get("PROWLARR_URL", "http://prowlarr:9696").rstrip("/") +EASYNEWS_INDEXER_URL = os.environ.get("EASYNEWS_INDEXER_URL", "http://easynews-as-indexer:8081").rstrip("/") +QBITTORRENT_URL = os.environ.get("QBITTORRENT_URL", "").rstrip("/") +BAZARR_URL = os.environ.get("BAZARR_URL", "http://bazarr:6767").rstrip("/") +LINGARR_URL = os.environ.get("LINGARR_URL", "http://lingarr:9876").rstrip("/") +LIBRETRANSLATE_URL = os.environ.get("LIBRETRANSLATE_URL", "http://libretranslate:5000").rstrip("/") +SONARR_PROWLARR_PROXY_URL = os.environ.get("SONARR_PROWLARR_PROXY_URL", "http://sonarr-prowlarr-proxy:8080").rstrip("/") +RADARR_PROWLARR_PROXY_URL = os.environ.get("RADARR_PROWLARR_PROXY_URL", "http://radarr-prowlarr-proxy:8080").rstrip("/") +SERVICE_CHECK_TIMEOUT = float(os.environ.get("SERVICE_CHECK_TIMEOUT", "3")) +SERVICE_REFRESH_SECONDS = int(os.environ.get("SERVICE_REFRESH_SECONDS", "30")) DB_PATH = os.environ.get("DB_PATH", "/data/cache.db") REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "20")) @@ -36,10 +48,30 @@ jobs = {"radarr": {"state": "idle", "total": 0, "done": 0, "error": None}, "sona job_lock = threading.Lock() system_lock = threading.Lock() previous_cpu = None +service_status_lock = threading.Lock() +service_check_executor = ThreadPoolExecutor(max_workers=16) 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":"❓"} +SERVICE_DEFINITIONS = [ + {"key": "radarr", "name": "Radarr", "url": f"{RADARR_URL}/api/v3/system/status", "api_key": RADARR_API_KEY, "kind": "api"}, + {"key": "sonarr", "name": "Sonarr", "url": f"{SONARR_URL}/api/v3/system/status" if SONARR_URL else "", "api_key": SONARR_API_KEY, "kind": "api"}, + {"key": "sabnzbd", "name": "SABnzbd", "url": f"{SAB_URL}/api?mode=version&output=json&apikey={SAB_API_KEY}" if SAB_URL else "", "api_key": "", "kind": "api"}, + {"key": "jellyfin", "name": "Jellyfin", "url": JELLYFIN_URL, "api_key": "", "kind": "http"}, + {"key": "seerr", "name": "Seerr", "url": SEERR_URL, "api_key": "", "kind": "http"}, + {"key": "prowlarr", "name": "Prowlarr", "url": PROWLARR_URL, "api_key": "", "kind": "http"}, + {"key": "easynews", "name": "Easynews as Indexer", "url": EASYNEWS_INDEXER_URL, "api_key": "", "kind": "http"}, + {"key": "qbittorrent", "name": "qBittorrent", "url": QBITTORRENT_URL, "api_key": "", "kind": "http"}, + {"key": "bazarr", "name": "Bazarr", "url": BAZARR_URL, "api_key": "", "kind": "http"}, + {"key": "lingarr", "name": "Lingarr", "url": LINGARR_URL, "api_key": "", "kind": "http"}, + {"key": "libretranslate", "name": "LibreTranslate", "url": LIBRETRANSLATE_URL, "api_key": "", "kind": "http"}, + {"key": "sonarr_prowlarr_proxy", "name": "Sonarr-Prowlarr Proxy", "url": SONARR_PROWLARR_PROXY_URL, "api_key": "", "kind": "http"}, + {"key": "radarr_prowlarr_proxy", "name": "Radarr-Prowlarr Proxy", "url": RADARR_PROWLARR_PROXY_URL, "api_key": "", "kind": "http"}, + {"key": "dashboard", "name": "Language Dashboard", "url": "", "api_key": "", "kind": "self"}, +] +service_status = {service["key"]: {"key": service["key"], "name": service["name"], "status": "checking", "label": "Prüfe …"} for service in SERVICE_DEFINITIONS} + def db(): Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True) con = sqlite3.connect(DB_PATH) @@ -54,6 +86,52 @@ def api_get(base, key, endpoint): r.raise_for_status() return r.json() +def check_service(service): + result = {"key": service["key"], "name": service["name"]} + if service["kind"] == "self": + result.update(status="online", label="Aktiv", detail="Dashboard läuft") + return result + if not service["url"]: + result.update(status="error", label="Nicht konfiguriert", detail="URL fehlt") + return result + try: + headers = {"X-Api-Key": service["api_key"]} if service["api_key"] else {} + response = requests.get(service["url"], headers=headers, timeout=SERVICE_CHECK_TIMEOUT, allow_redirects=False) + if 200 <= response.status_code < 400: + result.update(status="online", label="Aktiv", detail=f"HTTP {response.status_code}") + elif response.status_code in (401, 403) or response.status_code >= 500: + result.update(status="error", label="Fehler", detail=f"HTTP {response.status_code}") + else: + result.update(status="online", label="Erreichbar", detail=f"HTTP {response.status_code}") + except requests.Timeout: + result.update(status="timeout", label="Timeout", detail=f"> {SERVICE_CHECK_TIMEOUT:g}s") + except requests.RequestException as exc: + result.update(status="offline", label="Offline", detail=exc.__class__.__name__) + return result + +def refresh_services(): + futures = [service_check_executor.submit(check_service, service) for service in SERVICE_DEFINITIONS] + results = [future.result() for future in futures] + with service_status_lock: + service_status.update({result["key"]: result for result in results}) + +def service_monitor(): + while True: + try: + refresh_services() + except Exception: + log.exception("Dienststatus konnte nicht aktualisiert werden") + time.sleep(max(5, SERVICE_REFRESH_SECONDS)) + +threading.Thread(target=service_monitor, name="service-monitor", daemon=True).start() + +@app.get("/api/services") +def services(): + with service_status_lock: + values = list(service_status.values()) + online = sum(1 for service in values if service["status"] == "online") + return jsonify({"services": values, "online": online, "total": len(values), "updated_at": time.time()}) + def sab_get(mode, **params): if not SAB_URL or not SAB_API_KEY: raise RuntimeError("SAB_URL oder SAB_API_KEY ist nicht gesetzt.") diff --git a/docker-compose.yml b/docker-compose.yml index f963df4..52f4163 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: restart: unless-stopped ports: - "8099:8099" + extra_hosts: + - "host.docker.internal:host-gateway" env_file: - .env volumes: diff --git a/templates/index.html b/templates/index.html index bb7ba4e..a2a8fbf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -33,6 +33,7 @@ +