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).
6.7 KiB
Slice 5 — Users + Backups tables mobile layout (worker output)
Files changed
| File | Status | Lines |
|---|---|---|
frontend/src/hooks/useIsMobile.ts |
modified | +6 / -2 |
frontend/src/components/BackupAlertsTable.tsx |
modified | +45 / -0 |
frontend/src/components/BackupJobsTable.tsx |
modified | +48 / -0 |
frontend/src/components/BackupRunsTable.tsx |
modified | +38 / -1 |
frontend/src/components/__tests__/BackupAlertsTable.test.tsx |
modified | +39 / -1 |
frontend/src/components/__tests__/BackupRunsTable.test.tsx |
modified | +24 / -1 |
frontend/src/pages/UsersPage.impl.tsx |
modified | +58 / -4 |
frontend/src/pages/__tests__/UsersPage.test.tsx |
modified | +33 / -0 |
Total: ~317 changed lines (317 insertions, 10 deletions). Under the 400-line budget.
What was implemented
useIsMobile hardening (cross-cutting fix)
The hook now guards typeof window.matchMedia === "function" in both the useState initializer and the useEffect. Previously, jsdom environments without a matchMedia stub (the Backups component tests) would crash. This is a 6-line defensive fix matching the pattern the old UsersPage local hook already used.
5.1 — UsersPage card
Below md, the user table renders as <MobileCardRow> cards:
| Field | Key | Rationale |
|---|---|---|
| Display name (primary) | name |
userLabel(row) — the primary identifier |
| Username | username |
Falls back to jellyfin_id when username equals display_name |
| Activity | activity |
<Badge variant={activityBadgeVariant(...)}> — visual at-a-glance status |
email |
Falls back to "—" when absent |
Selection wiring: The checkbox renders in the actions slot of each card. onClick={(e) => e.stopPropagation()} prevents the card body tap (which opens the drawer via onRowClick) from also toggling selection. The checkbox uses the existing toggleUserSelected(row.jellyfin_id) handler and the selectedIdSet state — selection round-trips correctly. The checkbox has className="mobile-touch-target" for 44px min hit area.
Drawer open: onRowClick={(r) => setSearchParams({ user: r.jellyfin_id })} — same handler as the desktop table row click.
Compose dialog: The local useIsMobile (900px) was renamed to useComposeViewport to avoid collision with the shared 768px hook. The compose dialog still uses isComposeMobile (900px) for its fullScreen behavior. Compose is otherwise untouched (slice 8 scope).
5.2 — Backups cards (3 components)
BackupAlertsTable — primary = alert.message; fields = severity (Badge), type, created. Acknowledge button in actions slot (shortened to "Ack" for mobile space).
BackupJobsTable — primary = job.name; fields = source, schedule interval, last status (Badge). Uses an intermediate JobCardRow type to compose job + latest run status into a single row object for the card.
BackupRunsTable — primary = run.job_id; fields = status (Badge), duration, size, started. The status filter <Select> renders ABOVE both the card and table layouts (unchanged).
5.3 — Tests (4 new tests, 102 total)
- BackupAlertsTable: 2 new (mobile card render, acknowledge action on card)
- BackupRunsTable: 1 new (mobile card render with job_id primary)
- UsersPage: 1 new (mobile cards with display name + activity labels)
Validation
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
npm run build → ✓ built (tsc -b + vite)
npm run test → 25 files / 102 tests passed (was 98; +4 new)
Mobile field lists (per component)
| Component | Primary | Fields | Rationale |
|---|---|---|---|
| UsersPage | userLabel(row) |
username, activity (Badge), email | Identity + at-a-glance status + contact info |
| BackupAlertsTable | alert.message |
severity (Badge), alert_type, created_at | Descriptive text first; severity/type/date for triage |
| BackupJobsTable | job.name |
source, schedule, last status (Badge) | Job identity + config + health |
| BackupRunsTable | run.job_id |
status (Badge), duration, size, started | Run identity + outcome + timing |
Selection wiring on UsersPage cards
The checkbox is rendered in the MobileCardRow actions slot (right-aligned). onClick={(e) => e.stopPropagation()} prevents the card's onRowClick (drawer open) from firing when the checkbox is tapped. The checkbox calls toggleUserSelected(r.jellyfin_id), which is the same handler used by the desktop table. The selectedIdSet (derived from selectedUserIds state) drives checked and updates reactively. Multi-select works correctly on mobile.
Deviations from design
-
useIsMobilehardening. The shared hook from Slice 1 crashed in jsdom test environments that don't stubmatchMedia(the Backups component tests). Added atypeof window.matchMedia === "function"guard to both theuseStateinitializer and theuseEffect. This matches the defensive pattern the old UsersPage local hook already used and prevents ALL consumers from needing a matchMedia stub for desktop behavior. -
JobCardRowintermediate type in BackupJobsTable. The card needs bothBackupJoband its latest run status. Rather than passing a tuple or doing lookups inside the render function, I compose a smallJobCardRowinterface ({ job, status, run_started }) and map jobs to it before passing toMobileCardRow. -
Compose hook rename. Renamed the file-local
useIsMobile(900px) touseComposeViewportto avoid collision with the imported shareduseIsMobile(768px). No behavior change.
skill_resolution
none — no project/user SKILL.md paths were injected, and no .atl/skill-registry.md was found.
Residual risks
- Nested checkbox inside button on UsersPage cards. When
onRowClickis set,MobileCardRowrenders the card as a<button>. The checkbox (a Radix Checkbox, which renders a<button>) is inside it via theactionsslot. This is technically invalid HTML (interactive content nested in button), but browsers handle it correctly:stopPropagationon the checkbox'sonClickprevents the card's click handler. The existing desktop table uses the same pattern (onClick={(event) => event.stopPropagation()}on the checkbox inside a clickableTableRow). Acceptable. - No
BackupJobsTable.test.tsxmobile test. There is no existingBackupJobsTable.test.tsxfile in the test directory, so I didn't create one (out of scope to add a new test file for a component that previously had no dedicated test). The component is exercised via integration inBackupsPage.test.tsx. Low risk.
Review findings
No blockers identified during self-review. All validation commands green. No staged files.