# 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 `` 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` | `` — 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 `