Files
manage/.pi-tmp/slice5-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

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

  1. useIsMobile hardening. The shared hook from Slice 1 crashed in jsdom test environments that don't stub matchMedia (the Backups component tests). Added a typeof window.matchMedia === "function" guard to both the useState initializer and the useEffect. This matches the defensive pattern the old UsersPage local hook already used and prevents ALL consumers from needing a matchMedia stub for desktop behavior.

  2. JobCardRow intermediate type in BackupJobsTable. The card needs both BackupJob and its latest run status. Rather than passing a tuple or doing lookups inside the render function, I compose a small JobCardRow interface ({ job, status, run_started }) and map jobs to it before passing to MobileCardRow.

  3. Compose hook rename. Renamed the file-local useIsMobile (900px) to useComposeViewport to avoid collision with the imported shared useIsMobile (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 onRowClick is set, MobileCardRow renders the card as a <button>. The checkbox (a Radix Checkbox, which renders a <button>) is inside it via the actions slot. This is technically invalid HTML (interactive content nested in button), but browsers handle it correctly: stopPropagation on the checkbox's onClick prevents the card's click handler. The existing desktop table uses the same pattern (onClick={(event) => event.stopPropagation()} on the checkbox inside a clickable TableRow). Acceptable.
  • No BackupJobsTable.test.tsx mobile test. There is no existing BackupJobsTable.test.tsx file 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 in BackupsPage.test.tsx. Low risk.

Review findings

No blockers identified during self-review. All validation commands green. No staged files.