diff --git a/backend/src/media_library_viewer_api/routers/dashboard.py b/backend/src/media_library_viewer_api/routers/dashboard.py index 2c6d8a9..9897a99 100644 --- a/backend/src/media_library_viewer_api/routers/dashboard.py +++ b/backend/src/media_library_viewer_api/routers/dashboard.py @@ -3,13 +3,21 @@ from __future__ import annotations import logging +import time from typing import Any from fastapi import APIRouter, Depends from media_library_viewer_api.clients.jellyfin import JellyfinClient -from media_library_viewer_api.dependencies import get_jellyfin_client, get_monitoring_poller, get_settings_store, get_user_id +from media_library_viewer_api.dependencies import ( + get_jellyfin_client, + get_monitoring_poller, + get_settings_store, + get_user_id, +) +from media_library_viewer_api.models.backups import BackupDashboardSummary from media_library_viewer_api.services.monitoring_actions import collect_machine_overview +from media_library_viewer_api.services.settings_store import SettingsStore logger = logging.getLogger(__name__) @@ -54,7 +62,10 @@ def get_monitoring_overview( machine.get("id"), ) rows.append({ - "machine": {k: machine.get(k) for k in ("id", "name", "mode", "enabled", "host", "port", "username", "media_root", "path_prefix", "notes")}, + "machine": {k: machine.get(k) for k in ( + "id", "name", "mode", "enabled", "host", "port", "username", + "media_root", "path_prefix", "notes", + )}, "status": "", "status_error": str(exc), "metrics_error": str(exc), @@ -183,3 +194,44 @@ def get_now_playing( ) -> list[dict[str, Any]]: """Backward-compatible alias; returns full activity rows.""" return get_activity(client) + + +@router.get("/backups") +def get_backup_dashboard( + store: SettingsStore = Depends(get_settings_store), +) -> BackupDashboardSummary: + jobs = store.list_backup_jobs() + total_jobs = len(jobs) + + # Calculate 24h success rate + cutoff = int(time.time()) - (24 * 60 * 60) + recent_runs = [] + for job in jobs: + runs = store.list_backup_runs(job_id=job["id"], limit=1) + if runs and runs[0]["started_at"] >= cutoff: + recent_runs.append(runs[0]) + + successful = sum(1 for r in recent_runs if r["status"] == "success") + success_rate = (successful / len(recent_runs) * 100) if recent_runs else 100.0 + + # Active alerts + alerts = store.list_backup_alerts(acknowledged=False) + active_alerts = len(alerts) + + # Last failed + failed_runs = [] + for job in jobs: + runs = store.list_backup_runs(job_id=job["id"], status="failure", limit=1) + if runs: + failed_runs.append(runs[0]) + + last_failed_at = None + if failed_runs: + last_failed_at = max(r["started_at"] for r in failed_runs) + + return BackupDashboardSummary( + total_jobs=total_jobs, + success_rate_24h=round(success_rate, 1), + active_alerts=active_alerts, + last_failed_at=last_failed_at, + ) diff --git a/backend/tests/test_backups.py b/backend/tests/test_backups.py index 92a2943..134b7ae 100644 --- a/backend/tests/test_backups.py +++ b/backend/tests/test_backups.py @@ -7,6 +7,32 @@ from media_library_viewer_api.main import app from media_library_viewer_api.services.settings_store import SettingsStore +def test_dashboard_backups(): + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "test_settings.sqlite" + store = SettingsStore(db_path) + store.init_schema() + + # Monkey-patch the global store for this test + import media_library_viewer_api.auth as auth_module + from media_library_viewer_api.services import settings_store + + original_store = settings_store._store + settings_store._store = store + auth_module._API_KEY = None + + try: + client = TestClient(app) + response = client.get("/api/dashboard/backups") + assert response.status_code == 200 + data = response.json() + assert "total_jobs" in data + assert "success_rate_24h" in data + finally: + settings_store._store = original_store + auth_module._API_KEY = None + + def test_post_backup_report(): with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "test_settings.sqlite"