01527ae4f0
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.
217 lines
11 KiB
Markdown
217 lines
11 KiB
Markdown
# 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 |
|