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.
This commit is contained in:
2026-08-15 12:28:14 +02:00
parent a90b516f49
commit d69f82d995
5 changed files with 132 additions and 1 deletions
+78
View File
@@ -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.")