Reorganize the app around services as the hub. Operational content (Media, Files, Actions, Users, Backups) moves into type-specific tabs on the service page. Top nav shrinks to Main Dashboard + named dashboards + conditional per-type entries (appear when configured) + Services + Settings. Decisions (D1-D17): conditional type entries; instance switcher for multi-instance; Files/Actions into ssh_tasks tabs; Backups = new service type; Users -> Authentik (in scope); Observability split per type (no aggregate); main dashboard special at /; named dashboards = widgets + pinned service links; each named dashboard = top entry; Authentik = directory source (OIDC unchanged); Messaging -> Authentik users via SMTP; Jellyseerr absorbed into Jellyfin config; standard tab skeleton (Overview | content | Widgets | Config); Overview = health + metrics; routing /services/:type/:id + /d/:slug; legacy routes 404; empty-state CTAs. Authentik directory client + endpoint included. 12 chained PRs forecast (backend types -> frontend shell -> content tabs -> dashboards -> cleanup -> verify).
10 KiB
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
backupsintegration- Files:
backend/src/media_library_viewer_api/integrations/backups.py(new),integrations/registry.py - Details:
BackupsConfig(ingestion_label: str = "default"), no secrets, widget kindsummary(moveBackupsWidgetSourceadapter to bind the service_id). Register inSERVICE_DEFINITIONS.
- Files:
-
1.2 Add
authentikintegration- Files:
integrations/authentik.py(new),registry.py - Details:
AuthentikConfig(base_url: ServiceBaseUrl,timeout_seconds), secretapi_token(required). No widget kinds yet.
- Files:
-
1.3 Absorb
jellyseerrintoJellyfinConfig- Files:
integrations/jellyfin.py,integrations/jellyseerr.py(delete),integrations/registry.py,integrations/__init__.py - Details: Add optional
jellyseerr_url,jellyseerr_api_keytoJellyfinConfig. Delete thejellyseerrintegration module and registry entry. Update tests.
- Files:
-
1.4 Jellyseerr migration
- Files:
services/settings_store.py(ensure_defaults) - Details: On startup, migrate existing
jellyseerrrows into pairedjellyfininstances per the design. Log a warning for unpaired drops.
- Files:
-
1.5 Tests
- Update
backend/tests/test_services.py,test_widgets.pyfor the new types and the migration. Assert registry contains 8 types (alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks).
- Update
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 fromclients/jellyseerr.py. Tests:tests/test_authentik_client.py.
- Files:
-
2.2 Directory endpoint
- Files:
routers/authentik_users.py(new),main.py(register router) - Details:
GET /api/services/authentik/{service_id}/usersproxies to the client, resolving the service record via the existing dependency. Tests cover not-configured + unreachable + paginated-success.
- Files:
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.pyto drop the corresponding tests.
- Files:
-
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 enabledbackupsinstance when omitted. Dashboard summary- poller continue to work.
- Files:
-
3.3 Named dashboards backend
- Files:
models/dashboards.py(new),routers/dashboards.py(new),services/settings_store.py(table + CRUD) - Details:
named_dashboardstable (id, slug, label, sort_order, payload JSON of widget+link placements). Endpoints:GET/POST/PUT/DELETE /api/dashboards. Tests intests/test_dashboards.py.
- Files:
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_ENTRIESmap (jellyfin→Media, ssh_tasks→[Files, Actions], alertmanager→Alerts, etc.). Helper to filter by configured types.
- Files:
-
4.2 Data-driven nav in
App.tsx- Files:
frontend/src/App.tsx - Details: Replace static
navItemswith the memoized list from design. AdduseDashboards()and combine withuseServiceInstances(). Loading skeleton nav until settled. Legacy routes removed; add 404 catch-all.
- Files:
-
4.3 ServicePage tab skeleton + instance switcher
- Files:
frontend/src/pages/ServicePage.tsx, newpages/service-tabs/directory,pages/ServiceTypePage.tsx(redirect resolver) - Details: Refactor ServicePage to render
[Overview, ...content, Widgets, Config]fromserviceTabs(serviceType). Add/services/:typeresolver route. Content tabs are stub components ("coming soon"). Instance switcher dropdown when siblings > 1.
- Files:
-
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.
- Files:
-
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 frompages/Media.tsx) - Details: Accept
instanceprop, passinstance.idto media hooks. Preserve the index build controls + mobile card layout. Delete the old/mediaroute andApplications.tsxwrapper.
- Files:
-
5.2 RequestsTab (Jellyseerr enrichment)
- Files:
pages/service-tabs/RequestsTab.tsx - Details: Source from the absorbed
jellyseerr_url/jellyseerr_api_keyon the Jellyfin instance. Render request-management data.
- Files:
-
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 fromFileBrowser.impl.tsx) - Details: Accept
instanceprop. Delete old/filesroute + page wrapper.
- Files:
-
6.2 ActionsTab
- Files:
pages/service-tabs/ActionsTab.tsx(lift fromActions.tsx) - Details: Accept
instanceprop. Delete old/actionsroute + page.
- Files:
-
6.3 Tests
Slice 7 — Frontend: backups Jobs tab
-
7.1 JobsTab
- Files:
pages/service-tabs/JobsTab.tsx(lift fromcomponents/BackupsPage.tsx) - Details: Accept
instanceprop, scope queries byinstance.id. Delete old/backupsroute + page.
- Files:
-
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.
- Files:
-
8.2 MessagingTab
- Files:
pages/service-tabs/MessagingTab.tsx(lift compose UI fromUsersPage.impl.tsx) - Details: Recipient list sourced from Authentik users. Reuse the mail queue +
SMTP settings. Delete the old
/usersroute + UsersPage.
- Files:
-
8.3 Tests
Slice 9 — Frontend: Observability split (Alerts + Links + Metrics tabs)
-
9.1 AlertsTab
- Files:
pages/service-tabs/AlertsTab.tsx(lift fromObservabilityPage.tsx) - Details: Alertmanager alerts view, instance-scoped. Delete old
/observabilityroute + page.
- Files:
-
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.
- Files:
-
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.
- Files:
-
10.2 Pinned service links
- Files:
components/PinnedServiceLink.tsx, integration into the dashboard config dialog - Details: Shortcut variant targeting
/services/:type/:idor a specific tab.
- Files:
-
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.
- Files:
-
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.
- Files:
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.