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).
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.pyandrouters/users_impl.py(389 + 1 lines). - Removed
usersfrom themain.pyrouter import and itsapp.include_router(users.router)call. - Removed the orphaned
get_jellyseerr_clientdependency function and itsJellyseerrClientimport fromdependencies.py(grep confirmed it was only used byusers_impl.py;get_user_idstays — used by dashboard, media, and media_index_worker). - Removed the
TestUsersclass,mock_jellyseerrfixture,get_jellyseerr_clientimport, and themock_jellyseerroverride fromtests/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 TEXTcolumn tobackup_jobsvia a PRAGMA-table_info migration ininit_schema(). _row_to_jobnow includesservice_id;_normalize_backup_job_payloadaccepts and persists it;upsert_backup_jobINSERT/UPSERT includes the column._get_or_create_jobnow accepts aservice_idparameter 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 enabledbackupsservice instance, else empty string (backward-compatible with pre-service reports). - Both
post_backup_reportandpost_backup_startaccept an optional?service_id=query param and call_resolve_backup_service_idbefore 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 theservices.py/tasks.pypattern.settings_store.py:named_dashboardstable (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._slugifyderives a slug from label (lowercase, hyphenated);_unique_slugappends a numeric suffix on collision;_row_to_dashboardunpacks the JSON payload.- Router registered in
main.py. - The slug is derived from label when not provided; uniqueness is enforced via
_unique_slugwhich 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
-
Backups
service_idonbackup_jobs, notbackup_runs. The design left the choice open ("add aservice_idcolumn to the backup_jobs table (nullable) and persist it, OR store service_id on the run rows"). I chosebackup_jobsbecause 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 tocreate_backup_runor run rows. -
No backups attribution test in this slice. The existing backups tests (
test_backups.py) test via the report endpoint and would need abackupsservice instance seeded to exercise first-wins. Thetest_dashboards.pysuite is the higher-priority new test surface. The attribution logic is straightforward (_resolve_backup_service_id) and exercised indirectly through the existing endpoint tests. -
import reinside_slugifyrather than at module top. This avoids adding an import that might confuse ruff's unused-import checks if_slugifyis 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_idhelper 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.pyis still present but now has no router importing it. It is still imported bywidgets/sources.py(JellyfinWidgetSourcedoes 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_slugis not yet exposed via an endpoint. The frontend will need it for/d/:slugrouting. 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."
}