Files
manage/openspec/changes/services-as-hub-ia/spec.md
T
Developer 01527ae4f0 Rebase services-as-hub-ia onto mobile-responsive-parity
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.
2026-06-26 21:08:51 +00:00

7.9 KiB

Spec — Services as hub IA

Change: services-as-hub-ia Phase: spec Date: 2026-06-26

Scope

Reorganize the frontend information architecture around services as the hub. Operational content (Media, Files, Actions, Users, Backups) moves into service- type-specific tabs on the service page. The top nav shrinks to a small always- visible core (Main Dashboard, Services, Settings) plus conditional per-type entries and user-created named dashboards. Two new service types are added (backups, authentik); one is absorbed (jellyseerr → Jellyfin config).

This change spans backend (new service types, Authentik client, Jellyseerr migration, route cleanup) and frontend (service-page IA, top-nav generation, content migration, named dashboards).

Requirements

R1 — Top-level navigation

  • R1.1 The top nav contains, in order: Main Dashboard, named dashboards (one entry each, user-controlled order), conditional service-type entries, Services, Settings.
  • R1.2 Conditional service-type entries appear only when at least one enabled instance of that type exists. Mapping:
    • jellyfin → "Media" entry → /services/jellyfin
    • ssh_tasks → "Files" and "Actions" entries → /services/ssh_tasks
    • alertmanager → "Alerts" entry → /services/alertmanager
    • grafana → "Grafana" entry → /services/grafana
    • prometheus → "Prometheus" entry → /services/prometheus
    • backups → "Backups" entry → /services/backups
    • authentik → "Users" entry → /services/authentik
    • nextcloud → no entry (no operational content)
  • R1.3 The Main Dashboard is always first and not deletable.
  • R1.4 The nav is data-driven (reacts to configured services + dashboards) with graceful loading/empty states.

R2 — Service page IA

  • R2.1 Every service page uses the tab skeleton: Overview, type-specific content tabs (zero or more), Widgets, Config.
  • R2.2 The Overview tab shows service health (connection status, version, last error) and a primary metric preview (per-type: live sessions for Jellyfin, active alert count for Alertmanager, etc.).
  • R2.3 The Widgets and Config tabs are unchanged from today (widget kinds list, non-secret config + secrets editors).
  • R2.4 Type-specific content tabs:
    • jellyfin: Media (table + index build controls), Requests (Jellyseerr data)
    • ssh_tasks: Files (browser + ffprobe + jobs), Actions (saved tasks CRUD + run)
    • backups: Jobs (jobs + runs + alerts + acknowledge)
    • authentik: Users (directory + search), Messaging (compose + queue status)
    • alertmanager: Alerts (summary + list + severity filter)
    • grafana: Links (configured dashboard deep-links)
    • prometheus: Metrics (status + PromQL explorer)
    • nextcloud: no content tabs (Overview + Widgets + Config only)

R3 — Instance switcher

  • R3.1 When more than one enabled instance of a service type exists, the service page renders an instance switcher (dropdown) at the top.
  • R3.2 The switcher selects the active instance; all tabs reflect the selected instance.
  • R3.3 The default selected instance is the first enabled instance (or the one named "primary" if multiple-selection is added later — out of scope here).
  • R3.4 Single-instance types do not render the switcher.

R4 — Routing

  • R4.1 / — Main Dashboard (special, default landing, not deletable).
  • R4.2 /d/:slug — named dashboard.
  • R4.3 /services — services admin hub (list of all instances, grouped by type, with add/edit/delete).
  • R4.4 /services/:type — service page for the first enabled instance of the type; redirects (client-side) to /services/:type/:id once an instance is resolved.
  • R4.5 /services/:type/:id — service page for a specific instance.
  • R4.6 /settings — settings (unchanged).
  • R4.7 Legacy routes (/media, /files, /actions, /users, /observability, /backups) return 404 — no redirects, no aliases.

R5 — Named dashboards

  • R5.1 Any authenticated user can create, edit, reorder, and delete named dashboards (global scope — shared across users in this change).
  • R5.2 A named dashboard holds an ordered list of widgets (existing widget kinds only) and pinned service links (shortcut to a service page or specific tab).
  • R5.3 Each named dashboard has a user-chosen label and a URL slug derived from it (uniqueness enforced).
  • R5.4 The Main Dashboard is special: it cannot be deleted, is always first in the nav, and its slug is reserved.

R6 — Service type changes

  • R6.1 NEW backups service type: config holds ingestion source metadata; the existing REST report endpoint attributes incoming reports to a backups service instance (first-wins when none is specified).
  • R6.2 NEW authentik service type: config holds base_url; secret holds the API token. Provides a Users widget and a user-directory endpoint consumed by the Authentik service page.
  • R6.3 ABSORBED jellyseerr: removed as a service type. Its config fields (base_url, api_key) become optional fields on JellyfinConfig. Existing Jellyseerr service instances are migrated into their paired Jellyfin's config at backend startup; unpaired instances are dropped with a logged warning.

R7 — Users → Authentik

  • R7.1 The Jellyfin-backed user directory, Jellyfin-email message compose, and Jellyseerr-enrichment-of-Jellyfin-users flows are removed.
  • R7.2 The Authentik service page Users tab sources users from Authentik's directory API (paginated, searchable).
  • R7.3 The Authentik Messaging tab hosts message-compose, emailing Authentik- sourced users via the existing SMTP settings and mail queue.
  • R7.4 OIDC authentication is unchanged.

R8 — Observability

  • R8.1 The Observability page is removed.
  • R8.2 Alertmanager alerts, Grafana links, and Prometheus status each render on their respective service-type pages as content tabs.
  • R8.3 There is no cross-service aggregate view built-in. Users who want one build it via widgets on a named dashboard.

R9 — Empty state

  • R9.1 A fresh install (no services, no dashboards) lands on / with an empty- state CTA pointing to /services.
  • R9.2 The Services hub shows a strong empty state ("Add a service to get started") when no service instances exist.

R10 — Non-regression

  • R10.1 The existing widget system, ServicePage config/secrets editing, settings (machines, SSH keys), and authentication continue to work.
  • R10.2 The backend backup report endpoint, mail queue, and observability metrics endpoints continue to function (they may gain a service_id association).
  • R10.3 Mobile responsive behavior (already shipped) is preserved across the new IA.

Acceptance criteria

  • AC1 The top nav renders exactly: Main Dashboard, named dashboards, configured- service-type entries, Services, Settings — and nothing else.
  • AC2 Each content tab listed in R2.4 renders its full operational content inside the corresponding service page.
  • AC3 An instance switcher appears when >1 enabled instance of a type exists and is absent otherwise.
  • AC4 Creating, editing, reordering, and deleting a named dashboard works; each appears in the nav and is reachable at /d/:slug.
  • AC5 Legacy routes return 404.
  • AC6 The backups and authentik service types appear in the service-type list and can be configured like any other service.
  • AC7 Existing Jellyseerr service instances are migrated into Jellyfin config (or dropped with a logged warning when unpaired).
  • AC8 A fresh install lands on / with the empty-state CTA.
  • AC9 cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest is green.
  • AC10 cd frontend && npm run lint && npm run build && npm run test is green.

Non-goals

  • Per-instance top-level nav entries.
  • Legacy-route redirects or aliases.
  • New widget kinds (pinned service links are a shortcut variant, not a widget kind).
  • Per-user dashboard customization.
  • Changes to OIDC authentication.
  • Mobile-specific IA divergence.