# 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.
