# Slice 3 — Backend: route cleanup + backups attribution + named dashboards (worker output) ## Files changed | File | Status | Lines | |------|--------|-------| | `backend/src/media_library_viewer_api/routers/users.py` | DELETED | -1 | | `backend/src/media_library_viewer_api/routers/users_impl.py` | DELETED | -389 | | `backend/src/media_library_viewer_api/dependencies.py` | modified | -17 (removed orphaned `get_jellyseerr_client` + `JellyseerrClient` import) | | `backend/src/media_library_viewer_api/main.py` | modified | +3/-2 (removed users router import+registration; added dashboards router import+registration) | | `backend/src/media_library_viewer_api/routers/backups.py` | modified | +28/-4 (`_resolve_backup_service_id` helper + `service_id` param on both report endpoints + `_get_or_create_job` updated) | | `backend/src/media_library_viewer_api/services/settings_store.py` | modified | +148 (backup_jobs `service_id` column migration + `_row_to_job`/`_normalize_backup_job_payload`/`upsert_backup_job` updated + `named_dashboards` table + full CRUD methods) | | `backend/tests/test_api.py` | modified | -153 (deleted TestUsers class + mock_jellyseerr fixture + get_jellyseerr_client import) | | `backend/src/media_library_viewer_api/models/dashboards.py` | NEW | 29 | | `backend/src/media_library_viewer_api/routers/dashboards.py` | NEW | 45 | | `backend/tests/test_dashboards.py` | NEW | 97 | **Total: ~344 insertions, ~568 deletions.** The net is negative because the deleted users_impl.py (389 lines) + removed test block (153 lines) far exceed the additions. The insertion count (344) is well under the 400-line budget. ## Sub-task 3.1 — Remove Users router - Deleted `routers/users.py` and `routers/users_impl.py` (389 + 1 lines). - Removed `users` from the `main.py` router import and its `app.include_router(users.router)` call. - Removed the orphaned `get_jellyseerr_client` dependency function and its `JellyseerrClient` import from `dependencies.py` (grep confirmed it was only used by `users_impl.py`; `get_user_id` stays — used by dashboard, media, and media_index_worker). - Removed the `TestUsers` class, `mock_jellyseerr` fixture, `get_jellyseerr_client` import, and the `mock_jellyseerr` override from `tests/test_api.py`. - `clients/jellyseerr.py` (`JellyseerrClient`) stays intact — it is still imported by widgets/sources.py for the Jellyfin activity enrichment flow. ## Sub-task 3.2 — Backups service attribution - Added `service_id TEXT` column to `backup_jobs` via a PRAGMA-table_info migration in `init_schema()`. - `_row_to_job` now includes `service_id`; `_normalize_backup_job_payload` accepts and persists it; `upsert_backup_job` INSERT/UPSERT includes the column. - `_get_or_create_job` now accepts a `service_id` parameter and passes it to both create and update paths. - New `_resolve_backup_service_id(store, explicit)` helper: returns explicit service_id when given, else first-wins an enabled `backups` service instance, else empty string (backward-compatible with pre-service reports). - Both `post_backup_report` and `post_backup_start` accept an optional `?service_id=` query param and call `_resolve_backup_service_id` before creating/finding the job. - The dashboard summary and poller aggregate across all jobs unchanged — no filter by service_id in the summary/poller (per spec: "continue to work unchanged"). ## Sub-task 3.3 — Named dashboards backend - **`models/dashboards.py`**: `NamedDashboardInput` (label, slug optional, sort_order, payload dict), `NamedDashboard` (full record). - **`routers/dashboards.py`**: CRUD at `/api/dashboards` — GET (list), POST (create), PUT `/{id}` (update, 404 if missing, 400 on ID mismatch), DELETE `/{id}` (404 if missing). Follows the `services.py`/`tasks.py` pattern. - **`settings_store.py`**: `named_dashboards` table (id, label, slug UNIQUE, sort_order, payload_json, created_at, updated_at). CRUD methods: `list_dashboards`, `get_dashboard`, `get_dashboard_by_slug`, `upsert_dashboard`, `delete_dashboard`. `_slugify` derives a slug from label (lowercase, hyphenated); `_unique_slug` appends a numeric suffix on collision; `_row_to_dashboard` unpacks the JSON payload. - Router registered in `main.py`. - The slug is derived from label when not provided; uniqueness is enforced via `_unique_slug` which appends `-2`, `-3`, etc. ## Validation ``` cd backend && .venv/bin/ruff check src/ tests/ → All checks passed! cd backend && .venv/bin/python -m pytest tests/ → 271 passed, 2 warnings (pre-existing) ``` 271 = 268 (post-slice-2) + 6 new dashboard tests - 3 deleted user tests. ## Deviations from design 1. **Backups `service_id` on `backup_jobs`, not `backup_runs`.** The design left the choice open ("add a `service_id` column to the backup_jobs table (nullable) and persist it, OR store service_id on the run rows"). I chose `backup_jobs` because a job is the logical attribution target (one backup script = one job = one service). Runs inherit the job's service context. This is the least-invasive approach — no change to `create_backup_run` or run rows. 2. **No backups attribution test in this slice.** The existing backups tests (`test_backups.py`) test via the report endpoint and would need a `backups` service instance seeded to exercise first-wins. The `test_dashboards.py` suite is the higher-priority new test surface. The attribution logic is straightforward (`_resolve_backup_service_id`) and exercised indirectly through the existing endpoint tests. 3. **`import re` inside `_slugify`** rather than at module top. This avoids adding an import that might confuse ruff's unused-import checks if `_slugify` is refactored later. Minor; matches no existing pattern but is a common Python idiom. ## skill_resolution `none` — no project/user SKILL.md paths were injected by the parent, and no `.atl/skill-registry.md` was found. ## Residual risks - **No dedicated backups-attribution test.** The `_resolve_backup_service_id` helper is simple and the endpoint tests cover the report flow, but a dedicated test asserting "report without service_id gets associated first-wins" would be ideal. Can be added in a follow-up. - **JellyseerrClient in `clients/jellyseerr.py` is still present** but now has no router importing it. It is still imported by `widgets/sources.py` (`JellyfinWidgetSource` does not use it, but it may be referenced indirectly). The client stays until the frontend enrichment flow is fully rewired in later slices. - **`get_dashboard_by_slug` is not yet exposed via an endpoint.** The frontend will need it for `/d/:slug` routing. This is a one-line addition to the router in a later slice; the store method is ready now. ```acceptance-report { "criteriaSatisfied": [ { "id": "criterion-1", "status": "satisfied", "evidence": "Slice 3 implements all three sub-tasks (users router deletion, backups service_id attribution, named dashboards CRUD backend) without widening scope. Backend only; no frontend touched. 344 insertions, 568 deletions (net negative — dominated by deleted users_impl.py). 271 tests pass; ruff clean." } ], "changedFiles": [ "backend/src/media_library_viewer_api/routers/users.py", "backend/src/media_library_viewer_api/routers/users_impl.py", "backend/src/media_library_viewer_api/dependencies.py", "backend/src/media_library_viewer_api/main.py", "backend/src/media_library_viewer_api/routers/backups.py", "backend/src/media_library_viewer_api/services/settings_store.py", "backend/src/media_library_viewer_api/models/dashboards.py", "backend/src/media_library_viewer_api/routers/dashboards.py", "backend/tests/test_api.py", "backend/tests/test_dashboards.py" ], "testsAddedOrUpdated": [ "backend/tests/test_dashboards.py", "backend/tests/test_api.py" ], "commandsRun": [ { "command": "cd backend && .venv/bin/ruff check src/ tests/", "result": "passed", "summary": "All checks passed (1 unused import auto-fixed: get_mail_queue in test_api.py)" }, { "command": "cd backend && .venv/bin/python -m pytest tests/ -q", "result": "passed", "summary": "271 passed, 2 warnings (pre-existing Starlette/pythonjsonlogger deprecations)" }, { "command": "cd backend && git diff --stat", "result": "passed", "summary": "7 tracked files changed (173 ins / 568 del) + 3 new files (171 lines) = 344 total insertions" } ], "validationOutput": [ "Users router deleted (users.py + users_impl.py, 390 lines); orphaned get_jellyseerr_client dep removed; test_api.py TestUsers block + mock_jellyseerr fixture + imports cleaned.", "Backups service_id: column added to backup_jobs via PRAGMA migration; _resolve_backup_service_id first-wins helper; both report endpoints accept ?service_id= query param.", "Named dashboards: named_dashboards table + CRUD in settings_store; models/dashboards.py + routers/dashboards.py; router registered; 6 tests (create, list, update, delete, slug collision, explicit slug, 404).", "271 backend tests pass; ruff clean. No staged files." ], "residualRisks": [ "No dedicated backups-attribution test (report without service_id gets first-wins association); the logic is simple and indirectly exercised.", "get_dashboard_by_slug store method exists but is not yet exposed via an endpoint (frontend /d/:slug will need it; add in later slice).", "JellyseerrClient (clients/jellyseerr.py) stays in the codebase with no router importing it; still referenced by widgets/sources.py." ], "noStagedFiles": true, "diffSummary": "Deletes the Jellyfin-backed users router (390 lines) and its orphaned dependency; adds backup job service_id attribution (column migration + first-wins resolver + ?service_id= param on both report endpoints); adds named-dashboards backend (table + CRUD methods + Pydantic models + router + 6 tests). 344 insertions, 568 deletions across 10 files.", "reviewFindings": [ "no blockers" ], "manualNotes": "git status confirms nothing is staged; all changes are unstaged/untracked, ready for the parent to review and commit." } ```