Files
manage/openspec/changes/web-ui-rework/tasks.md
T
Developer 1e23c07a20 feat(frontend): slice 6a — Users directory surface + drawer (shadcn)
Web UI rework. Slice 6a (force-split; 6b = compose dialog next):
- UsersPage.impl.tsx directory surface off @mui: shadcn Table family +
  Checkbox + Badge (status: success=chart-2/destructive/secondary) +
  Avatar + Tooltip + Progress + Alert/Button/Stack/Typography
- MUI Drawer -> shadcn Sheet side="right" for user detail drawer
  (buildUserDrawerModel rendering preserved)
- Selection-across-pagination + search/filter parity preserved
- Compose-dialog MUI subset (Dialog/TextField/Divider/IconButton +
  9 icons) intentionally LEFT for slice 6b

Gate: build + lint + test green.
2026-06-17 14:36:07 +00:00

262 lines
25 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.
# Tasks — web-ui-rework
<!-- markdownlint-disable-file MD004 -->
> Phase: **tasks**. Concrete, reviewable implementation tasks for the MUI v9 →
> shadcn/ui + Tailwind v4 + lucide-react finish migration. Grounded in
> `proposal.md`, `exploration.md`, `spec.md`, and `design.md` (all authoritative).
> No source changes in this phase — this file is the contract `sdd-apply` executes
> and `sdd-verify` checks.
>
> Locked decisions (do not re-litigate): single design system — shadcn/ui, Tailwind
> v4, and lucide-react; Backups top-level nav item; surface renamed **Media** at
> `/media` with `/applications` → redirect (mirrors `/monitoring` → `/observability`);
> TanStack Table **visibility-only** parity (pagination, row selection, row click,
> column visibility — NO sorting, NO resizing); palette keeps `#4f8cff` with
> `chart-1..5` repurposed as status/Grafana-link cues; comfortable density everywhere;
> Vitest + Testing Library harness added in slice 1; force-chained PRs, ≤400 changed
> lines/slice.
## Review Workload Forecast
| Field | Value |
|-------|-------|
| Estimated changed lines | ~2,8003,900 total (sum of per-slice ranges below) |
| 400-line budget risk | High (slices 1, 6, 7 sit at/over the boundary; 45 medium) |
| Chained PRs recommended | Yes |
| Suggested split | PR 1 → PR 2 → PR 3 → PR 4 → PR 5 → PR 6a → PR 6b → PR 7a → PR 7b → PR 8 (sub-splits on size overruns) |
| Delivery strategy | auto-chain (force-chained per locked decision; sub-split a slice before exceeding 400) |
| Chain strategy | stacked-to-main |
### Per-slice changed-line estimate
| Slice | Focus | Est. changed lines | ≤400? | Sub-split policy |
|-------|-------|--------------------|-------|------------------|
| 1 | Foundation (primitives, tanstack, Vitest, theme/badge) | ~400580 | **At/over** — vendored shadcn primitives dominate | Likely split **1a** (shadcn primitives + deps) → **1b** (Vitest harness + `theme.ts` removal + `success` Badge + chart comments). Vendored generated primitives may also be taken as a size exception. |
| 2 | Shared components (11 blocks) | ~280420 | Medium | If over, split **2a** (cards/buttons/dialogs) → **2b** (tables/panels). |
| 3 | Backups cluster + nav | ~250400 | Likely OK | Single PR; split nav edit out if over. |
| 4 | Dashboard + Applications | ~300450 | Medium | If over, split **4a** (Applications, smaller) → **4b** (Dashboard). |
| 5 | Settings + Actions | ~350500 | Medium-High | If over, split **5a** (Actions) → **5b** (Settings). |
| 6 | Users (largest) | ~550850 | **No** | Force split **6a** (directory table + selection + Drawer/Sheet) → **6b** (compose dialog + formatting actions + attachments). |
| 7 | DataGrid → TanStack (highest risk) | ~450700 | **No** | Force split **7a** (`DataTable` wrapper + tests + FileBrowser) → **7b** (Media, server-driven pagination). |
| 8 | Cleanup + docs | ~80160 | OK | Single PR. |
**Confirmation:** Slices 3 and 8 fit ≤400 as a single PR. Slices 1, 2, 4, 5 are
"medium" — apply the **sub-split-before-exceeding** rule (measure the diff mid-slice;
if it crosses 400, split at the next clean boundary). Slices 6 and 7 are **confirmed
over 400** and MUST be sub-split (6a/6b, 7a/7b) as shown. Every sub-PR keeps the
build+lint+test gate below.
**Headline:** ~3,000 changed lines across **~1012 chained PRs** (8 base slices +
mandatory Users/DataGrid sub-splits + optional Foundation/shared/Dashboard/Settings
sub-splits on overrun). Recommendation: **Force-chained, one PR per slice/sub-slice.**
### Plain-text guard lines
```text
Decision needed before apply: Yes
Chained PRs recommended: Yes
Chain strategy: stacked-to-main
400-line budget risk: High
```
> Decision needed: yes, because (a) Slice 1 vendored-shadcn-primitives size is at/over
> 400 (choose sub-split 1a/1b vs. size exception for generated code), and (b) Slices 6
> and 7 are confirmed over 400 and must be sub-split — confirm the 6a/6b and 7a/7b
> boundaries before apply. These are delivery-mechanics decisions; the product/design
> content is fully locked in `spec.md`/`design.md`.
## Slice ordering rationale & dependencies
1. **Foundation** — must be first: all later slices consume the new primitives, `@tanstack/react-table`, the Vitest harness, the `success` Badge variant, and the cleared `theme.ts`.
2. **Shared components** — depends on 1; must precede pages so every page reuses one building-block language (anti-drift).
3. **Backups cluster + nav** — depends on 2 (uses `Table`/`Badge`/`Tabs`/shared cards); also lands the IA nav + route edits (`/backups` item, `/applications``/media`).
4. **Dashboard + Applications** — depends on 2 and 3 (IA/route edits for Media live here-or-3; Dashboard reuses `BackupDashboardWidget` from 3, shared cards from 2).
5. **Settings + Actions** — depends on 2 (shared dialog/cards/inputs); independent of 3/4 content.
6. **Users** — depends on 2 (shared `Sheet`/`Table`/`Badge`/`Dialog`); largest, sub-split.
7. **DataGrid → TanStack** — depends on 2 (`Table` primitive), 4 (`Media.tsx` page already MUI-migrated to plain primitives first so only the grid swaps), and the settled `success`/status Badge cues. Deliberately last so the Table primitive, tokens, and Badge semantics are frozen.
8. **Cleanup + docs** — depends on all; removes the now-unused `@mui/*` + `@emotion/*` deps only after every consumer is gone.
Dependency DAG: `1 → 2 → {3, 4, 5, 6}` with `4 ← 3` (IA/route + backup widget);
`{3,4,5,6} → 7` (7 needs pages' MUI shell already converted so only the grid swaps);
`{1..7} → 8`.
## Universal slice exit gate (HARD — repeated for every slice)
A slice/sub-slice is **not done** until ALL of the following pass from `frontend/`:
- `npm run build` green (`tsc -b` + `vite build`).
- `npm run lint` green (eslint).
- `npm test` green (Vitest single-run) — applicable from slice 1 onward.
- Existing legacy suites still pass: `node --test tests` (`users.test.mjs`, `userState.test.mjs`).
- Manual smoke of every migrated page in that slice (no behavior regression).
Each slice section restates this gate as its final task.
---
## Slice 1 — Foundation (primitives, TanStack Table, Vitest, token/Badge cleanup)
> Enables every later slice. No page behavior change yet. Target: ~400580 changed
> lines — **likely sub-split 1a/1b** (primitives+deps vs. harness+cleanup) or
> vendored-primitives size exception. Decision: confirm split before apply.
- [x] Add the missing shadcn primitives by running, from `frontend/`:
`npx shadcn@latest add tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area`
(do NOT re-add already-present: `button tooltip sheet card badge alert select skeleton collapsible`).
- [x] Install the table dependency: `npm i @tanstack/react-table`.
- [x] Remove orphaned charting deps from `frontend/package.json`: `recharts` and `d3` (verify zero `from "recharts"` / `from "d3"` / `from "d3-*"` imports in `frontend/src` before removing; none expected).
- [x] Delete `frontend/src/theme.ts` (no-op `getAppTheme` shim) and remove every import of `theme` / `getAppTheme` in `frontend/src` (grep first, then delete).
- [x] Add the `success` Badge variant to `frontend/src/components/ui/badge.tsx`, drawing color from the repurposed `chart-2` token per design §2.3 (`bg-chart-2/10 text-chart-2 … dark:bg-chart-2/20 …`); optionally add `warning` (`chart-3`) if a call site needs it.
- [x] Document the `chart-1..5` role mapping as an inline comment block above the tokens in **both** the `@theme` block and `.dark` block of `frontend/src/index.css` (chart-1=info/brand, chart-2=success, chart-3=warning, chart-4=destructive, chart-5=neutral-accent). Do NOT change any token value; do NOT drop any token. Primary stays `#4f8cff`.
- [x] Configure the Vitest harness: create `frontend/vitest.config.ts` (separate from `vite.config.ts`) using `vitest/config` `defineConfig`, `@vitejs/plugin-react`, `test.environment: "jsdom"`, `test.globals: true`, `test.setupFiles: ["./src/test/setup.ts"]`, the `@` path alias from `tsconfig.app.json`, and `test.include: ["src/**/*.{test,spec}.{ts,tsx}"]` (must NOT claim the `frontend/tests/*.mjs` node suites).
- [x] Create `frontend/src/test/setup.ts` importing `@testing-library/jest-dom` for matcher registration.
- [x] Install dev deps: `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `jsdom`.
- [x] Add npm scripts to `frontend/package.json`: `"test": "vitest run"`, `"test:watch": "vitest"`, `"test:node": "node --test tests"`.
- [x] Add one trivial passing component test under `frontend/src/**` to prove the harness (e.g. a `success` Badge renders with the `chart-2` cue).
- [x] Verify lucide-react `^1.14.0` exports the names used by later slices (`Pencil, X, Paperclip, Bold, Italic, Link, List, Mail, Send, Trash2, DatabaseBackup, ExternalLink`); if `DatabaseBackup` is missing, fall back to `HardDrive`/`Archive` (record the chosen fallback in the slice note).
- [x] **Exit gate:** no `theme`/`getAppTheme` imports remain; `npm run build` + `npm run lint` + `npm test` + `node --test tests` all green.
---
## Slice 2 — Shared components (lock the building-block language)
> Migrate the 11 reusable blocks first to prevent cross-slice drift. Each keeps its
> exported API intact so downstream pages compile unchanged. ~280420 lines → split
> 2a/2b if over.
- [x] Migrate `frontend/src/components/SectionCard.tsx` (Box/Card/CardContent/Stack/Typography → shadcn `Card` family + Tailwind stack; comfortable density, `gap-4`).
- [x] Migrate `frontend/src/components/SelectionRailCard.tsx` (Box/Card/CardContent/Typography → `Card` + Tailwind; preserve `minHeight`/scrollable body/footer props).
- [x] Migrate `frontend/src/components/TabbedCard.tsx` (Box/Card/CardContent/Tabs → shadcn `Tabs` (`TabsList`/`TabsTrigger`/`TabsContent`) on a `Card`).
- [x] Migrate `frontend/src/components/MetricCard.tsx` (Card/CardContent/Typography → shadcn `Card` + typography ramp: label `text-sm`, value `text-lg font-semibold`, subtext `text-xs text-muted-foreground`).
- [x] Migrate `frontend/src/components/DiskSpaceCard.tsx` (Box/Card/CardContent/Grid/LinearProgress/Stack/Typography → `Card` + CSS grid + shadcn `Progress`; preserve used/free/total/percent breakdown).
- [x] Migrate `frontend/src/components/HoverEditButton.tsx` (`@mui/material` IconButton + `@mui/icons-material/EditOutlined``Button variant="ghost" size="icon"` + lucide `Pencil`; keep the hover-in visibility transition).
- [x] Migrate `frontend/src/components/DialogFooter.tsx` (Box/Button/DialogActions → `Button` row (`flex flex-row items-center gap-2`); preserve cancel/confirm/secondary-action props + busy/disabled labels).
- [x] Migrate `frontend/src/components/ConfirmDialog.tsx` (Dialog/DialogContent/DialogTitle/Stack/Typography → shadcn `Dialog` family + `DialogFooter` from this slice).
- [x] Migrate `frontend/src/components/LibraryOverview.tsx` (Card/CardContent/Grid/Stack/Typography → `Card` + responsive CSS grid `grid grid-cols-1 md:grid-cols-2 gap-4`).
- [x] Migrate `frontend/src/components/NowPlaying.tsx` (wrapper around `SessionActivityPanel`; keep the empty-state message contract) and migrate `frontend/src/components/SessionActivityPanel.tsx` (Button/Chip/Paper/Table family/Typography → `Button`/`Badge`/bordered surface/shadcn `Table` family; status → Badge variant mapping per design §2.3, healthy=`success`).
- [x] Add at least one behavioral component test per migrated block (co-located under the component's `__tests__/`), e.g. `MetricCard` renders label/value/subtext; status Badge variant mapping for `SessionActivityPanel`.
- [x] **Exit gate:** all 11 shared components MUI-free (`grep -rlE '@mui/(material|icons-material)' src/components` returns none of these files); exported APIs unchanged so pages still compile; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
---
## Slice 3 — Backups cluster + navigation/IA
> ~250400 lines, likely a single PR. Depends on slice 2 (Table/Badge/Tabs/cards).
> This is where Backups becomes a top-level nav item and the Media/Applications route
> is reconciled.
- [x] Migrate `frontend/src/components/BackupAlertsTable.tsx` (Chip/Paper/Table family/FormControl/InputLabel/MenuItem/Select/Button → `Badge` (status cues), bordered surface, shadcn `Table` family, shadcn `Select`; acknowledge button preserved; severity → Badge variant).
- [x] Migrate `frontend/src/components/BackupJobsTable.tsx` (Chip/Paper/Table family → `Badge` + shadcn `Table`; latest-run status + next-expected timing preserved).
- [x] Migrate `frontend/src/components/BackupRunsTable.tsx` (Chip/FormControl/InputLabel/MenuItem/Paper/Select/Table family → shadcn `Select` + `Badge` + `Table`; status filter + formatted duration/size/timestamp preserved).
- [x] Migrate `frontend/src/components/BackupDashboardWidget.tsx` (Box/Card/CardContent/Chip/Typography → shadcn `Card` + `Badge`; total jobs / 24h success rate / active alerts / last-failed-time preserved).
- [x] Migrate `frontend/src/components/BackupsPage.tsx` (Box/Tab/Tabs/Typography → shadcn `Tabs`; tabs Jobs/Runs/Alerts behavior + acknowledge mutation preserved).
- [x] Apply the IA nav + route edits in `frontend/src/App.tsx` per design §4: import a Backups icon (`DatabaseBackup`, or the slice-1-chosen fallback) from lucide-react; add a top-level `{ path: "/backups", label: "Backups", icon: … }` nav item (after Files, before Users); retarget the Media nav item from `/applications` to `/media`; in **both** route blocks add `<Route path="/media" element={<Applications />} />` and convert `<Route path="/applications" …>` to `<Route path="/applications" element={<Navigate to="/media" replace />} />`, mirroring the existing `/monitoring``/observability` redirect.
- [x] Add/extend component tests for the migrated Backups tables (status Badge variant mapping; alert acknowledge callback).
- [x] **Exit gate:** `/backups` reachable from the sidebar; `/applications` redirects to `/media`; Backups cluster MUI-free; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
---
## Slice 4 — Dashboard + Media/Applications surface
> ~300450 lines, medium. Depends on slices 2 and 3 (IA routes for Media + the
> `BackupDashboardWidget` from slice 3). Split 4a (Applications) → 4b (Dashboard) if
> over 400.
- [x] Migrate `frontend/src/pages/Applications.tsx` (Alert/Box/Card/CardContent/Chip/Grid/Stack/Tab/Typography → `Alert`/`Card`/`Badge`/responsive CSS grid/`Tabs`; Jellyfin library stats + Media tab preserved).
- [x] Migrate `frontend/src/pages/Dashboard.tsx` (20 MUI components: Alert/Box/Button/Card/CardContent/Chip/Dialog/DialogContent/DialogTitle/FormControl/FormControlLabel/FormHelperText/Grid/InputLabel/MenuItem/Select/Stack/Switch/TextField/Typography → shadcn `Card`/CSS grid/`Dialog`/`Select`/`Switch`/`Input`+`Label`/`Badge`; shortcut CRUD (website/action/users), machine picker, NowPlaying + BackupDashboardWidget composition, comfortable density).
- [x] Preserve the Dashboard → Media navigation and shortcut deep-links under the reconciled `/media` route.
- [x] Add component tests for the migrated Dashboard (shortcut create/save/delete flow) and Applications (library stats render).
- [x] **Exit gate:** Dashboard + Applications MUI-free and visually consistent; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
---
## Slice 5 — Settings + Actions (form-heavy pair)
> ~350500 lines, medium-high. Depends on slice 2. Keep uncontrolled/`useState` form
> parity — NO form library. Split 5a (Actions) → 5b (Settings) if over 400.
- [x] Migrate `frontend/src/pages/Actions.tsx` (19 MUI components incl. Tab/Tabs/Select/MenuItem/FormControl/InputLabel/Divider/Dialog → shadcn `Tabs`/`Select`/`Separator`/`Dialog`; saved-task editor, machine selection, run history preserved).
- [x] Migrate `frontend/src/pages/Settings.tsx` (18 MUI components incl. Grid/Switch/Checkbox/FormControlLabel/Tab/Select/Dialog → CSS grid/`Switch`/`Checkbox`/`Label`/`Tabs`/`Select`/`Dialog`; monitoring-machine CRUD, SSH-key management, SSH test/validation feedback, danger-zone reset, tabbed UI preserved).
- [x] Keep all current form behaviors (controlled `useState`, SSH validation messages, ConfirmDialog integration from slice 2) — no form-library introduction.
- [x] Add component tests for the migrated Settings (machine save/delete confirm) and Actions (save/run task) where behavior is exercisable without live SSH.
- [x] **Exit gate:** Settings + Actions MUI-free; forms behave as before; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
---
## Slice 6 — Users (largest consumer; force sub-split 6a/6b)
> ~550850 lines, **confirmed over 400** — MUST sub-split. Depends on slice 2
> (`Sheet`/`Table`/`Badge`/`Dialog`/`Avatar`). Apply the icon map from design §5
> (verify the 9 names at the `lucide-react` pin, done in slice 1).
### Slice 6a — directory table, selection, drawer
- [x] Migrate `frontend/src/pages/UsersPage.impl.tsx` directory surface: user table (`Table`/`TableBody`/`TableCell`/`TableContainer`/`TableHead`/`TableRow` + `Checkbox`/`Chip`/`Avatar`/`Tooltip` + `LinearProgress`) → shadcn `Table` family + `Checkbox` + `Badge` (status cues) + `Avatar` + `Tooltip` + `Progress`.
- [x] Replace MUI `Drawer` with shadcn `Sheet side="right"` for the user detail drawer; preserve `buildUserDrawerModel` rendering.
- [x] Preserve selection-across-pagination semantics (selected-user-id set survives paging/filtering) and the search/filter logic (`mergeUsersWithActivity`, row-level text matching).
- [x] Wire status → Badge variant mapping (healthy/activity = `success` cue) consistently with design §2.3.
### Slice 6b — compose dialog, formatting actions, attachments
- [ ] Migrate the compose dialog (`Dialog`/`DialogActions`/`DialogContent`/`DialogTitle` + `TextField`/`Divider`/`IconButton`) → shadcn `Dialog` family + `Input`/`Textarea`/`Separator` + `Button variant="ghost" size="icon"`.
- [ ] Replace the 9 `@mui/icons-material` icons with lucide per design §5: `Close→X`, `AttachFile→Paperclip`, `FormatBold→Bold`, `FormatItalic→Italic`, `Link→Link`, `FormatListBulleted→List`, `MailOutlined→Mail`, `Send→Send`, `DeleteOutlined→Trash2`.
- [ ] Preserve the rich-text compose behavior: subject + html body, markup insertion actions (bold/italic/link/list), file attachments (FormData), queue-status polling (`useUserMessageQueueStatus`), and send (`useSendUserMessage`).
- [ ] Add component tests for selection toggle, drawer open, and at least one compose formatting action.
- [ ] **Exit gate (6a+6b):** `UsersPage.impl.tsx` fully MUI/icon-MUI-free; drawer, selection-across-pages, and compose/send behavior preserved; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green on each sub-PR.
---
## Slice 7 — DataGrid → TanStack Table (highest risk; force sub-split 7a/7b)
> ~450700 lines, **confirmed over 400** — MUST sub-split. Deliberately last so the
> `Table` primitive, tokens, and Badge cues are frozen. **Visibility-only** parity:
> pagination (Media, server-driven), row selection, row click, column visibility.
> **No sorting, no resizing.** Depends on slices 2 and 4.
### Slice 7a — DataTable wrapper + FileBrowser
- [ ] Create `frontend/src/components/ui/data-table.tsx`: a generic wrapper over `@/components/ui/table` built on `@tanstack/react-table` per design §3.1, exposing `columns`, `data`, `getRowId`, `enableRowSelection`/`rowSelection`/`onRowSelectionChange`, `onRowClick`, `columnVisibility`/`onColumnVisibilityChange`/`enableColumnVisibilityToggle`, `enablePagination`/`manualPagination`/`pagination`/`onPaginationChange`/`pageSizeOptions`/`rowCount`, and `emptyMessage`.
- [ ] Wire `useReactTable` with `getCoreRowModel()`; `getPaginationRowModel()` only when `enablePagination && !manualPagination`; controlled `rowSelection` + `columnVisibility`; **never** `getSortedRowModel`, **never** `enableColumnResizing`/`size`.
- [ ] Render a leading display selection column (header select-all-on-page via `Checkbox`) only when `enableRowSelection`; row `onClick → onRowClick?.(row.original)` with `cursor-pointer`, selection-cell click stops propagation; column-visibility dropdown via `DropdownMenu` + `Checkbox` when `enableColumnVisibilityToggle`.
- [ ] Add component tests for `DataTable`: row-selection toggle, column-visibility toggle, row-click callback fires (RED→GREEN before re-wiring pages).
- [ ] Migrate `frontend/src/pages/FileBrowser.impl.tsx` off `@mui/x-data-grid` onto `DataTable`: build `fileColumns: ColumnDef<FileEntry>[]` for the 5 columns (`type, name, ext, size, modified`); `enableRowSelection`; `onRowClick` → selects the file for ffprobe preview (preserved); `enableColumnVisibilityToggle`; **no pagination** (full listing as today). Also migrate its remaining `@mui/material` shell (Card/SectionCard/TabbedCard/Select/Input) to shadcn primitives.
- [ ] **Exit gate (7a):** `DataTable` + FileBrowser on TanStack Table; FileBrowser row-click → ffprobe preview and column set (type/name/ext/size/modified) preserved; `@mui/x-data-grid` no longer imported by FileBrowser; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
### Slice 7b — Media (server-driven pagination)
- [ ] Migrate `frontend/src/pages/Media.tsx` off `@mui/x-data-grid` onto `DataTable`: build `mediaColumns: ColumnDef<MediaItem>[]` for the 15 locked columns (`title, series, season, episode, type, year, runtime_min, size, bitrate, hdr, video, resolution, date_added, library, path`).
- [ ] Render `DataTable` with `enableRowSelection`, `enablePagination` + `manualPagination` + `rowCount` (driven by `queryResult.total`), `onRowClick → navigate('/files?path=…')` (opens file browser at the item's path — preserved), and `enableColumnVisibilityToggle` (toggleable set must match the 15-column list above exactly).
- [ ] Lift pagination + column-visibility state into the existing `usePersistentState` media state and feed `useMediaQuery({ limit, offset, … })`; use a stable path-derived `getRowId` so selection survives server-driven paging.
- [ ] Also migrate the remaining `@mui/material` Media shell (Card/SectionCard/Grid/Select/Input/LinearProgress) to shadcn primitives + CSS grid + `Progress`; preserve index-build controls + progress (stop/force-stop).
- [ ] Add component tests asserting the toggleable column set equals the locked 15 and that row-click triggers the navigation callback.
- [ ] **Exit gate (7b):** Media on TanStack Table with pagination (server-driven, page-size + total-count + page-nav parity), row selection, row click → file browser, column-visibility parity; `@mui/x-data-grid` no longer imported anywhere; **no sorting, no resizing** present; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green; manual smoke of Media paging + row-click and FileBrowser row-click preview.
---
## Slice 8 — Cleanup + docs (remove MUI/@emotion, final gates, docs)
> ~80160 lines, single PR. Depends on all prior slices. `@mui/*` + `@emotion/*` are
> removed only after every consumer is gone.
- [ ] Remove from `frontend/package.json`: `@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`, `@emotion/react`, `@emotion/styled` (run `npm install`/regenerate the lockfile).
- [ ] Grep-verify ZERO remaining imports: recursive search of `frontend/src` for `@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`, `@emotion/react`, `@emotion/styled` returns no matches (hard gate for the spec "No MUI imports remain" scenario).
- [ ] Grep-verify `recharts`, `d3`, `d3-*` have zero imports and are absent from `package.json` (carry-over from slice 1; re-confirm).
- [ ] Confirm `frontend/src/theme.ts` does not exist and no `theme`/`getAppTheme` import remains.
- [ ] Run final gates from `frontend/`: `npm run build` (tsc -b + vite build), `npm run lint`, `npm test`, and `node --test tests` — all green.
- [ ] Update `docs/REQUIREMENTS.md` per `AGENTS.md`: document the single design system (shadcn/ui + Tailwind v4 + lucide-react), the thin-dashboard observability model (no in-app charts; Grafana deep-links), TanStack tables (visibility-only parity), the reconciled IA (Backups top-level nav; Media at `/media` with `/applications` redirect), the repurposed `chart-*` status cues, and the removal of `@mui/*`/`@emotion/*`/`recharts`/`d3`/`theme.ts`.
- [ ] **Exit gate:** recursive search of `frontend/src` for `@mui/*` and `@emotion/*` returns zero; requirements doc updated; `npm run build` + `npm run lint` + `npm test` + `node --test tests` all green.
---
## Acceptance cross-check (verification commands for sdd-verify)
- `cd frontend && npm run build` → must be green after every slice.
- `cd frontend && npm run lint` → must be green after every slice.
- `cd frontend && npm test` → must be green from slice 1 onward.
- `cd frontend && node --test tests` → legacy suites (`users.test.mjs`, `userState.test.mjs`) stay green throughout.
- `grep -rlE '@mui/(material|icons-material|x-data-grid)|@emotion/(react|styled)' frontend/src` → must return **nothing** after slice 8.
- `grep -rlE 'recharts|from .d3|from .d3-' frontend/src` → must return nothing (re-confirm at slice 8).
- `test ! -e frontend/src/theme.ts` → must succeed after slice 1.
- Inspect `frontend/src/App.tsx`: `/media` canonical route + `/applications``<Navigate to="/media" replace />`; a top-level Backups nav item present.