# Proposal — Services as hub IA **Change:** `services-as-hub-ia` **Phase:** proposal **Date:** 2026-06-26 ## Problem The current information architecture treats **concepts** (Media, Files, Actions, Users, Observability, Backups) as first-class top-level destinations. Services (Jellyfin, SSH, Alertmanager, etc.) are configured separately and reached via a "Services" admin page that holds only connection config + widgets. This produces two problems: 1. **Duplicated ontology.** "Media" and "the Jellyfin service page" are two different places that both reference the same Jellyfin instance. The Media page is where you browse; the service page is where you configure. There is no single "Jellyfin" place. 2. **Concept-pages assume exactly one source.** The Media page assumes media comes from Jellyfin, the Files page assumes files come from SSH, the Users page assumes users come from Jellyfin. Multi-instance setups (2 Jellyfins, 2 SSH targets) have no first-class home; you switch via query params. Meanwhile, several concepts have outgrown their current shape: - **Users** is Jellyfin-specific and overlaps with the OIDC provider (Authentik) that already holds the canonical user directory. Maintaining a parallel Jellyfin-only user directory is duplicated work. - **Observability** aggregates three service types (Alertmanager, Grafana, Prometheus) into one page, but each of those services is already a first-class registry instance. The aggregate page is a special case. - **Backups** receives reports via a REST endpoint but has no service-record home; it cannot be named, multi-instanced, or surfaced like other services. - **Jellyseerr** is configured as a separate service but its only role is enriching Jellyfin users — it has no standalone value. ## Proposal Reorganize the app around **services as the hub**. The top-level navigation shrinks to a tiny always-visible core plus **conditional per-type entries** that materialize only when a matching service is configured. Operational content (Media, Files, Actions, Users) moves **into the service page** as tabs. ### Top-level navigation (after) - **Main Dashboard** (always visible, special, at `/`) - **Named dashboards** (always visible once created; one top-level entry each, at `/d/:slug`) - **Conditional service-type entries** — one per configured service type, linking to the type's service page with an in-page instance switcher: - "Media" appears when a Jellyfin service exists - "Files" and "Actions" appear when an ssh_tasks service exists - "Alerts" when Alertmanager exists; "Grafana" when Grafana exists; "Prometheus" when Prometheus exists (Observability page is removed) - "Backups" when a backups service exists - "Users" when an Authentik service exists - **Services** (always visible — the admin hub for managing service instances) - **Settings** (always visible — unchanged) ### Service page IA (after) Every service page uses the same tab skeleton: ``` [Overview] [type-specific content tabs...] [Widgets] [Config] ``` - **Overview** — service health + key metrics (connection status, version, primary widget preview). - **Content tabs** — per service type: - **Jellyfin**: Media (table + index build), Requests (Jellyseerr enrichment) - **ssh_tasks**: Files (browser + ffprobe), Actions (saved tasks) - **backups**: Jobs (jobs + runs + alerts) - **authentik**: Users (directory), Messaging (compose) - **alertmanager**: Alerts - **grafana**: Links - **prometheus**: Metrics / status - **Widgets** — widget kinds this service provides (unchanged from today). - **Config** — non-secret config + secrets (unchanged from today). Multi-instance: when >1 instance of a type exists, the service page shows an **instance switcher** (dropdown at the top of the page) rather than separate routes per instance. ### Service type changes - **NEW: `backups`** — becomes a service type in the registry. The current REST report endpoint keeps working for passive ingestion; reports are attributed to a backups service instance. The `BackupsPage` content (jobs/runs/alerts) moves into the backups service page's Jobs tab. - **NEW: `authentik`** — becomes a service type. Its Users tab is the new user directory (replacing the Jellyfin-based Users page). Its Messaging tab hosts the message-compose flow, emailing Authentik-sourced users via the existing SMTP settings. OIDC auth flow is unchanged. - **ABSORBED: `jellyseerr`** — ceases to be its own service type. Its config fields (`base_url`, `api_key`) move onto the Jellyfin service config as optional fields. The Jellyfin service page gains a Requests tab backed by the configured Jellyseerr. Existing Jellyseerr service instances are migrated into their paired Jellyfin's config (or dropped if no pairing can be inferred). - **UNCHANGED**: `alertmanager`, `grafana`, `prometheus`, `ssh_tasks`, `nextcloud` keep their service-type status. Their operational content (if any) moves into tabs on their service page. ### Removed / replaced - **`/media`** — content moves into Jellyfin service page (Media tab). Old route returns 404. - **`/files`, `/actions`** — content moves into ssh_tasks service page (Files / Actions tabs). Old routes return 404. - **`/users`** — replaced by Authentik service page (Users tab). Old route returns 404. The Jellyfin-backed user directory, Jellyfin-email message compose, and Jellyseerr-enrichment-of-Jellyfin-users are removed. - **`/observability`** — removed. Its content splits across the Alertmanager, Grafana, and Prometheus service pages. Old route returns 404. The cross- service "single pane of glass" is intentionally sacrificed; users who want it build it on a named dashboard via widgets. - **`/backups`** — content moves into the backups service page (Jobs tab). Old route returns 404. - **Jellyseerr service type** — configuration absorbed into Jellyfin. ### Named dashboards - The main Dashboard at `/` stays **special** (the default landing, not deletable, always first in nav). - Users can create **named dashboards** at `/d/:slug`. Each named dashboard is a configurable grid of **widgets + pinned service links** (shortcuts to specific service pages or tabs). - Each named dashboard appears as its own top-level nav entry, in a user- controlled order. The main dashboard always sits first. ### Routing - `/` — main Dashboard (special, default landing) - `/d/:slug` — named dashboard - `/services` — services admin hub (list of all service instances, grouped by type) - `/services/:type` — service page for the first/primary instance of a type, with an instance switcher when >1 exists - `/services/:type/:id` — service page for a specific instance - `/settings` — settings (unchanged) - All legacy top-level routes (`/media`, `/files`, `/actions`, `/users`, `/observability`, `/backups`) return **404** — no redirects, no aliases. ### Empty state A fresh install with no services configured and no dashboards lands on the main Dashboard with a strong CTA ("Add a service to get started" → Services). The Services page has a matching empty state. Top nav shows only Dashboard / Services / Settings until services or dashboards are added. ## Non-goals - **No changes to OIDC / SSO authentication.** Authentik-as-IdP keeps doing what it does today; this change adds Authentik-as-directory-source only. - **No per-instance top-level entries.** A type gets one conditional entry with an in-page instance switcher; nav does not grow with the number of instances. - **No legacy-route redirects.** Old URLs 404; bookmarks must be updated. - **No tablet-specific or mobile-specific IA divergence.** The IA is the same across breakpoints (mobile responsive parity already shipped). - **No new widget kinds.** Named dashboards compose existing widget kinds plus pinned service links (a new shortcut variant, not a widget kind). - **No backend API contract changes beyond the new service types and the Authentik directory endpoint.** Existing endpoints keep their shape. - **No multi-tenant or per-user dashboard customization.** Dashboards are global (shared across all authenticated users) in this change. ## Key technical risks - **Content migration is large.** Media, Files, Actions, Users, Backups each move from a top-level page into a service tab. Each is a non-trivial component with its own hooks, tests, and state. This is the bulk of the implementation risk and review burden. - **Jellyseerr absorption migration.** Existing Jellyseerr service instances must be migrated into their paired Jellyfin's config at backend startup, with a clear policy when pairing is ambiguous (multiple Jellyfins, no Jellyfin). - **Authentik directory API.** The Authentik service page needs a backend client that queries Authentik's user/group directory API. Scope of that API (which fields, pagination, search) must be pinned during design. - **Nav generation is data-driven.** Top nav must react to configured services and existing dashboards. This is a new TanStack-Query dependency in the App shell, with loading/empty states. - **Backups attribution.** Existing backup reports have no service_id. The migration must assign them to a backups service instance (first-wins or job-name-matching policy). ## Risks (flagged, not blocking) - **Loss of cross-service Observability overview.** A fresh install with no dashboards configured has no alerts-overview until the user builds one. The mitigation (widgets on a named dashboard) is real but requires user setup. Revisit if it bites. - **Authentik directory coverage.** Authentik's user directory may not carry the same fields the current Jellyfin-based messaging flow relied on (e.g. Jellyfin- specific avatar URLs, activity state). Some fields will simply go away. ## Decision matrix (from grilling) | # | Decision | Choice | |---|----------|--------| | D1 | Top nav model | Conditional type entries (one per configured service type, appearing only when configured) | | D2 | Multi-instance | Type + instance switcher on the service page | | D3 | Files + Actions | Move into ssh_tasks service page as tabs | | D4 | Backups | New service type in the registry | | D5 | Users | Replaced by Authentik (included in this change) | | D6 | Observability | Split per service type (no aggregate page) | | D7 | Main Dashboard | Stays special at `/`, not deletable, default landing | | D8 | Named dashboards | Widgets + pinned service links | | D9 | Named dashboards nav | Each named dashboard = one top-level entry | | D10 | Authentik role | User directory source (OIDC auth unchanged) | | D11 | Messaging | Moves to Authentik service page; emails Authentik users via existing SMTP | | D12 | Jellyseerr | Absorbed into Jellyfin config (no longer its own service type) | | D13 | Service page tabs | Standard skeleton: Overview \u2234 content \u2234 Widgets \u2234 Config | | D14 | Overview tab | Health + key metrics | | D15 | Routing | `/services/:type/:id`, `/services/:type` (first/primary), `/d/:slug`, `/` | | D16 | Legacy routes | Return 404 (no redirects, no aliases) | | D17 | Empty state | Dashboard CTA + Services empty state |