# Tasks — Services as hub IA **Change:** `services-as-hub-ia` **Phase:** tasks **Date:** 2026-06-26 ## Review workload forecast | Field | Value | |-------|-------| | Estimated changed lines | ~4500–6000 | | Chained PRs recommended | Yes (12 slices) | | Chain strategy | stacked-to-main | | Slice order | 1–3 backend → 4 shell → 5–9 content tabs → 10 dashboards → 11 cleanup → 12 verify | Each slice is committed separately. Every slice must leave `cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest` **and** `cd frontend && npm run lint && npm run build && npm run test` green. Every touched page gains a Vitest case at the new route and asserts the old route 404s (where applicable). --- ## Slice 1 — Backend: new service types + Jellyseerr absorption **Goal:** Registry reflects the new world. No frontend change yet. - [ ] **1.1 Add `backups` integration** - Files: `backend/src/media_library_viewer_api/integrations/backups.py` (new), `integrations/registry.py` - Details: `BackupsConfig` (`ingestion_label: str = "default"`), no secrets, widget kind `summary` (move `BackupsWidgetSource` adapter to bind the service_id). Register in `SERVICE_DEFINITIONS`. - [ ] **1.2 Add `authentik` integration** - Files: `integrations/authentik.py` (new), `registry.py` - Details: `AuthentikConfig` (`base_url: ServiceBaseUrl`, `timeout_seconds`), secret `api_token` (required). No widget kinds yet. - [ ] **1.3 Absorb `jellyseerr` into `JellyfinConfig`** - Files: `integrations/jellyfin.py`, `integrations/jellyseerr.py` (delete), `integrations/registry.py`, `integrations/__init__.py` - Details: Add optional `jellyseerr_url`, `jellyseerr_api_key` to `JellyfinConfig`. Delete the `jellyseerr` integration module and registry entry. Update tests. - [ ] **1.4 Jellyseerr migration** - Files: `services/settings_store.py` (`ensure_defaults`) - Details: On startup, migrate existing `jellyseerr` rows into paired `jellyfin` instances per the design. Log a warning for unpaired drops. - [ ] **1.5 Tests** - Update `backend/tests/test_services.py`, `test_widgets.py` for the new types and the migration. Assert registry contains 8 types (alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks). --- ## Slice 2 — Backend: Authentik directory client + endpoint - [ ] **2.1 AuthentikClient** - Files: `clients/authentik.py` (new) - Details: `users(search, page, page_size) -> {items, total}` against the Authentik directory API. Reuse the requests-session pattern from `clients/jellyseerr.py`. Tests: `tests/test_authentik_client.py`. - [ ] **2.2 Directory endpoint** - Files: `routers/authentik_users.py` (new), `main.py` (register router) - Details: `GET /api/services/authentik/{service_id}/users` proxies to the client, resolving the service record via the existing dependency. Tests cover not-configured + unreachable + paginated-success. --- ## Slice 3 — Backend: route cleanup + backups attribution - [ ] **3.1 Remove Users router** - Files: `routers/users.py`, `routers/users_impl.py` (delete), `main.py`, `dependencies.py` - Details: Delete the Jellyfin-backed user directory + message-compose router and its deps. Update `test_api.py` to drop the corresponding tests. - [ ] **3.2 Backups service attribution** - Files: `routers/backups.py`, `services/settings_store.py` - Details: backup report endpoint accepts optional `?service_id=`; first-wins association to an enabled `backups` instance when omitted. Dashboard summary - poller continue to work. - [ ] **3.3 Named dashboards backend** - Files: `models/dashboards.py` (new), `routers/dashboards.py` (new), `services/settings_store.py` (table + CRUD) - Details: `named_dashboards` table (id, slug, label, sort_order, payload JSON of widget+link placements). Endpoints: `GET/POST/PUT/DELETE /api/dashboards`. Tests in `tests/test_dashboards.py`. --- ## Slice 4 — Frontend: top-nav generation + service-page skeleton **Goal:** Data-driven nav + tab-skeleton ServicePage shell. Content tabs are stubs that say "coming soon" so the rest of the app stays green. - [ ] **4.1 Service-type → nav-entry map** - Files: `frontend/src/integrations/navEntries.ts` (new) - Details: Static `SERVICE_TYPE_NAV_ENTRIES` map (jellyfin→Media, ssh_tasks→[Files, Actions], alertmanager→Alerts, etc.). Helper to filter by configured types. - [ ] **4.2 Data-driven nav in `App.tsx`** - Files: `frontend/src/App.tsx` - Details: Replace static `navItems` with the memoized list from design. Add `useDashboards()` and combine with `useServiceInstances()`. Loading skeleton nav until settled. Legacy routes removed; add 404 catch-all. - [ ] **4.3 ServicePage tab skeleton + instance switcher** - Files: `frontend/src/pages/ServicePage.tsx`, new `pages/service-tabs/` directory, `pages/ServiceTypePage.tsx` (redirect resolver) - Details: Refactor ServicePage to render `[Overview, ...content, Widgets, Config]` from `serviceTabs(serviceType)`. Add `/services/:type` resolver route. Content tabs are stub components ("coming soon"). Instance switcher dropdown when siblings > 1. - [ ] **4.4 Empty-state CTAs** - Files: `frontend/src/pages/Dashboard.tsx`, `pages/ServicesPage.tsx` - Details: Dashboard shows "Add a service" CTA when no services. Services page strong empty state. - [ ] **4.5 Tests** - Nav-generation tests, service-page-skeleton tests, 404-on-legacy-routes tests. --- ## Slice 5 — Frontend: Jellyfin content tabs (Media + Requests) - [ ] **5.1 MediaTab** - Files: `pages/service-tabs/MediaTab.tsx` (lift from `pages/Media.tsx`) - Details: Accept `instance` prop, pass `instance.id` to media hooks. Preserve the index build controls + mobile card layout. Delete the old `/media` route and `Applications.tsx` wrapper. - [ ] **5.2 RequestsTab (Jellyseerr enrichment)** - Files: `pages/service-tabs/RequestsTab.tsx` - Details: Source from the absorbed `jellyseerr_url`/`jellyseerr_api_key` on the Jellyfin instance. Render request-management data. - [ ] **5.3 Tests** - New tests for MediaTab (instance-scoped), RequestsTab. Delete old Media page tests. --- ## Slice 6 — Frontend: ssh_tasks content tabs (Files + Actions) - [ ] **6.1 FilesTab** - Files: `pages/service-tabs/FilesTab.tsx` (lift from `FileBrowser.impl.tsx`) - Details: Accept `instance` prop. Delete old `/files` route + page wrapper. - [ ] **6.2 ActionsTab** - Files: `pages/service-tabs/ActionsTab.tsx` (lift from `Actions.tsx`) - Details: Accept `instance` prop. Delete old `/actions` route + page. - [ ] **6.3 Tests** --- ## Slice 7 — Frontend: backups Jobs tab - [ ] **7.1 JobsTab** - Files: `pages/service-tabs/JobsTab.tsx` (lift from `components/BackupsPage.tsx`) - Details: Accept `instance` prop, scope queries by `instance.id`. Delete old `/backups` route + page. - [ ] **7.2 Tests** --- ## Slice 8 — Frontend: Authentik Users + Messaging tabs - [ ] **8.1 UsersTab** - Files: `pages/service-tabs/UsersTab.tsx`, `hooks/useAuthentikUsers.ts`, `api/authentik.ts` - Details: Directory table + search, sourced from the new endpoint. No Jellyfin/Jellyseerr enrichment. - [ ] **8.2 MessagingTab** - Files: `pages/service-tabs/MessagingTab.tsx` (lift compose UI from `UsersPage.impl.tsx`) - Details: Recipient list sourced from Authentik users. Reuse the mail queue + SMTP settings. Delete the old `/users` route + UsersPage. - [ ] **8.3 Tests** --- ## Slice 9 — Frontend: Observability split (Alerts + Links + Metrics tabs) - [ ] **9.1 AlertsTab** - Files: `pages/service-tabs/AlertsTab.tsx` (lift from `ObservabilityPage.tsx`) - Details: Alertmanager alerts view, instance-scoped. Delete old `/observability` route + page. - [ ] **9.2 LinksTab + MetricsTab** - Files: `pages/service-tabs/LinksTab.tsx`, `pages/service-tabs/MetricsTab.tsx` - Details: Grafana deep-links; Prometheus status + PromQL explorer. Each instance-scoped. - [ ] **9.3 Tests** --- ## Slice 10 — Frontend: named dashboards - [ ] **10.1 NamedDashboardPage** - Files: `pages/NamedDashboardPage.tsx`, `hooks/useDashboards.ts`, `api/dashboards.ts` - Details: Render widgets + pinned service links at `/d/:slug`. CRUD via the new endpoints. - [ ] **10.2 Pinned service links** - Files: `components/PinnedServiceLink.tsx`, integration into the dashboard config dialog - Details: Shortcut variant targeting `/services/:type/:id` or a specific tab. - [ ] **10.3 Dashboard management UI** - Files: a new "Manage dashboards" entry on the Services or Settings page - Details: Create/rename/reorder/delete named dashboards. - [ ] **10.4 Tests** --- ## Slice 11 — Cleanup + docs - [ ] **11.1 Delete dead code** - Files: any remaining top-level page wrappers, unused hooks, stale types. - Details: Confirm no references to removed routes/pages remain. - [ ] **11.2 Update `docs/REQUIREMENTS.md`** - Files: `docs/REQUIREMENTS.md` - Details: Rewrite the Information Architecture section. Document the service- type → nav-entry map, the service-page tab skeleton, named dashboards, routing, and the Users→Authentik + Observability-split decisions. - [ ] **11.3 Update `CHANGELOG.md`** --- ## Slice 12 — Verify - [ ] **12.1 Cross-route manual pass** - Details: Walk every service type's page + tabs; walk named dashboards; walk the empty state; confirm legacy routes 404. - [ ] **12.2 Verify report** - Files: `openspec/changes/services-as-hub-ia/verify-report.md` - Details: Per-AC evidence (AC1–AC10), tool versions, manual notes, residual risks. --- ## Notes - Slices 1–3 are backend-only; slice 4 is the frontend shell turning on the new IA with stubs; 5–9 replace stubs with real content; 10 adds named dashboards; 11–12 close out. - Slices 5–9 are independent and can be reordered or parallelized across branches if useful, but each must merge green with its stub replaced. - The frontend content lifts (5–9) are the bulk of the line count; treat each as a self-contained review-sized PR.