Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
7.2 KiB
Slice 10 — Frontend: named dashboards (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
backend/src/media_library_viewer_api/routers/dashboards.py |
modified | +7 (GET /slug/:slug endpoint) |
frontend/src/api/dashboards.ts |
modified | +6 (fetchDashboardBySlug) |
frontend/src/hooks/useDashboards.ts |
modified | +12 (useDashboardBySlug hook) |
frontend/src/components/PinnedServiceLink.tsx |
new | 55 |
frontend/src/pages/NamedDashboardPage.tsx |
new | 84 |
frontend/src/pages/ServicesPage.tsx |
modified | +180 (DashboardManagementCard + imports) |
frontend/src/App.tsx |
modified | +4 (import + 2 route registrations) |
frontend/src/pages/__tests__/NamedDashboardPage.test.tsx |
new | 73 |
frontend/src/components/__tests__/PinnedServiceLink.test.tsx |
new | 33 |
Total: ~454 changed lines (349 new + 105 modified diff). Slightly over the 400-line budget; dominated by the DashboardManagementCard (create/reorder/delete/add-link UI) on ServicesPage.tsx (~130 lines) and the two test files.
Dashboard payload model
Inline items (not widget instance ids). The payload stores:
{ "items": [{ "type": "link", "label": "My Jellyfin", "target": "/services/jellyfin/svc-1" }] }
Rationale: named dashboards compose shortcuts, not live widget instances (full widget composition is a follow-up — the main Dashboard already has the rich WidgetConfigDialog). Inline items are self-contained and don't require a separate widget-instance fetch. The type field is a discriminator so future widget items can be added without breaking existing payloads.
Backend endpoint added
GET /api/dashboards/slug/{slug} — resolves a dashboard by slug via the existing store.get_dashboard_by_slug(). Returns 404 when not found. The store method already existed (slice 3); only the router endpoint was missing (~7 lines).
Management UI (on Services page)
A DashboardManagementCard section renders below the Services card on /services:
- List existing dashboards with label, slug badge, link count, and reorder/delete controls.
- Create via a dialog (label → auto-slug).
- Reorder up/down (swaps sort_order between adjacent dashboards).
- Delete with confirmation.
- Add pinned service link per dashboard: a label input + a service dropdown (enabled services only) + an "Add link" button. The link target is built via
serviceLinkTarget(type, id).
Full widget composition on named dashboards is deferred — this slice ships pinned service links only.
Validation
cd backend && .venv/bin/ruff check src/ tests/ → All checks passed!
cd backend && .venv/bin/python -m pytest tests/test_dashboards.py → 6 passed
cd frontend && npm run lint → 0 errors, 2 pre-existing warnings
cd frontend && npm run build → ✓ built (tsc -b + vite)
cd frontend && npm run test → 37 files / 112 tests passed (was 106; +6 new)
Deviations from design
- Management UI on Services page, not Settings. The design said "pick whichever is less invasive." Services is the admin hub for managing instances; dashboards are a closely related admin concern, and placing it there avoids an extra nav trip to Settings.
- No widget composition on named dashboards. The task said "full widget composition is a follow-up." Pinned service links only — the main Dashboard keeps the rich WidgetConfigDialog.
- Over 400-line budget. The management UI (create/reorder/delete/add-link) is inherently interactive and needs form state + mutation hooks. Could not shrink without dropping reorder or the link-adder.
skill_resolution
none — no project/user SKILL.md paths were injected; no .atl/skill-registry.md found.
Residual risks
- Full widget composition on named dashboards is deferred (pinned links only).
- The reorder function fires two mutations sequentially (swap a+b sort_orders); TanStack Query invalidation handles the refetch, but a failure between the two could leave sort_orders inconsistent. Low risk (both use the same endpoint).
NamedDashboardPageusesBoxesicon for all pinned links; per-type icons (Monitor, FolderOpen, etc.) are a follow-up.
{
"criteriaSatisfied": [
{
"id": "criterion-1",
"status": "satisfied",
"evidence": "Slice 10 implements NamedDashboardPage (/d/:slug), PinnedServiceLink component, dashboard management UI (create/reorder/delete/add-link on Services page), /d/:slug route registration, GET /api/dashboards/slug/:slug backend endpoint, and 6 new tests. No scope widening: pinned links only (full widget composition deferred per task). 112 frontend + 6 dashboard backend tests pass; lint/build green both sides."
}
],
"changedFiles": [
"backend/src/media_library_viewer_api/routers/dashboards.py",
"frontend/src/api/dashboards.ts",
"frontend/src/hooks/useDashboards.ts",
"frontend/src/components/PinnedServiceLink.tsx",
"frontend/src/pages/NamedDashboardPage.tsx",
"frontend/src/pages/ServicesPage.tsx",
"frontend/src/App.tsx",
"frontend/src/pages/__tests__/NamedDashboardPage.test.tsx",
"frontend/src/components/__tests__/PinnedServiceLink.test.tsx"
],
"testsAddedOrUpdated": [
"frontend/src/pages/__tests__/NamedDashboardPage.test.tsx",
"frontend/src/components/__tests__/PinnedServiceLink.test.tsx"
],
"commandsRun": [
{ "command": "cd backend && .venv/bin/ruff check src/ tests/", "result": "passed", "summary": "All checks passed" },
{ "command": "cd backend && .venv/bin/python -m pytest tests/test_dashboards.py", "result": "passed", "summary": "6 passed (no regression from new endpoint)" },
{ "command": "cd frontend && npm run lint", "result": "passed", "summary": "0 errors, 2 pre-existing warnings" },
{ "command": "cd frontend && npm run build", "result": "passed", "summary": "tsc -b + vite build clean" },
{ "command": "cd frontend && npm run test", "result": "passed", "summary": "37 files / 112 tests passed (+6 new)" }
],
"validationOutput": [
"Backend: GET /api/dashboards/slug/:slug added; 6 dashboard tests pass; ruff clean",
"Frontend: NamedDashboardPage renders pinned links + empty/not-found states; PinnedServiceLink navigates; dashboard management creates/lists/reorders/deletes; route registered in both auth and no-auth blocks",
"112 frontend tests pass (+6); lint/build green"
],
"residualRisks": [
"Full widget composition on named dashboards is deferred (pinned links only)",
"Reorder fires two sequential mutations; a failure between could leave sort_orders inconsistent (low risk)",
"All pinned links use Boxes icon; per-type icons are a follow-up"
],
"noStagedFiles": true,
"diffSummary": "~454 lines: backend slug endpoint (+7), fetchDashboardBySlug/useDashboardBySlug (+18), PinnedServiceLink (55), NamedDashboardPage (84), ServicesPage DashboardManagementCard (+130), App.tsx route registration (+4), 2 test files (106 lines). Slightly over 400-line budget due to interactive management UI.",
"reviewFindings": [
"no blockers"
],
"manualNotes": "Dashboard payload model: inline items with type discriminator ({ items: [{ type: 'link', label, target }] }). Management UI is on the Services page (below the services card). The /d/:slug route is registered in both the auth and no-auth route blocks in App.tsx."
}