# Spec — Services as hub IA **Change:** `services-as-hub-ia` **Phase:** spec **Date:** 2026-06-26 ## Scope Reorganize the frontend information architecture around services as the hub. Operational content (Media, Files, Actions, Users, Backups) moves into service- type-specific tabs on the service page. The top nav shrinks to a small always- visible core (Main Dashboard, Services, Settings) plus conditional per-type entries and user-created named dashboards. Two new service types are added (`backups`, `authentik`); one is absorbed (`jellyseerr` → Jellyfin config). This change spans backend (new service types, Authentik client, Jellyseerr migration, route cleanup) and frontend (service-page IA, top-nav generation, content migration, named dashboards). ## Requirements ### R1 — Top-level navigation - R1.1 The top nav contains, in order: Main Dashboard, named dashboards (one entry each, user-controlled order), conditional service-type entries, Services, Settings. - R1.2 Conditional service-type entries appear only when at least one enabled instance of that type exists. Mapping: - `jellyfin` → "Media" entry → `/services/jellyfin` - `ssh_tasks` → "Files" and "Actions" entries → `/services/ssh_tasks` - `alertmanager` → "Alerts" entry → `/services/alertmanager` - `grafana` → "Grafana" entry → `/services/grafana` - `prometheus` → "Prometheus" entry → `/services/prometheus` - `backups` → "Backups" entry → `/services/backups` - `authentik` → "Users" entry → `/services/authentik` - `nextcloud` → no entry (no operational content) - R1.3 The Main Dashboard is always first and not deletable. - R1.4 The nav is data-driven (reacts to configured services + dashboards) with graceful loading/empty states. ### R2 — Service page IA - R2.1 Every service page uses the tab skeleton: Overview, type-specific content tabs (zero or more), Widgets, Config. - R2.2 The Overview tab shows service health (connection status, version, last error) and a primary metric preview (per-type: live sessions for Jellyfin, active alert count for Alertmanager, etc.). - R2.3 The Widgets and Config tabs are unchanged from today (widget kinds list, non-secret config + secrets editors). - R2.4 Type-specific content tabs: - `jellyfin`: Media (table + index build controls), Requests (Jellyseerr data) - `ssh_tasks`: Files (browser + ffprobe + jobs), Actions (saved tasks CRUD + run) - `backups`: Jobs (jobs + runs + alerts + acknowledge) - `authentik`: Users (directory + search), Messaging (compose + queue status) - `alertmanager`: Alerts (summary + list + severity filter) - `grafana`: Links (configured dashboard deep-links) - `prometheus`: Metrics (status + PromQL explorer) - `nextcloud`: no content tabs (Overview + Widgets + Config only) ### R3 — Instance switcher - R3.1 When more than one enabled instance of a service type exists, the service page renders an instance switcher (dropdown) at the top. - R3.2 The switcher selects the active instance; all tabs reflect the selected instance. - R3.3 The default selected instance is the first enabled instance (or the one named "primary" if multiple-selection is added later — out of scope here). - R3.4 Single-instance types do not render the switcher. ### R4 — Routing - R4.1 `/` — Main Dashboard (special, default landing, not deletable). - R4.2 `/d/:slug` — named dashboard. - R4.3 `/services` — services admin hub (list of all instances, grouped by type, with add/edit/delete). - R4.4 `/services/:type` — service page for the first enabled instance of the type; redirects (client-side) to `/services/:type/:id` once an instance is resolved. - R4.5 `/services/:type/:id` — service page for a specific instance. - R4.6 `/settings` — settings (unchanged). - R4.7 Legacy routes (`/media`, `/files`, `/actions`, `/users`, `/observability`, `/backups`) return 404 — no redirects, no aliases. ### R5 — Named dashboards - R5.1 Any authenticated user can create, edit, reorder, and delete named dashboards (global scope — shared across users in this change). - R5.2 A named dashboard holds an ordered list of widgets (existing widget kinds only) and pinned service links (shortcut to a service page or specific tab). - R5.3 Each named dashboard has a user-chosen label and a URL slug derived from it (uniqueness enforced). - R5.4 The Main Dashboard is special: it cannot be deleted, is always first in the nav, and its slug is reserved. ### R6 — Service type changes - R6.1 **NEW `backups`** service type: config holds ingestion source metadata; the existing REST report endpoint attributes incoming reports to a backups service instance (first-wins when none is specified). - R6.2 **NEW `authentik`** service type: config holds base_url; secret holds the API token. Provides a Users widget and a user-directory endpoint consumed by the Authentik service page. - R6.3 **ABSORBED `jellyseerr`**: removed as a service type. Its config fields (`base_url`, `api_key`) become optional fields on `JellyfinConfig`. Existing Jellyseerr service instances are migrated into their paired Jellyfin's config at backend startup; unpaired instances are dropped with a logged warning. ### R7 — Users → Authentik - R7.1 The Jellyfin-backed user directory, Jellyfin-email message compose, and Jellyseerr-enrichment-of-Jellyfin-users flows are removed. - R7.2 The Authentik service page Users tab sources users from Authentik's directory API (paginated, searchable). - R7.3 The Authentik Messaging tab hosts message-compose, emailing Authentik- sourced users via the existing SMTP settings and mail queue. - R7.4 OIDC authentication is unchanged. ### R8 — Observability - R8.1 The Observability page is removed. - R8.2 Alertmanager alerts, Grafana links, and Prometheus status each render on their respective service-type pages as content tabs. - R8.3 There is no cross-service aggregate view built-in. Users who want one build it via widgets on a named dashboard. ### R9 — Empty state - R9.1 A fresh install (no services, no dashboards) lands on `/` with an empty- state CTA pointing to `/services`. - R9.2 The Services hub shows a strong empty state ("Add a service to get started") when no service instances exist. ### R10 — Non-regression - R10.1 The existing widget system, ServicePage config/secrets editing, settings (machines, SSH keys), and authentication continue to work. - R10.2 The backend backup report endpoint, mail queue, and observability metrics endpoints continue to function (they may gain a service_id association). - R10.3 Mobile responsive behavior (already shipped) is preserved across the new IA. ## Acceptance criteria - AC1 The top nav renders exactly: Main Dashboard, named dashboards, configured- service-type entries, Services, Settings — and nothing else. - AC2 Each content tab listed in R2.4 renders its full operational content inside the corresponding service page. - AC3 An instance switcher appears when >1 enabled instance of a type exists and is absent otherwise. - AC4 Creating, editing, reordering, and deleting a named dashboard works; each appears in the nav and is reachable at `/d/:slug`. - AC5 Legacy routes return 404. - AC6 The `backups` and `authentik` service types appear in the service-type list and can be configured like any other service. - AC7 Existing Jellyseerr service instances are migrated into Jellyfin config (or dropped with a logged warning when unpaired). - AC8 A fresh install lands on `/` with the empty-state CTA. - AC9 `cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest` is green. - AC10 `cd frontend && npm run lint && npm run build && npm run test` is green. ## Non-goals - Per-instance top-level nav entries. - Legacy-route redirects or aliases. - New widget kinds (pinned service links are a shortcut variant, not a widget kind). - Per-user dashboard customization. - Changes to OIDC authentication. - Mobile-specific IA divergence.