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