Files
manage/.pi-tmp/svc-hub-slice3-out.md
T
Developer a8dfbd5dc6 Cleanup: delete dead top-level pages + update docs (Slice 11)
Delete the old top-level page files whose content was migrated into
service-page tabs in slices 5-9:
- pages/Media.tsx, Applications.tsx (-> MediaTab)
- pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab)
- pages/Actions.tsx (-> ActionsTab)
- pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs)
- components/BackupsPage.tsx (-> JobsTab)
- components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs)
- hooks/useUsers.ts (orphaned after Users page deletion)
- the corresponding page test files (Media, FileBrowser, Applications,
  Actions, UsersPage) that tested the deleted pages directly.

The service-tab components are the live implementations; ServicePage
renders them. No live code references the deleted files.

Docs: append an Information Architecture section to REQUIREMENTS.md
documenting the services-as-hub model (nav shape, service-page tabs,
service type registry, Users->Authentik, Observability split, legacy
route 404s, empty state). Add a CHANGELOG entry under [Unreleased].

92 frontend tests pass (was 112; -20 deleted page tests); 271 backend
tests pass; lint/build green.

Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
2026-06-26 20:11:02 +00:00

9.9 KiB

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.
{
  "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."
}