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).
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user