Files
manage/openspec/changes/services-as-hub-ia/proposal.md
T
Developer fe028b0e6f Plan services-as-hub IA rework (OpenSpec change)
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).
2026-06-26 17:16:52 +00:00

11 KiB

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