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

7.6 KiB
Raw Blame History

Slice 3 Review — Media table mobile layout (mobile-responsive-parity)

Reviewer: fresh adversarial pass. Scope: unstaged diff on frontend/src/pages/Media.tsx and frontend/src/pages/__tests__/Media.test.tsx.

Verification commands run

Command Result
npm run lint pass (0 errors; 2 pre-existing warnings in UsersPage.impl.tsx, unrelated to Slice 3)
npm run build pass (tsc -b + vite, built in 854ms)
npm run test pass (25 files, 94 tests)

Point-by-point

1. Desktop non-regression — CONFIRMED CORRECT

The diff is a clean branch-add, not a rewrite. The DataTable block was moved into the else of isMobile ? <mobile> : <DataTable> with every prop byte-for- byte identical to the pre-change version (Media.tsx:671-697): columns, data, getRowId, enableRowSelection, rowSelection, onRowSelectionChange, onRowClick, enableColumnVisibilityToggle, columnVisibility, onColumnVisibilityChange, enablePagination, manualPagination, pagination, onPaginationChange, pageSizeOptions, rowCount, emptyMessage. The wrapping <div className="rounded-lg border bg-card"> and the status?.exists gate are preserved on both branches. No desktop prop was dropped, renamed, or reordered. R3.6 / R10.1 satisfied.

2. Mobile card fields — CONFIRMED CORRECT

mediaCardFields (Media.tsx:83-96) matches the real MediaItem type (types/index.ts:274), not the design doc's illustrative field names:

  • title (string) — primary ✓
  • sizer.size || "-" (string, null-safe) ✓
  • hdrr.hdr || "-" (string, null-safe) ✓
  • libraryr.library || "-" (string, null-safe) ✓
  • year (number | null) → r.year != null ? String(r.year) : "-" ✓ explicitly null-safe

5 fields total (1 primary + 4), inside the spec's 35 range (R3.2). No undefined access possible — every field guards against empty/null. The design example used size_display/is_hdr/library_name (illustrative); the worker correctly used the real keys. Good.

3. Pagination duplication — NOT A BUG; acceptable tech debt

MediaMobilePagination (Media.tsx:107-188) duplicates DataTablePagination (data-table.tsx). I verified the semantics match exactly:

Concern DataTable MediaMobilePagination Match
Rows count rowCount ?? 0 (manual) totalRows = total (queryResult?.total ?? 0)
pageCount Math.max(1, Math.ceil(rowCount/pageSize)) totalPages = Math.max(1, Math.ceil(total/pageSize)) (Media.tsx:403)
Page-size change table.setPageSize() → resets pageIndex:0 onPaginationChange(() => ({pageIndex:0, pageSize:Number(value)}))
Prev disabled !getCanPreviousPage() = pageIndex>0 inverted pageIndex <= 0
Next disabled !getCanNextPage() = pageIndex>=pageCount-1 inverted pageIndex >= pageCount - 1
Page indicator Page {pageIndex+1} of {pageCount} same

No off-by-one, no missing clamp, no stale state. The mobile component reads pageIndex/pageSize derived the same way as the controlled pagination state fed to DataTable (Media.tsx:355-356), so the two paths can't drift on values.

Could they reuse DataTable's pagination by extracting it? That would require editing the shared data-table.tsx (export DataTablePagination or split a TablePagination), which is explicitly out of scope for Slice 3 and would risk R3.6/R10.1 (the shared component powers the desktop path). Acceptable to defer to a follow-up refactor slice. Non-blocking smell, not a must-fix.

4. Row click navigation — CONFIRMED CORRECT

handleRowClick (Media.tsx:398-400) is passed unchanged to MobileCardRow.onRowClick (Media.tsx:659). MobileCardRow makes the whole card a <button type="button"> with onClick={() => onRowClick(row)} (mobile-card.tsx), so a tap navigates to /files?path=<encoded>. The test "navigates to the file browser when a card is tapped on mobile" asserts navigate is called once with the encoded path. ✓

5. Column-visibility toggle hidden below md (R3.5) — CONFIRMED CORRECT

On the mobile branch only MobileCardRow renders; no DataTable, so the Columns DropdownMenu never mounts. Tested explicitly: hides the column-visibility toggle below md asserts queryByRole("button", { name: /Columns/ }) is null. The desktop test asserts the same button is present at desktop width. ✓ R3.5 satisfied both ways.

6. Test quality — GOOD

  • matchMedia mock (Media.test.tsx:159-183): correct. It discriminates on query.includes("768") so useIsMobile (768px) toggles with the flag while usePrefersSmallScreen (900px) stays false — which is the right default for the desktop path (no MOBILE_HIDDEN_COLUMNS forcing). Adds/removes listeners are no-ops; sufficient for jsdom. Applied in beforeEach defaulting to desktop, overridden per-test via setMatchMedia(true).
  • Desktop test asserts BOTH a DataTable column header (Title) AND the Columns toggle button. ✓
  • The 5 new tests assert real behavior: card titles + field labels render, no column headers leak, pagination renders (2 rows, Page 1 of 1, Previous disabled), card tap navigates, desktop renders DataTable. None are tautological.

7. enableRowSelection on mobile — NOT A REGRESSION (minor spec note)

The mobile card does not render a selection checkbox; MobileCardRow has no selection affordance. However, rowSelection/setRowSelection in Media.tsx is vestigial: grepping the file, the state is declared (Media.tsx:326) and passed to DataTable, but nothing in Media.tsx consumes it — there is no batch action, bulk-delete, or "selected count" UI wired to it. So dropping selection on mobile breaks no actual workflow, because no batch workflow exists on desktop either. R3.3's literal "selection semantics preserved on the card" is loosely violated, but the spec's "(tap target = the whole card where applicable)" clause and the absence of any selection consumer make navigation the correct primary mobile interaction. Non-blocking note. If a batch action is ever added to Media later, mobile selection will need an explicit follow-up.

Other observations (non-blocking)

  • The mobile card is wrapped in <div className="p-4"> inside the bordered card, then MediaMobilePagination sits below it inside the same rounded-lg border bg-card. Consistent with the desktop wrapping. Fine.
  • isSmall (usePrefersSmallScreen, 900px) is still used for effectiveColumnVisibility desktop sub-breakpoint hiding; isMobile (useIsMobile, 768px) drives the card/table branch. Two hooks, two distinct purposes, correctly not conflated. The design notes useIsMobile should replace ad-hoc matchMedia; usePrefersSmallScreen is a separate 900px concern left intact — acceptable, not in Slice 3's scope to consolidate.
  • Lint warnings are in UsersPage.impl.tsx, pre-existing, unrelated.

Verdict: commit

No blockers. Desktop path is byte-identical (non-regression confirmed), mobile fields are null-safe and type-correct, pagination duplication is semantically equivalent (tech debt, not a bug), navigation preserved, column-visibility correctly hidden, tests assert real behavior on both breakpoints, and lint/build/test are green. The two non-blocking notes (pagination duplication; vestigial selection not surfaced on mobile) are appropriate follow-up items, not commit gates.

Acceptance

  • criterion-1 (implement the change without widening scope): satisfied. Only the two Slice 3 files changed; DataTable and other slices untouched; no scope creep into shared-component refactors.