a8dfbd5dc6
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).
133 lines
9.9 KiB
Markdown
133 lines
9.9 KiB
Markdown
# 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."
|
|
}
|
|
```
|