# Slice 5 Review — Users + Backups mobile card layout **Change:** `mobile-responsive-parity` · **Slice:** 5 (Users + Backups tables) **Reviewer mode:** fresh adversarial · **Date:** 2026-06-26 **Verdict: fix-then-commit** (one real HTML-validity issue; everything else clean) --- ## Commands run (all green) | Command | Result | Notes | |---|---|---| | `npm run lint` | ✅ 0 errors | Only 2 pre-existing `react-hooks/exhaustive-deps` warnings in `UsersPage.impl.tsx` (lines 149/188). Verified identical on `HEAD` (lines 120/159) — **not introduced by this slice**. | | `npm run build` | ✅ built | `tsc -b` typecheck clean (build runs it). | | `npm run test` | ✅ 25 files / 102 tests | All pass. | | `git diff --cached --stat` | empty | No staged files. | --- ## 1. `useIsMobile` hardening — SAFE ✅ `frontend/src/hooks/useIsMobile.ts`: added `typeof window.matchMedia === "function"` to both the lazy `useState` initializer and the `useEffect` guard. - In real browsers `window.matchMedia` is always a function, so the added predicate is always `true` and **behavior is identical**. - In jsdom (no native `matchMedia`) the change converts a hard `TypeError` ("window.matchMedia is not a function") into a graceful `false` (desktop). This is strictly safer — it can only turn a crash into a non-crash. - All existing consumers (`App.tsx`, `Dashboard`, `Media`, `FileBrowser`) already stub `matchMedia` in their test files, so their tests are unaffected. Confirmed no regression path for slices 1–4. **Conclusion:** safe across all consumers; no regression. --- ## 2. UsersPage desktop Table — preserved EXACTLY ✅ I programmatically extracted the `…
` block from `HEAD` and from the working tree and diffed them token-for-token. - The only differences are **line re-wrapping** caused by deeper indentation (e.g. the `checked || selectedUser?.jellyfin_id === row.jellyfin_id` expression and `onClick={() => setSearchParams({ user: row.jellyfin_id })}` wrap onto more lines). Every token, attribute, and child is present in both. - **Columns preserved (10):** select-all checkbox header, User, Email, Activity, Type (hidden md), Jellyseerr, Role (hidden md), Permissions, Reqs (hidden md), Contact (hidden md). - **Header checkbox** `toggleVisibleSelection` — preserved. - **Row checkbox** `toggleUserSelected` + `onClick={(event) => event.stopPropagation()}` + `aria-label` — preserved. - **Row click** `onClick={() => setSearchParams({ user: row.jellyfin_id })}` — preserved. - **Avatar** (`AvatarImage`/`AvatarFallback`), username fallback, all `` variants, `data-state="selected"`, `cursor-pointer` — all preserved. **Conclusion:** the desktop branch is the original table re-indented one level deeper into the `: (` else arm. No prop, column, or handler was dropped. Diff stat (207 ins / 149 del) is dominated by this re-indentation; the true behavioral delta is small (cards branch + `userCardFields` + `useComposeViewport` rename). --- ## 3. Compose hook rename — correct ✅ The file-local 900px `useIsMobile` was renamed `useComposeViewport`; the shared 768px `useIsMobile` (from `hooks/`) now drives the directory-table branch. - `isComposeMobile` (900px) → used **only** at `UsersPage.impl.tsx:827` for the compose `DialogContent` full-screen class. Breakpoint unchanged (`(max-width: 900px)`). - `isMobile` (768px) → used **only** at `UsersPage.impl.tsx:511` for the table/card branch. - Verified no stray references to the old local name remain (`grep` confirms 2 distinct symbols, correctly wired). --- ## 4. Backups cards (3 components) ✅ - **BackupAlertsTable:** mobile branch renders `MobileCardRow` with primary=message + severity/type/created; **Ack action preserved** in the `actions` slot (`mobile-touch-target`, calls `onAcknowledge(a.id)`; hidden when `acknowledged`). Desktop `` block is byte-identical (diff is purely additive before the `return`). - **BackupJobsTable:** mobile branch builds `JobCardRow[]` (joins `latestRuns` exactly as the desktop row does) with primary=name + source/schedule/last-status. No per-row action exists in the desktop original, so none is "lost". Desktop table unchanged. - **BackupRunsTable:** status-filter `
` is re-indented into the `: (` else arm but content is identical (same 5 columns, same formatters, same `statusVariant`). Spec R3.2 (primary + 3–5 fields) satisfied for all three. Spec R3.6 (desktop unchanged) satisfied. --- ## 5. UsersPage mobile selection — INVALID HTML NESTING (confirmed issue) `MobileCardRow` renders the card as a ` ``` This is **invalid HTML** (`` with `onClick`. A `` is not a ``). Fix in `mobile-card.tsx` (render clickable card as `div role="button"` or lift `actions` out of the button). - **Suggestions (non-blocking):** - Add a UsersPage mobile test asserting `toggleUserSelected` round-trip + checkbox `stopPropagation`. - Add a `BackupJobsTable` mobile test (currently zero coverage on a touched file). - Refresh the stale "MUI useMediaQuery" comment in `UsersPage.test.tsx`. - **Verified clean:** `useIsMobile` hardening (no regression), desktop UsersPage Table preserved exactly (token-identical), compose 900px breakpoint preserved, all 3 Backups desktop tables byte-identical, status filter + Ack action preserved on mobile, lint/build/test green, no staged files. **Verdict: fix-then-commit** — resolve the single button-in-button HTML validity issue (localized to `mobile-card.tsx`), then this slice is good to commit. The two test-coverage suggestions can land in the same commit or a follow-up. ```acceptance-report { "criteriaSatisfied": [ { "id": "criterion-1", "status": "satisfied", "evidence": "Slice 5 implements Users + 3 Backups mobile card layouts per spec R3.1/R3.2/R3.3 and tasks 5.1/5.2/5.3 without widening scope (no backend, no new product behavior, no sm: breakpoint). Desktop layouts preserved exactly; only presentation-layer parity added." } ], "changedFiles": [ "frontend/src/hooks/useIsMobile.ts", "frontend/src/components/BackupAlertsTable.tsx", "frontend/src/components/BackupJobsTable.tsx", "frontend/src/components/BackupRunsTable.tsx", "frontend/src/components/__tests__/BackupAlertsTable.test.tsx", "frontend/src/components/__tests__/BackupRunsTable.test.tsx", "frontend/src/pages/UsersPage.impl.tsx", "frontend/src/pages/__tests__/UsersPage.test.tsx" ], "testsAddedOrUpdated": [ "frontend/src/components/__tests__/BackupAlertsTable.test.tsx", "frontend/src/components/__tests__/BackupRunsTable.test.tsx", "frontend/src/pages/__tests__/UsersPage.test.tsx" ], "commandsRun": [ { "command": "cd frontend && npm run lint", "result": "passed", "summary": "0 errors; 2 pre-existing react-hooks/exhaustive-deps warnings (verified identical on HEAD, not introduced by slice 5)." }, { "command": "cd frontend && npm run build", "result": "passed", "summary": "vite build + tsc -b typecheck clean; 1976 modules transformed." }, { "command": "cd frontend && npm run test", "result": "passed", "summary": "25 test files / 102 tests passed (vitest)." }, { "command": "git diff --cached --stat", "result": "passed", "summary": "Empty — no staged files." } ], "validationOutput": [ "useIsMobile guard safe: only changes jsdom (crash->false); real browsers unchanged; no regression to App/Dashboard/Media/FileBrowser consumers.", "UsersPage desktop
block token-diffed against HEAD: identical except line re-wrapping from deeper indentation; all 10 columns, header/row checkboxes, toggleVisibleSelection/toggleUserSelected, setSearchParams row click, Avatar, all Badge variants preserved.", "Compose 900px breakpoint preserved via useComposeViewport rename; isMobile (768px) used only for table branch, isComposeMobile (900px) only for compose dialog.", "BackupRunsTable status-filter Select rendered outside isMobile ternary -> preserved on mobile+desktop; BackupAlertsTable Ack action preserved in card actions slot; BackupJobsTable has no per-row actions to lose.", "Desktop Backups tables byte-identical (Alerts/Jobs additive only; Runs re-indented into else arm, content equal)." ], "residualRisks": [ "BLOCKER: MobileCardRow renders a , not a button). Fix in frontend/src/components/ui/mobile-card.tsx.", "BackupJobsTable.tsx is a touched file with no test file -> zero direct coverage; AC8 not fully met for this component.", "UsersPage mobile test does not assert toggleUserSelected round-trip nor checkbox stopPropagation (behaviors are implemented but untested).", "Stale comment in UsersPage.test.tsx references 'MUI useMediaQuery' after the useComposeViewport rename (cosmetic)." ], "noStagedFiles": true, "diffSummary": "Slice 5 adds mobile MobileCardRow branches to UsersPage + 3 Backups tables, renames the local 900px compose hook to useComposeViewport to avoid collision with the shared 768px useIsMobile, and hardens useIsMobile with a typeof matchMedia guard. UsersPage diff is ~356 lines but mostly re-indentation of the preserved desktop Table into an isMobile ternary else branch; true behavioral delta is small.", "reviewFindings": [ "blocker: frontend/src/components/ui/mobile-card.tsx:82 + frontend/src/pages/UsersPage.impl.tsx:511-530 —