feat: add backup monitoring API endpoints

This commit is contained in:
2026-05-11 21:34:52 +02:00
parent 836f733a2e
commit 6b0f2df629
3 changed files with 191 additions and 133 deletions
@@ -14,6 +14,7 @@ from media_library_viewer_api.auth import require_jwt_auth, validate_auth_settin
from media_library_viewer_api.config import get_settings
from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller
from media_library_viewer_api.logging_utils import configure_logging, describe_settings
from media_library_viewer_api.routers import backups as backups_router
from media_library_viewer_api.routers import dashboard, files, jobs, media, monitoring, tasks, users
from media_library_viewer_api.routers.settings import router as settings_router
@@ -105,6 +106,7 @@ app.include_router(jobs.router)
app.include_router(users.router)
app.include_router(tasks.router)
app.include_router(settings_router)
app.include_router(backups_router.router)
@app.get("/api/health")
@@ -0,0 +1,165 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from ..auth import require_api_key
from ..models.backups import (
BackupAlertResponse,
BackupReportRequest,
BackupRunResponse,
)
from ..services.backup_alert_engine import generate_alerts_for_run
from ..services.settings_store import SettingsStore, get_settings_store
router = APIRouter(prefix="/api/backups", tags=["backups"])
def _get_or_create_job(store: SettingsStore, report: BackupReportRequest) -> dict[str, Any]:
job = store.get_backup_job_by_name(report.name)
if not job:
job = store.upsert_backup_job({
"name": report.name,
"source": report.source,
"target": report.target,
"schedule_interval_seconds": report.schedule_interval_seconds,
})
elif report.schedule_interval_seconds:
store.upsert_backup_job({
"id": job["id"],
"name": report.name,
"source": report.source,
"target": report.target,
"schedule_interval_seconds": report.schedule_interval_seconds,
})
job = store.get_backup_job(job["id"])
return job
@router.post("/report")
def post_backup_report(
report: BackupReportRequest,
store: SettingsStore = Depends(get_settings_store),
_auth: str = Depends(require_api_key),
) -> BackupRunResponse:
job = _get_or_create_job(store, report)
# Check for duplicate (same job + started_at within 1s)
existing_runs = store.list_backup_runs(job_id=job["id"], limit=5)
started_at_ts = int(report.started_at.timestamp())
for existing in existing_runs:
if abs(existing["started_at"] - started_at_ts) <= 1:
return BackupRunResponse(**existing)
run_data = {
"job_id": job["id"],
"started_at": started_at_ts,
"ended_at": int(report.ended_at.timestamp()) if report.ended_at else None,
"status": report.status,
"bytes_transferred": report.bytes_transferred,
"duration_ms": report.duration_ms,
"error_message": report.error_message,
"details": report.details,
}
run = store.create_backup_run(run_data)
# Generate alerts
previous_runs = store.list_backup_runs(job_id=job["id"], status="success", limit=20)
alerts = generate_alerts_for_run(run, previous_runs, job)
for alert in alerts:
store.create_backup_alert(alert)
# Resolve old alerts of the same type if this run is successful
if report.status == "success":
store.resolve_backup_alerts_for_job(job["id"], "failed_status")
store.resolve_backup_alerts_for_job(job["id"], "anomaly_size")
store.resolve_backup_alerts_for_job(job["id"], "anomaly_duration")
# Map details -> details_json for response model
run["details_json"] = run.pop("details", None)
return BackupRunResponse(**run)
@router.post("/report/start")
def post_backup_start(
report: BackupReportRequest,
store: SettingsStore = Depends(get_settings_store),
_auth: str = Depends(require_api_key),
) -> BackupRunResponse:
job = _get_or_create_job(store, report)
run_data = {
"job_id": job["id"],
"started_at": int(report.started_at.timestamp()),
"status": "in_progress",
}
run = store.create_backup_run(run_data)
# Map details -> details_json for response model
run["details_json"] = run.pop("details", None)
return BackupRunResponse(**run)
@router.get("/jobs")
def get_backup_jobs(
store: SettingsStore = Depends(get_settings_store),
) -> list[dict[str, Any]]:
jobs = store.list_backup_jobs()
return jobs
@router.get("/jobs/{job_id}")
def get_backup_job(
job_id: str,
store: SettingsStore = Depends(get_settings_store),
) -> dict[str, Any]:
job = store.get_backup_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Backup job not found")
runs = store.list_backup_runs(job_id=job_id, limit=20)
return {
"job": job,
"runs": runs,
}
@router.get("/runs")
def get_backup_runs(
job_id: str | None = None,
status: str | None = None,
limit: int = 50,
store: SettingsStore = Depends(get_settings_store),
) -> list[BackupRunResponse]:
runs = store.list_backup_runs(job_id=job_id, status=status, limit=limit)
return [BackupRunResponse(**run) for run in runs]
@router.get("/runs/{run_id}")
def get_backup_run(
run_id: str,
store: SettingsStore = Depends(get_settings_store),
) -> BackupRunResponse:
run = store.get_backup_run(run_id)
if not run:
raise HTTPException(status_code=404, detail="Backup run not found")
return BackupRunResponse(**run)
@router.get("/alerts")
def get_backup_alerts(
job_id: str | None = None,
acknowledged: bool | None = None,
severity: str | None = None,
store: SettingsStore = Depends(get_settings_store),
) -> list[BackupAlertResponse]:
alerts = store.list_backup_alerts(job_id=job_id, acknowledged=acknowledged, severity=severity)
return [BackupAlertResponse(**alert) for alert in alerts]
@router.post("/alerts/{alert_id}/acknowledge")
def acknowledge_backup_alert(
alert_id: str,
store: SettingsStore = Depends(get_settings_store),
) -> BackupAlertResponse:
alert = store.acknowledge_backup_alert(alert_id)
if not alert:
raise HTTPException(status_code=404, detail="Alert not found")
return BackupAlertResponse(**alert)