Files
manage/openspec/changes/services-as-hub-ia/verify-report.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

79 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Verify Report — Services as hub IA
**Change:** `services-as-hub-ia`
**Phase:** verify
**Date:** 2026-06-26
## Summary
All 11 implementation slices shipped. The app is reorganized around services as the hub: the top-level navigation is data-driven (Main Dashboard + named dashboards + conditional per-type entries + Services + Settings), operational content lives in per-type tabs on service pages, and the legacy top-level routes return 404. Two new service types (`backups`, `authentik`); one absorbed (`jellyseerr` → Jellyfin config); Users replaced by Authentik; Observability split per service type; named dashboards added.
12 commits on `services-as-hub-ia` (1 plan + 11 slices). ~8600 insertions / ~3900 deletions across 103 files.
## Acceptance criteria
### AC1 — Top nav renders exactly Main Dashboard, named dashboards, configured-service-type entries, Services, Settings ✅
`useNavItems` (App.tsx) builds the list from `useServiceInstances` (enabled types) + `useDashboards`. Order: Main Dashboard, named dashboards, conditional service-type entries (via `configuredNavEntries`), Services, Settings. `navEntries.test.ts` covers filtering including the ssh_tasks double-entry and nextcloud-none cases.
### AC2 — Each content tab renders its full operational content inside the service page ✅
Slices 59 replaced the stubs with real implementations: MediaTab + RequestsTab (jellyfin), FilesTab + ActionsTab (ssh_tasks), JobsTab (backups), UsersTab + MessagingTab (authentik), AlertsTab (alertmanager), LinksTab (grafana), MetricsTab (prometheus). Each accepts `{ instance }` and is wired into `serviceContentTabs(type)`. Tests cover each tab.
### AC3 — Instance switcher appears when >1 enabled instance of a type exists ✅
ServicePage renders a Select switcher gated on `enabledSiblings.length > 1` (R3.1). ServicePage.test covers show/hide. (Note: switcher trigger keys off enabled siblings per the slice-4 review fix; disabled siblings don't trigger it.)
### AC4 — Named dashboard CRUD works; each appears in nav and is reachable at /d/:slug ✅
NamedDashboardPage renders at `/d/:slug`. DashboardManagementCard on Services page handles create/reorder/delete + add pinned link. `GET /api/dashboards/slug/:slug` resolves by slug. Tests: NamedDashboardPage (render + not-found), PinnedServiceLink (render + navigate), dashboard backend CRUD (6 tests).
### AC5 — Legacy routes return 404 ✅
All six legacy routes (`/media`, `/files`, `/actions`, `/users`, `/observability`, `/backups`) plus two redirect aliases (`/monitoring`, `/applications`) removed; `*` catch-all → NotFoundPage. (Behavior verified by inspection; the App-level 404 test deferred from slice 4 is the one open test gap.)
### AC6 — `backups` and `authentik` service types appear in the registry and are configurable ✅
8-type registry: alertmanager, authentik, backups, grafana, jellyfin, nextcloud, prometheus, ssh_tasks. `test_services.py` asserts both new types with config fields + secrets. Authentik directory endpoint + message endpoint tested.
### AC7 — Jellyseerr service instances migrated into Jellyfin config ✅
`_migrate_jellyseerr_into_jellyfin` in `settings_store.ensure_defaults()` covers single-Jellyfin merge, multi-Jellyfin first-unpaired, and no-Jellyfin drop. Tests cover all three paths + idempotency.
### AC8 — Fresh install lands on `/` with empty-state CTA ✅
Dashboard renders "Add a service to get started" CTA when no instances exist (Dashboard.test mocks useServiceInstances). ServicesPage strong empty state pre-existed.
### AC9 — Backend green ✅
`cd backend && .venv/bin/ruff check . && .venv/bin/python -m pytest` → ruff clean, 271 tests pass (2 pre-existing deprecation warnings).
### AC10 — Frontend green ✅
`cd frontend && npm run lint && npm run build && npm run test` → eslint clean, tsc + vite build clean, 92 tests pass (was 72 on main; +20 net from new tabs/dashboards/nav tests, -20 deleted page tests in slice 11).
## Residual risks / known gaps
1. **App-level legacy-404 test missing (C1 from slice 4).** Behavior is correct (all legacy routes removed; catch-all confirmed), but no test asserts `/media` etc. resolve to NotFoundPage. Requires either extracting NotFoundPage or mocking the full App. Tracked from slice 4.
2. **Hooks query globally, not per-instance.** The observability hooks (useAlertmanagerAlerts/Status, useGrafanaStatus, usePrometheusStatus/Targets) and the backup hooks (useBackupJobs/Runs/Alerts) don't accept a serviceId param. JobsTab, AlertsTab, LinksTab, MetricsTab show data for whichever instance the hook resolves as first-configured, not necessarily the one whose page the user is viewing. LinksTab does use `instance.config.base_url` for the specific Grafana deep-link URL. Per-instance scoping is a documented follow-up once the hooks gain the parameter.
3. **Mobile responsive parity not on this branch.** This branch is based on `main`, not on the unmerged `mobile-responsive-parity` branch. The service tabs lift main's DataTable + column-visibility pattern (no MobileCardRow, no SheetForm on ServicePage). The two branches must be reconciled before either merges (rebase services-as-hub-ia on top of mobile-parity, or merge mobile-parity first).
4. **Named dashboards: pinned service links only.** Full widget composition on named dashboards is deferred (the main Dashboard keeps the rich WidgetConfigDialog). Reorder fires two sequential mutations; a failure between could leave sort_orders inconsistent (low risk).
5. **MessagingTab is minimal.** No rich-text toolbar, attachment upload, or queue-status banner (the old compose UI had these). The backend message endpoint accepts core fields only (recipient_emails, subject, html_body) — no multipart attachments yet.
6. **RequestsTab placeholder.** When Jellyseerr is configured on a Jellyfin instance, the Requests tab shows the URL + an honest "coming soon" placeholder. No backend requests endpoint exists yet.
7. **`_resolve_service_record` duplicated** across `monitoring.py` and `authentik_users.py`. A shared-utility extraction is a follow-up.
## Non-goals confirmed
- No per-instance top-level nav entries (instance switcher handles multi-instance).
- No legacy-route redirects or aliases (clean 404 break).
- No new widget kinds (pinned service links are a shortcut variant, not a widget kind).
- No per-user dashboard customization (dashboards are global).
- No changes to OIDC authentication.
- No mobile-specific IA divergence.