Combine both branches into a single coherent branch: - Full mobile responsive parity (useIsMobile, MobileCardRow, SheetForm, .mobile-touch-target, mobile cards, SheetForm forms, 44px targets, dirty-state confirm, TablePagination, refetchIntervalInBackground). - Full services-as-hub IA (data-driven nav, service-page tab skeleton, new service types, Authentik directory + messaging, named dashboards, legacy routes 404, Observability split, Jellyseerr absorbed). Enhancement: service tabs now use mobile-parity primitives: - MediaTab: MobileCardRow below md (title/size/HDR/library/year) + TablePagination; DataTable at md+ (desktop branch preserved). - FilesTab: MobileCardRow below md (name/type/size/modified) + handleRowClick; DataTable at md+. - ServicePage: SheetForm branch below md (open-on-mount, sticky header + save bar, cancel navigates back to /services, dirty-state guard). - Dashboard: single-column + section anchors below md (from mobile-parity) + empty-state CTA (from services-hub). - App.tsx: useIsMobile() replaces inline matchMedia (from mobile-parity) + data-driven useNavItems (from services-hub). - Backup tables (BackupAlerts/Jobs/Runs) already have MobileCardRow from mobile-parity; JobsTab inherits mobile behavior through its sub-components. Conflict resolutions: - Backend: entirely from services-hub (mobile didn't touch it). - Deleted pages (Media/FileBrowser/Actions/Users/UsersPage/Applications/ ObservabilityPage/BackupsPage + hooks/useUsers + tests): kept deleted (services-hub deleted them; content moved into service tabs). - New service-tabs/*: from services-hub, enhanced with mobile patterns. - App.tsx: services-hub's data-driven nav + mobile-parity's useIsMobile. - Dashboard.tsx: merged (services-hub CTA + mobile-parity sections/anchors). - ServicePage.tsx: services-hub's tab skeleton + mobile-parity's SheetForm. - Primitives (useIsMobile/mobile-card/sheet-form/etc.): from mobile-parity. 117 frontend tests pass (mobile-parity's 122 - 5 deleted page tests + services-hub's new tab/dashboard tests); 271 backend tests pass; lint/ build green both sides.
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:
- 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.
- 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. TheBackupsPagecontent (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,nextcloudkeep 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 |