# 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,800–3,900 total (sum of per-slice ranges below) |
| 400-line budget risk | High (slices 1, 6, 7 sit at/over the boundary; 4–5 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) | ~400–580 | **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) | ~280–420 | Medium | If over, split **2a** (cards/buttons/dialogs) → **2b** (tables/panels). |
| 3 | Backups cluster + nav | ~250–400 | Likely OK | Single PR; split nav edit out if over. |
| 4 | Dashboard + Applications | ~300–450 | Medium | If over, split **4a** (Applications, smaller) → **4b** (Dashboard). |
| 5 | Settings + Actions | ~350–500 | Medium-High | If over, split **5a** (Actions) → **5b** (Settings). |
| 6 | Users (largest) | ~550–850 | **No** | Force split **6a** (directory table + selection + Drawer/Sheet) → **6b** (compose dialog + formatting actions + attachments). |
| 7 | DataGrid → TanStack (highest risk) | ~450–700 | **No** | Force split **7a** (`DataTable` wrapper + tests + FileBrowser) → **7b** (Media, server-driven pagination). |
| 8 | Cleanup + docs | ~80–160 | 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 **~10–12 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: ~400–580 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. ~280–420 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

> ~250–400 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

> ~300–450 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)

> ~350–500 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)

> ~550–850 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

- [x] Migrate the compose dialog (`Dialog`/`DialogActions`/`DialogContent`/`DialogTitle` + `TextField`/`Divider`/`IconButton`) → shadcn `Dialog` family + `Input`/`Textarea`/`Separator` + `Button variant="ghost" size="icon"`.
- [x] 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`.
- [x] 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`).
- [x] Add component tests for selection toggle, drawer open, and at least one compose formatting action.
- [x] **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)

> ~450–700 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

- [x] 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`.
- [x] Wire `useReactTable` with `getCoreRowModel()`; `getPaginationRowModel()` only when `enablePagination && !manualPagination`; controlled `rowSelection` + `columnVisibility`; **never** `getSortedRowModel`, **never** `enableColumnResizing`/`size`.
- [x] 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`.
- [x] Add component tests for `DataTable`: row-selection toggle, column-visibility toggle, row-click callback fires (RED→GREEN before re-wiring pages).
- [x] 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.
- [x] **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)

- [x] 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`).
- [x] 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).
- [x] 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.
- [x] 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).
- [x] Add component tests asserting the toggleable column set equals the locked 15 and that row-click triggers the navigation callback.
- [x] **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)

> ~80–160 lines, single PR. Depends on all prior slices. `@mui/*` + `@emotion/*` are
> removed only after every consumer is gone.

- [x] Remove from `frontend/package.json`: `@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`, `@emotion/react`, `@emotion/styled` (run `npm install`/regenerate the lockfile).
- [x] 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).
- [x] Grep-verify `recharts`, `d3`, `d3-*` have zero imports and are absent from `package.json` (carry-over from slice 1; re-confirm).
- [x] Confirm `frontend/src/theme.ts` does not exist and no `theme`/`getAppTheme` import remains.
- [x] Run final gates from `frontend/`: `npm run build` (tsc -b + vite build), `npm run lint`, `npm test`, and `node --test tests` — all green.
- [x] 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`.
- [x] **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.
