Backend: drop users router, backups service attribution, named dashboards (Slice 3)

Users router removed:
- Delete routers/users.py + users_impl.py (Jellyfin-backed user directory,
  Jellyfin-email message compose, Jellyseerr enrichment).
- Drop orphaned get_jellyseerr_client dep from dependencies.py
  (get_user_id stays; used by dashboard/media/media_index_worker).
- clients/jellyseerr.py stays (still imported by widgets/sources.py).
- test_api.py TestUsers block + mock_jellyseerr fixture removed.

Backups service attribution:
- backup_jobs gains a nullable service_id column (PRAGMA migration).
- _resolve_backup_service_id helper: explicit service_id wins, else
  first-wins an enabled backups instance, else empty (backward-compat).
- Both report endpoints accept ?service_id= and persist it on the job.
- Dashboard summary + poller aggregate across all jobs unchanged.

Named dashboards backend:
- named_dashboards table (id, label, slug UNIQUE, sort_order, payload_json,
  timestamps) with full CRUD methods + _slugify/_unique_slug helpers.
- models/dashboards.py (NamedDashboardInput/NamedDashboard).
- routers/dashboards.py: GET/POST/PUT/DELETE /api/dashboards.
- Router registered in main.py.

Tests: test_dashboards.py (CRUD, slug collision, explicit slug, 404);
test_api.py trimmed. 271 backend tests pass (was 268; +6 dashboards -3
users); ruff clean.

Refs openspec/changes/services-as-hub-ia/ (spec R5/R6.1, tasks slice 3).
This commit is contained in:
Developer
2026-06-26 18:33:24 +00:00
parent 9370e52cfc
commit a43d6a6206
10 changed files with 337 additions and 568 deletions
@@ -15,7 +15,23 @@ 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]:
def _resolve_backup_service_id(store: SettingsStore, explicit: str | None = None) -> str:
"""Return the service_id for backup attribution.
First-wins: if no explicit service_id is given, pick the first enabled
``backups`` service instance (spec R6.1). Returns an empty string when
none is configured (backward-compatible with pre-service reports).
"""
if explicit:
return explicit
candidates = store.list_services("backups")
for svc in candidates:
if svc.get("enabled"):
return svc["id"]
return ""
def _get_or_create_job(store: SettingsStore, report: BackupReportRequest, service_id: str = "") -> dict[str, Any]:
job = store.get_backup_job_by_name(report.name)
if not job:
job = store.upsert_backup_job(
@@ -24,6 +40,7 @@ def _get_or_create_job(store: SettingsStore, report: BackupReportRequest) -> dic
"source": report.source,
"target": report.target,
"schedule_interval_seconds": report.schedule_interval_seconds,
"service_id": service_id,
}
)
elif report.schedule_interval_seconds:
@@ -34,6 +51,7 @@ def _get_or_create_job(store: SettingsStore, report: BackupReportRequest) -> dic
"source": report.source,
"target": report.target,
"schedule_interval_seconds": report.schedule_interval_seconds,
"service_id": service_id,
}
)
job = store.get_backup_job(job["id"])
@@ -43,10 +61,12 @@ def _get_or_create_job(store: SettingsStore, report: BackupReportRequest) -> dic
@router.post("/report")
def post_backup_report(
report: BackupReportRequest,
service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
_auth: str = Depends(require_api_key),
) -> BackupRunResponse:
job = _get_or_create_job(store, report)
resolved_service_id = _resolve_backup_service_id(store, service_id)
job = _get_or_create_job(store, report, resolved_service_id)
# Check for duplicate (same job + started_at within 1s)
existing_runs = store.list_backup_runs(job_id=job["id"], limit=5)
@@ -88,10 +108,12 @@ def post_backup_report(
@router.post("/report/start")
def post_backup_start(
report: BackupReportRequest,
service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
_auth: str = Depends(require_api_key),
) -> BackupRunResponse:
job = _get_or_create_job(store, report)
resolved_service_id = _resolve_backup_service_id(store, service_id)
job = _get_or_create_job(store, report, resolved_service_id)
run_data = {
"job_id": job["id"],