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).
4.8 KiB
Slice 2 — Dashboard mobile layout (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
frontend/src/pages/Dashboard.tsx |
modified | +128 / -7 |
frontend/src/pages/__tests__/Dashboard.test.tsx |
modified | +178 / -2 |
Total: ~306 changed lines (299 insertions, 7 deletions). Under the 400-line budget.
What was implemented
2.1 — Single-column grid below md
Below md (isMobile === true), widgets render inside MobileWidgetSections which uses grid grid-cols-1 gap-4. At md and above, the existing flat visibleWidgets.map(...) renders unchanged — no grid wrapper is introduced on desktop (preserving the exact previous behavior).
2.2 — Section anchor bar
Below md, a horizontally scrollable pill bar (MobileWidgetSections anchor bar) groups widgets by section. Clicking a pill calls document.getElementById(...).scrollIntoView({ behavior: "smooth", block: "start" }). Each section renders with scroll-mt-16 so the sticky TopBar doesn't cover the heading.
Section-to-widget mapping:
- Observability (Activity icon): service-bound widgets whose service_type is
alertmanager,prometheus, orgrafana. - Media (Monitor icon): service-bound widgets whose service_type is
jellyfin. - Backups (DatabaseBackup icon): built-in widgets with
widget_kind === "backups". - Custom (LayoutDashboard icon): built-in
static,ssh_tasks,nextcloud, and any unmatched widget.
Section order: Observability → Media → Backups → Custom. Empty sections are not rendered.
Icons match the existing nav (App.tsx navItems): Activity for Observability, Monitor for Media, DatabaseBackup for Backups.
2.3 — Tests
Extended Dashboard.test.tsx with 3 new tests (6 total, all passing):
- Mobile renders single column with anchor bar: verifies Observability/Media/Backups sections appear, Custom does NOT (empty section hidden), all widgets render.
- Desktop hides anchor bar: verifies no section headings or pills at desktop width.
- Anchor pill jumps via scrollIntoView: spies on
Element.prototype.scrollIntoView, clicks the Media pill, asserts the spy was called.
matchMedia mock: Added setMatchMedia(matches: boolean) helper that stubs window.matchMedia for the (max-width: 768px) query. Called in beforeEach with false (desktop default). Each mobile test calls setMatchMedia(true).
Validation
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
npm run build → ✓ built in 855ms (tsc -b + vite)
npm run test → 25 files / 89 tests passed (was 86; +3 new)
Deviations from design
-
Desktop path preserved as bare map (no grid wrapper). The design pseudocode said
grid grid-cols-1 md:grid-cols-*. The actual existing desktop code has no grid — it's a flatvisibleWidgets.map(...)inside aflex flex-col gap-4parent. The task explicitly said "preserve whatever the current code does" and "do NOT change desktop behavior". Adding a grid wrapper (evengrid-cols-1) around the desktop path would be a structural change. So theisMobilebranch rendersMobileWidgetSections(which has its owngrid grid-cols-1) on mobile, and the bare map on desktop. Desktop DOM is byte-for-byte identical to before. -
Section headings (
<h3>) on mobile. The design/spec did not explicitly name section headings, only the anchor bar. I added a subtle<h3 className="text-sm font-semibold text-muted-foreground">per section so the sections are visually identifiable after scrolling. This is additive mobile-only markup; desktop is unaffected. -
useServiceInstances()added to Dashboard. Required to resolve service-bound widget types for section grouping. TanStack Query dedupes by key, so this shares the cache withWidgetInstanceCard's ownuseServiceInstances()call — no extra network request.
skill_resolution
none — no project/user SKILL.md paths were injected by the parent, and no .atl/skill-registry.md was found. The task was self-contained against the OpenSpec design/tasks docs.
Residual risks
- jsdom
matchMediastate is per-test, not reactive: theuseIsMobilehook readsmatchMediasynchronously duringuseStateinit, then sets up a listener. The test setsmatchMediabefore render. If a test needed to simulate a live resize mid-render, the mock'saddEventListeneris a no-op (no event fires). This is adequate for breakpoint-branch tests but cannot test responsive transitions. Acceptable for this slice. - Anchor pill duplicate text: each section label appears in both the pill and the
<h3>. Tests usegetAllByTextorgetByRole("button", { name })to disambiguate. This is a minor testing concern, not a runtime issue.
Review findings
No blockers identified during self-review. All validation commands green.