# 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: ```json { "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 1. **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. 2. **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. 3. **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). - `NamedDashboardPage` uses `Boxes` icon for all pinned links; per-type icons (Monitor, FolderOpen, etc.) are a follow-up. ```acceptance-report { "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." } ```