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.
271 lines
10 KiB
Markdown
271 lines
10 KiB
Markdown
# Tasks — Services as hub IA
|
||
|
||
**Change:** `services-as-hub-ia`
|
||
**Phase:** tasks
|
||
**Date:** 2026-06-26
|
||
|
||
## Review workload forecast
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Estimated changed lines | ~4500–6000 |
|
||
| Chained PRs recommended | Yes (12 slices) |
|
||
| Chain strategy | stacked-to-main |
|
||
| Slice order | 1–3 backend → 4 shell → 5–9 content tabs → 10 dashboards → 11 cleanup → 12 verify |
|
||
|
||
Each slice is committed separately. Every slice must leave
|
||
`cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest` **and**
|
||
`cd frontend && npm run lint && npm run build && npm run test` green. Every
|
||
touched page gains a Vitest case at the new route and asserts the old route 404s
|
||
(where applicable).
|
||
|
||
---
|
||
|
||
## Slice 1 — Backend: new service types + Jellyseerr absorption
|
||
|
||
**Goal:** Registry reflects the new world. No frontend change yet.
|
||
|
||
- [ ] **1.1 Add `backups` integration**
|
||
- Files: `backend/src/media_library_viewer_api/integrations/backups.py` (new),
|
||
`integrations/registry.py`
|
||
- Details: `BackupsConfig` (`ingestion_label: str = "default"`), no secrets,
|
||
widget kind `summary` (move `BackupsWidgetSource` adapter to bind the
|
||
service_id). Register in `SERVICE_DEFINITIONS`.
|
||
|
||
- [ ] **1.2 Add `authentik` integration**
|
||
- Files: `integrations/authentik.py` (new), `registry.py`
|
||
- Details: `AuthentikConfig` (`base_url: ServiceBaseUrl`, `timeout_seconds`),
|
||
secret `api_token` (required). No widget kinds yet.
|
||
|
||
- [ ] **1.3 Absorb `jellyseerr` into `JellyfinConfig`**
|
||
- Files: `integrations/jellyfin.py`, `integrations/jellyseerr.py` (delete),
|
||
`integrations/registry.py`, `integrations/__init__.py`
|
||
- Details: Add optional `jellyseerr_url`, `jellyseerr_api_key` to
|
||
`JellyfinConfig`. Delete the `jellyseerr` integration module and registry
|
||
entry. Update tests.
|
||
|
||
- [ ] **1.4 Jellyseerr migration**
|
||
- Files: `services/settings_store.py` (`ensure_defaults`)
|
||
- Details: On startup, migrate existing `jellyseerr` rows into paired
|
||
`jellyfin` instances per the design. Log a warning for unpaired drops.
|
||
|
||
- [ ] **1.5 Tests**
|
||
- Update `backend/tests/test_services.py`, `test_widgets.py` for the new types
|
||
and the migration. Assert registry contains 8 types (alertmanager, authentik,
|
||
backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks).
|
||
|
||
---
|
||
|
||
## Slice 2 — Backend: Authentik directory client + endpoint
|
||
|
||
- [ ] **2.1 AuthentikClient**
|
||
- Files: `clients/authentik.py` (new)
|
||
- Details: `users(search, page, page_size) -> {items, total}` against the
|
||
Authentik directory API. Reuse the requests-session pattern from
|
||
`clients/jellyseerr.py`. Tests: `tests/test_authentik_client.py`.
|
||
|
||
- [ ] **2.2 Directory endpoint**
|
||
- Files: `routers/authentik_users.py` (new), `main.py` (register router)
|
||
- Details: `GET /api/services/authentik/{service_id}/users` proxies to the
|
||
client, resolving the service record via the existing dependency. Tests
|
||
cover not-configured + unreachable + paginated-success.
|
||
|
||
---
|
||
|
||
## Slice 3 — Backend: route cleanup + backups attribution
|
||
|
||
- [ ] **3.1 Remove Users router**
|
||
- Files: `routers/users.py`, `routers/users_impl.py` (delete), `main.py`,
|
||
`dependencies.py`
|
||
- Details: Delete the Jellyfin-backed user directory + message-compose router
|
||
and its deps. Update `test_api.py` to drop the corresponding tests.
|
||
|
||
- [ ] **3.2 Backups service attribution**
|
||
- Files: `routers/backups.py`, `services/settings_store.py`
|
||
- Details: backup report endpoint accepts optional `?service_id=`; first-wins
|
||
association to an enabled `backups` instance when omitted. Dashboard summary
|
||
- poller continue to work.
|
||
|
||
- [ ] **3.3 Named dashboards backend**
|
||
- Files: `models/dashboards.py` (new), `routers/dashboards.py` (new),
|
||
`services/settings_store.py` (table + CRUD)
|
||
- Details: `named_dashboards` table (id, slug, label, sort_order, payload JSON
|
||
of widget+link placements). Endpoints: `GET/POST/PUT/DELETE /api/dashboards`.
|
||
Tests in `tests/test_dashboards.py`.
|
||
|
||
---
|
||
|
||
## Slice 4 — Frontend: top-nav generation + service-page skeleton
|
||
|
||
**Goal:** Data-driven nav + tab-skeleton ServicePage shell. Content tabs are
|
||
stubs that say "coming soon" so the rest of the app stays green.
|
||
|
||
- [ ] **4.1 Service-type → nav-entry map**
|
||
- Files: `frontend/src/integrations/navEntries.ts` (new)
|
||
- Details: Static `SERVICE_TYPE_NAV_ENTRIES` map (jellyfin→Media,
|
||
ssh_tasks→[Files, Actions], alertmanager→Alerts, etc.). Helper to filter by
|
||
configured types.
|
||
|
||
- [ ] **4.2 Data-driven nav in `App.tsx`**
|
||
- Files: `frontend/src/App.tsx`
|
||
- Details: Replace static `navItems` with the memoized list from design. Add
|
||
`useDashboards()` and combine with `useServiceInstances()`. Loading skeleton
|
||
nav until settled. Legacy routes removed; add 404 catch-all.
|
||
|
||
- [ ] **4.3 ServicePage tab skeleton + instance switcher**
|
||
- Files: `frontend/src/pages/ServicePage.tsx`, new `pages/service-tabs/`
|
||
directory, `pages/ServiceTypePage.tsx` (redirect resolver)
|
||
- Details: Refactor ServicePage to render `[Overview, ...content, Widgets,
|
||
Config]` from `serviceTabs(serviceType)`. Add `/services/:type` resolver
|
||
route. Content tabs are stub components ("coming soon"). Instance switcher
|
||
dropdown when siblings > 1.
|
||
|
||
- [ ] **4.4 Empty-state CTAs**
|
||
- Files: `frontend/src/pages/Dashboard.tsx`, `pages/ServicesPage.tsx`
|
||
- Details: Dashboard shows "Add a service" CTA when no services. Services
|
||
page strong empty state.
|
||
|
||
- [ ] **4.5 Tests**
|
||
- Nav-generation tests, service-page-skeleton tests, 404-on-legacy-routes
|
||
tests.
|
||
|
||
---
|
||
|
||
## Slice 5 — Frontend: Jellyfin content tabs (Media + Requests)
|
||
|
||
- [ ] **5.1 MediaTab**
|
||
- Files: `pages/service-tabs/MediaTab.tsx` (lift from `pages/Media.tsx`)
|
||
- Details: Accept `instance` prop, pass `instance.id` to media hooks. Preserve
|
||
the index build controls + mobile card layout. Delete the old `/media` route
|
||
and `Applications.tsx` wrapper.
|
||
|
||
- [ ] **5.2 RequestsTab (Jellyseerr enrichment)**
|
||
- Files: `pages/service-tabs/RequestsTab.tsx`
|
||
- Details: Source from the absorbed `jellyseerr_url`/`jellyseerr_api_key` on
|
||
the Jellyfin instance. Render request-management data.
|
||
|
||
- [ ] **5.3 Tests**
|
||
- New tests for MediaTab (instance-scoped), RequestsTab. Delete old Media page
|
||
tests.
|
||
|
||
---
|
||
|
||
## Slice 6 — Frontend: ssh_tasks content tabs (Files + Actions)
|
||
|
||
- [ ] **6.1 FilesTab**
|
||
- Files: `pages/service-tabs/FilesTab.tsx` (lift from `FileBrowser.impl.tsx`)
|
||
- Details: Accept `instance` prop. Delete old `/files` route + page wrapper.
|
||
|
||
- [ ] **6.2 ActionsTab**
|
||
- Files: `pages/service-tabs/ActionsTab.tsx` (lift from `Actions.tsx`)
|
||
- Details: Accept `instance` prop. Delete old `/actions` route + page.
|
||
|
||
- [ ] **6.3 Tests**
|
||
|
||
---
|
||
|
||
## Slice 7 — Frontend: backups Jobs tab
|
||
|
||
- [ ] **7.1 JobsTab**
|
||
- Files: `pages/service-tabs/JobsTab.tsx` (lift from `components/BackupsPage.tsx`)
|
||
- Details: Accept `instance` prop, scope queries by `instance.id`. Delete old
|
||
`/backups` route + page.
|
||
|
||
- [ ] **7.2 Tests**
|
||
|
||
---
|
||
|
||
## Slice 8 — Frontend: Authentik Users + Messaging tabs
|
||
|
||
- [ ] **8.1 UsersTab**
|
||
- Files: `pages/service-tabs/UsersTab.tsx`, `hooks/useAuthentikUsers.ts`,
|
||
`api/authentik.ts`
|
||
- Details: Directory table + search, sourced from the new endpoint. No
|
||
Jellyfin/Jellyseerr enrichment.
|
||
|
||
- [ ] **8.2 MessagingTab**
|
||
- Files: `pages/service-tabs/MessagingTab.tsx` (lift compose UI from
|
||
`UsersPage.impl.tsx`)
|
||
- Details: Recipient list sourced from Authentik users. Reuse the mail queue +
|
||
SMTP settings. Delete the old `/users` route + UsersPage.
|
||
|
||
- [ ] **8.3 Tests**
|
||
|
||
---
|
||
|
||
## Slice 9 — Frontend: Observability split (Alerts + Links + Metrics tabs)
|
||
|
||
- [ ] **9.1 AlertsTab**
|
||
- Files: `pages/service-tabs/AlertsTab.tsx` (lift from `ObservabilityPage.tsx`)
|
||
- Details: Alertmanager alerts view, instance-scoped. Delete old
|
||
`/observability` route + page.
|
||
|
||
- [ ] **9.2 LinksTab + MetricsTab**
|
||
- Files: `pages/service-tabs/LinksTab.tsx`, `pages/service-tabs/MetricsTab.tsx`
|
||
- Details: Grafana deep-links; Prometheus status + PromQL explorer. Each
|
||
instance-scoped.
|
||
|
||
- [ ] **9.3 Tests**
|
||
|
||
---
|
||
|
||
## Slice 10 — Frontend: named dashboards
|
||
|
||
- [ ] **10.1 NamedDashboardPage**
|
||
- Files: `pages/NamedDashboardPage.tsx`, `hooks/useDashboards.ts`,
|
||
`api/dashboards.ts`
|
||
- Details: Render widgets + pinned service links at `/d/:slug`. CRUD via the
|
||
new endpoints.
|
||
|
||
- [ ] **10.2 Pinned service links**
|
||
- Files: `components/PinnedServiceLink.tsx`, integration into the dashboard
|
||
config dialog
|
||
- Details: Shortcut variant targeting `/services/:type/:id` or a specific tab.
|
||
|
||
- [ ] **10.3 Dashboard management UI**
|
||
- Files: a new "Manage dashboards" entry on the Services or Settings page
|
||
- Details: Create/rename/reorder/delete named dashboards.
|
||
|
||
- [ ] **10.4 Tests**
|
||
|
||
---
|
||
|
||
## Slice 11 — Cleanup + docs
|
||
|
||
- [ ] **11.1 Delete dead code**
|
||
- Files: any remaining top-level page wrappers, unused hooks, stale types.
|
||
- Details: Confirm no references to removed routes/pages remain.
|
||
|
||
- [ ] **11.2 Update `docs/REQUIREMENTS.md`**
|
||
- Files: `docs/REQUIREMENTS.md`
|
||
- Details: Rewrite the Information Architecture section. Document the service-
|
||
type → nav-entry map, the service-page tab skeleton, named dashboards,
|
||
routing, and the Users→Authentik + Observability-split decisions.
|
||
|
||
- [ ] **11.3 Update `CHANGELOG.md`**
|
||
|
||
---
|
||
|
||
## Slice 12 — Verify
|
||
|
||
- [ ] **12.1 Cross-route manual pass**
|
||
- Details: Walk every service type's page + tabs; walk named dashboards; walk
|
||
the empty state; confirm legacy routes 404.
|
||
|
||
- [ ] **12.2 Verify report**
|
||
- Files: `openspec/changes/services-as-hub-ia/verify-report.md`
|
||
- Details: Per-AC evidence (AC1–AC10), tool versions, manual notes, residual
|
||
risks.
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
- Slices 1–3 are backend-only; slice 4 is the frontend shell turning on the new
|
||
IA with stubs; 5–9 replace stubs with real content; 10 adds named dashboards;
|
||
11–12 close out.
|
||
- Slices 5–9 are independent and can be reordered or parallelized across
|
||
branches if useful, but each must merge green with its stub replaced.
|
||
- The frontend content lifts (5–9) are the bulk of the line count; treat each as
|
||
a self-contained review-sized PR.
|