Files
manage/.pi-tmp/slice2-worker-out.md
T
Developer a8dfbd5dc6 Cleanup: delete dead top-level pages + update docs (Slice 11)
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).
2026-06-26 20:11:02 +00:00

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, or grafana.
  • 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):

  1. Mobile renders single column with anchor bar: verifies Observability/Media/Backups sections appear, Custom does NOT (empty section hidden), all widgets render.
  2. Desktop hides anchor bar: verifies no section headings or pills at desktop width.
  3. 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

  1. 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 flat visibleWidgets.map(...) inside a flex flex-col gap-4 parent. The task explicitly said "preserve whatever the current code does" and "do NOT change desktop behavior". Adding a grid wrapper (even grid-cols-1) around the desktop path would be a structural change. So the isMobile branch renders MobileWidgetSections (which has its own grid grid-cols-1) on mobile, and the bare map on desktop. Desktop DOM is byte-for-byte identical to before.

  2. 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.

  3. useServiceInstances() added to Dashboard. Required to resolve service-bound widget types for section grouping. TanStack Query dedupes by key, so this shares the cache with WidgetInstanceCard's own useServiceInstances() 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 matchMedia state is per-test, not reactive: the useIsMobile hook reads matchMedia synchronously during useState init, then sets up a listener. The test sets matchMedia before render. If a test needed to simulate a live resize mid-render, the mock's addEventListener is 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 use getAllByText or getByRole("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.