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

143 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ✓
- `size``r.size || "-"` (string, null-safe) ✓
- `hdr``r.hdr || "-"` (string, null-safe) ✓
- `library``r.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.