a8dfbd5dc6
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).
71 lines
4.8 KiB
Markdown
71 lines
4.8 KiB
Markdown
# 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.
|