# Exploration — web-ui-rework

> Phase: **explore**. Evidence-grounded mapping of the rework surface. No code changes.
> Recovered by the orchestrator after the `sdd-explore` subagent hit an MCP-bridge heap OOM;
> all figures below were re-derived directly from source with multiline-aware parsing and verified with `grep`.

## 1. Scope and non-goals

### In scope

- Finish the **MUI v9 → shadcn/ui + Tailwind CSS + lucide-react** migration for every remaining `@mui/*` consumer in `frontend/src`.
- Deliver a **visual/UX redesign** on top of the migrated primitives, consistent with the already-migrated style references: `frontend/src/App.tsx` (shell + sidebar) and `frontend/src/components/ObservabilityPage.tsx`.
- Align the UI to the **new observability model**: Manage is a *thin dashboard*; charts/metrics/logs live in external, decoupled Grafana. In-app surfaces show Alertmanager alerts + Prometheus target health + Grafana deep-links only.
- Remove orphaned charting deps (`recharts`, `d3`) and the no-op `theme.ts` shim.
- Add the missing shadcn primitives and `@tanstack/react-table` needed for the migration.
- Update `docs/REQUIREMENTS.md` to reflect the UX/architecture change (per `AGENTS.md`).

### Non-goals

- **No in-app charting.** Do not re-introduce recharts/d3 or build custom charts; metrics visualisation stays in Grafana.
- **No new design system.** Use shadcn/ui + the existing Tailwind v4 `@theme` token system; do not adopt a different component library.
- **No backend API changes** as part of this rework. The data contracts in `frontend/src/types/*` are unchanged unless a UI simplification forces one, which must then be flagged separately.
- **No MUI retention.** `@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`, `@emotion/react`, `@emotion/styled` are all removed by the final slice.
- **No auth/routing model change.** OIDC flow (`auth.ts`, `react-oidc-context`) and `react-router-dom` structure stay as-is; only nav *items*/IA may shift.

## 2. Extended component inventory (verified from source)

Parsing note: MUI imports are multiline (`import {\n Box,\n Card,\n} from "@mui/material"`); figures below use a brace-bounded multiline parser and were cross-checked with `grep -rlE '@mui/(material|icons-material|x-data-grid)' src`.

### 2a. `@mui/material` — 22 files, 37 distinct components

| File | # | Components |
|---|---|---|
| `pages/UsersPage.impl.tsx` | 25 | Alert, Avatar, Box, Button, Checkbox, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, Drawer, IconButton, LinearProgress, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip, Typography |
| `pages/Dashboard.tsx` | 20 | Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogContent, DialogTitle, FormControl, FormControlLabel, FormHelperText, Grid, InputLabel, MenuItem, Select, Stack, Switch, TextField, Typography |
| `pages/Actions.tsx` | 19 | Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogContent, DialogTitle, Divider, FormControl, InputLabel, MenuItem, Select, Stack, Tab, Tabs, TextField, Typography |
| `pages/Settings.tsx` | 18 | Alert, Box, Button, Card, CardContent, Checkbox, Chip, Dialog, DialogContent, DialogTitle, FormControlLabel, Grid, MenuItem, Stack, Switch, Tab, TextField, Typography |
| `pages/FileBrowser.impl.tsx` | 15 | Alert, Box, Button, Card, CardContent, Chip, FormControl, Grid, InputLabel, MenuItem, Select, Stack, Tab, TextField, Typography |
| `pages/Media.tsx` | 14 | Alert, Box, Button, Card, CardContent, FormControl, Grid, InputLabel, LinearProgress, MenuItem, Select, Stack, TextField, Typography |
| `components/BackupRunsTable.tsx` | 12 | Chip, FormControl, InputLabel, MenuItem, Paper, Select, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
| `components/SessionActivityPanel.tsx` | 10 | Button, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography |
| `components/BackupAlertsTable.tsx` | 9 | Button, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
| `pages/Applications.tsx` | 9 | Alert, Box, Card, CardContent, Chip, Grid, Stack, Tab, Typography |
| `components/BackupJobsTable.tsx` | 8 | Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow |
| `components/DiskSpaceCard.tsx` | 7 | Box, Card, CardContent, Grid, LinearProgress, Stack, Typography |
| `components/BackupDashboardWidget.tsx` | 5 | Box, Card, CardContent, Chip, Typography |
| `components/ConfirmDialog.tsx` | 5 | Dialog, DialogContent, DialogTitle, Stack, Typography |
| `components/LibraryOverview.tsx` | 5 | Card, CardContent, Grid, Stack, Typography |
| `components/SectionCard.tsx` | 5 | Box, Card, CardContent, Stack, Typography |
| `components/BackupsPage.tsx` | 4 | Box, Tab, Tabs, Typography |
| `components/SelectionRailCard.tsx` | 4 | Box, Card, CardContent, Typography |
| `components/TabbedCard.tsx` | 4 | Box, Card, CardContent, Tabs |
| `components/DialogFooter.tsx` | 3 | Box, Button, DialogActions |
| `components/MetricCard.tsx` | 3 | Card, CardContent, Typography |
| `components/HoverEditButton.tsx` | 1 | IconButton |

**Component frequency (migration sizing):** Typography 16 · Box 14 · Card/CardContent 13 · Chip 11 · Stack 11 · Button 9 · Grid 7 · Alert 7 · MenuItem 6 · TextField 6 · Paper/Table*/FormControl/InputLabel/Select/Tab 5 · Dialog/DialogContent/DialogTitle 5 · Tabs 3 · LinearProgress 3 · DialogActions/IconButton/Divider/FormControlLabel/Switch/Checkbox 2 · FormHelperText/Avatar/Drawer/Tooltip 1.

### 2b. `@mui/x-data-grid` — 2 files (DataGrid)

- `pages/Media.tsx` — `DataGrid`, `GridColDef`
- `pages/FileBrowser.impl.tsx` — `DataGrid`, `GridColDef`, `GridRowSelectionModel`

### 2c. `@mui/icons-material` — 2 files, 10 distinct icons (default-import-per-icon style)

| File | Icons → lucide-react equivalent |
|---|---|
| `components/HoverEditButton.tsx` | `EditOutlined` → `Pencil` |
| `pages/UsersPage.impl.tsx` | `Close`→`X` · `AttachFile`→`Paperclip` · `FormatBold`→`Bold` · `FormatItalic`→`Italic` · `Link`→`Link` · `FormatListBulleted`→`List` · `MailOutlined`→`Mail` · `Send`→`Send` · `DeleteOutlined`→`Trash2` |

> **lucide caveat:** `package.json` pins `lucide-react: ^1.14.0`, an unusual major. The named icons above (`Pencil`, `X`, `Paperclip`, `Bold`, `Italic`, `Link`, `List`, `Mail`, `Send`, `Trash2`) are standard lucide exports, but the version pin must be verified to export them before the UsersPage slice.

### 2d. Orphaned / unused deps (verified zero imports)

- `recharts: ^3.8.1` — **no `from "recharts"` anywhere.** Removable.
- `d3: ^7.9.0` — **no `from "d3"` / `from "d3-*"` anywhere.** Removable.
- `frontend/src/theme.ts` — no-op shim (`getAppTheme` returns `{}`); safe to delete once no caller remains.

### 2e. Already-migrated style references (the design targets)

- `frontend/src/App.tsx` — shell: fixed sidebar (`w-16`/`w-60` collapse), `TopBar` (sticky, backdrop-blur, version chips), `MobileDrawer` (shadcn `Sheet`), `useDarkMode` toggles `dark` class on `<html>`.
- `frontend/src/components/ObservabilityPage.tsx` (575 lines) — uses shadcn `Card/Badge/Alert/Button/Select/Skeleton/Collapsible` + 12 lucide icons; Grafana deep-links via kiosk URLs (`/d/node-exporter-overview/...?kiosk&var-instance=…`) and Explore.

## 3. MUI → shadcn/ui + Tailwind mapping table

| MUI | shadcn/ui + Tailwind | Primitive status |
|---|---|---|
| `Card` / `CardContent` | `Card` / `CardContent` (`@/components/ui/card`) | **present** |
| `Typography` | semantic `<h1>`–`<h6>`/`<p>`/`<span>` + Tailwind `text-* font-*` | none needed |
| `Box` | `<div>` + flex/grid utilities | none needed |
| `Stack` | `<div className="flex flex-col gap-*">` (or `flex-row`) | none needed |
| `Grid` | CSS grid `grid grid-cols-* gap-*` w/ responsive `md:`/`lg:` | none needed |
| `Chip` | `Badge` (`@/components/ui/badge`) | **present** |
| `Button` | `Button` (`@/components/ui/button`) | **present** |
| `IconButton` | `Button variant="ghost" size="icon"` (pattern already used in shell) | **present** |
| `Tooltip` | `Tooltip` (`@/components/ui/tooltip`) | **present** |
| `Alert` | `Alert`/`AlertTitle`/`AlertDescription` | **present** |
| `Select`/`MenuItem`/`FormControl`/`InputLabel` | `Select` family (`@/components/ui/select`) | **present** |
| `Paper` | `<div className="rounded-lg border bg-card">` or `Card` | none needed |
| `Table`/`TableHead`/`TableBody`/`TableRow`/`TableCell`/`TableContainer` | shadcn `Table` family | **add** |
| `Tabs`/`Tab` | shadcn `Tabs` (`TabsList`/`TabsTrigger`/`TabsContent`) | **add** |
| `Dialog`/`DialogTitle`/`DialogContent`/`DialogActions` | shadcn `Dialog` (+ `DialogFooter`, `DialogHeader`) | **add** |
| `TextField` | `Input` (+ `Label`) | **add** |
| `Checkbox` | `Checkbox` | **add** |
| `Switch` | `Switch` | **add** |
| `LinearProgress` | `Progress` | **add** |
| `Divider` | `Separator` | **add** |
| `Avatar` | `Avatar` | **add** |
| `Drawer` | `Sheet` (side="right") — already used for mobile nav | **present** |
| `FormControlLabel` / `FormHelperText` | `Label` + `<p className="text-xs text-muted-foreground">` | **add** `Label` |
| `DataGrid` (`@mui/x-data-grid`) | TanStack Table + shadcn `Table` (see §4) | **add** `@tanstack/react-table` |

## 4. DataGrid decision — TanStack Table (KEY TECHNICAL RISK)

**Recommendation:** migrate both DataGrid usages to **`@tanstack/react-table`** (headless) styled with the shadcn `Table` primitive. Rationale: headless (no opinionated styling, fits Tailwind), first-class sorting/filtering/pagination/selection/column-visibility, actively maintained, and the project already uses `@tanstack/react-query` so the dependency family is consistent.

**Feature parity that MUST be preserved** (derived from source):

| Page | Columns (`field`) | Features in use |
|---|---|---|
| `pages/Media.tsx` | title, series, season, episode, type, year, runtime_min, size, bitrate, hdr, video, resolution, date_added, library, path | pagination, row selection, row click (opens file browser), column visibility management, stable row id |
| `pages/FileBrowser.impl.tsx` | type, name, ext, size, modified | row selection, row click (selects file for ffprobe), column visibility management |

**Open parity questions for the proposal phase:**

- Does the redesign keep client-side column *resizing*, or only *visibility*? (Source scan shows visibility; resizing usage is ambiguous and must be confirmed against the live DataGrid props.)
- Sorting/filtering: Media has external controls already (its own filter UI); decide whether TanStack sorting is added or deferred to keep slice size ≤400 lines.

This is the single highest-risk slice; it is sequenced **last** so the shared Table primitive and design tokens are already settled.

## 5. shadcn primitives gap list

Add via `npx shadcn@latest add <name>` (project already initialised — `components.json`, `@/components/ui`, `cn()` in `src/lib/utils.ts`):

```
tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area
```

Plus dependency: `npm i @tanstack/react-table`.

Already present (do not re-add): `button tooltip sheet card badge alert select skeleton collapsible`.

## 6. Design tokens, theming, and redesign direction

**Current tokens (`frontend/src/index.css`, Tailwind v4 `@theme`)** — already a complete shadcn-style system:

- Font: Inter (300–700 via Google Fonts).
- Light: background `#fafafa`, foreground `#0f172a`, card `#ffffff`, primary `#4f8cff`, border `#e2e8f0`, muted-foreground `#64748b`.
- `.dark`: background `#0f172a`, card `#1e293b`, border `#334155`, muted-foreground `#94a3b8`.
- Radius `0.625rem`. Chart palette `chart-1..5` defined but **unused** (no charting) — keep for Grafana-link color cues, or drop.
- Dark mode: `useDarkMode()` in `App.tsx` toggles `.dark` on `<html>`; `theme.ts` is a no-op shim.

**Config:** `frontend/tailwind.config.cjs` is minimal (`content` glob only) — correct for Tailwind v4 which reads tokens from `@theme` in CSS. No JS theme config to migrate.

**Redesign direction (consistent with shell + ObservabilityPage):**

- **Density:** comfortable, not cramped. Page padding `p-4 md:p-6` (matches `<main>` in `App.tsx`). Card gap `gap-4`. Section spacing via `SectionCard` replacement.
- **Typography ramp:** headings `text-lg font-semibold` (page title, per `TopBar`), section titles `text-base font-semibold`, body `text-sm`, captions `text-xs text-muted-foreground`.
- **Color semantics:** status uses Badge variants — `default` (info), `secondary` (neutral), `destructive` (error/firing alert), and a success variant to add for healthy/OK (currently only ObservabilityPage has health semantics). Keep primary blue `#4f8cff`.
- **Surfaces:** prefer `Card` over ad-hoc `Paper`; one border weight (`border-border`), one radius (`rounded-lg`).
- **Layout primitive:** replace MUI `Grid`/`Stack` with a small set of Tailwind patterns documented in the design doc so all 22 files stay consistent across slices.

## 7. Information architecture & navigation

**Current nav (`App.tsx` `navItems`):** Dashboard (`/`), Observability (`/observability`), Media (`/applications`), Files (`/files`), Users (`/users`), Actions (`/actions`), Settings (`/settings`).

**Routing facts:**

- `/monitoring` → `<Navigate to="/observability" replace />` (legacy redirect).
- `/media` → `Applications` (alias).
- `/backups` route **exists** (`BackupsPage`) but is **not in the sidebar** — reachable only by URL or via the Dashboard widget's deep-link.

**IA recommendations for the proposal:**

- Surface **Backups** in the nav (currently hidden) — either its own item or folded into a "Media ops" group.
- Reconcile the **Media ↔ Applications** naming (nav says "Media", route is `/applications`, page component is `Applications`); pick one name.
- Keep Observability as a first-class top-level item (it is the post-decoupling home for status + Grafana links).

## 8. Observability-decoupling implications for the redesign

- **Stays in-app:** Alertmanager alerts list, Prometheus target health table, monitoring-machine status, Grafana deep-link cards (kiosk iframe URLs + Explore). All already implemented in `ObservabilityPage.tsx`.
- **Becomes / stays a Grafana deep-link:** any metric chart, time-series, log stream. The `chart-1..5` tokens and orphaned `recharts`/`d3` deps are leftovers from the removed in-app Monitoring UI — removing them codifies the decoupling.
- **Design consequence:** no chart components need to be designed; "metric" surfaces become either a number (MetricCard) or an outbound Grafana link. The redesign should make the **external-link affordance** consistent (lucide `ExternalLink`, already used in `ObservabilityPage`).

## 9. Slice plan preview (Force-chained, ≤400 changed lines/PR)

Sequenced so each slice leaves `npm run build` + `npm run lint` green and is independently reviewable:

1. **Foundation** — add shadcn primitives (§5), add `@tanstack/react-table`, remove `recharts`+`d3` from `package.json`, delete `theme.ts`. Enables all later slices.
2. **Shared components** — migrate the reusable building blocks first to prevent drift: `SectionCard`, `SelectionRailCard`, `TabbedCard`, `MetricCard`, `DiskSpaceCard`, `HoverEditButton`, `DialogFooter`, `ConfirmDialog`, `LibraryOverview`, `NowPlaying`/`SessionActivityPanel`.
3. **Backups cluster** — `BackupAlertsTable`, `BackupJobsTable`, `BackupRunsTable`, `BackupsPage`, `BackupDashboardWidget` (+ surface `/backups` in nav).
4. **Dashboard + Applications** — `Dashboard.tsx`, `Applications.tsx`.
5. **Settings + Actions** — `Settings.tsx`, `Actions.tsx`.
6. **Users** — `UsersPage.impl.tsx` (largest MUI consumer: 25 components + 9 icons + Drawer + Table + rich-text compose). May sub-split.
7. **DataGrid → TanStack Table** — `Media.tsx` + `FileBrowser.impl.tsx` (highest risk; sequenced last).
8. **Cleanup + docs** — remove `@mui/*` + `@emotion/*` deps, final typecheck/lint pass, update `docs/REQUIREMENTS.md`.

~8 slices; slices 6 and 7 are the most likely to need sub-splitting at apply time to stay under 400 lines.

## 10. Risks & open questions for the proposal

1. **DataGrid parity (high)** — TanStack Table must reproduce pagination/selection/row-click/column-visibility; resizing/sorting scope undecided (§4).
2. **UsersPage size (high)** — single largest migration; rich-text compose UI with formatting actions, attachments, drawer, table. Needs its own careful slice (possibly split).
3. **Cross-slice visual drift (medium)** — 22 files migrated over 8 slices; mitigated by doing shared components (slice 2) and documenting the Tailwind layout patterns in the design doc first.
4. **lucide-react v1 pin (low–medium)** — unusual major (`^1.14.0`); verify all 10 mapped icon names exist before the UsersPage slice.
5. **Form patterns (low)** — current forms are uncontrolled/`useState`; keep parity, no form library introduction (non-goal).
6. **IA decisions (product)** — Backups nav placement, Media/Applications naming (§7).
7. **Testing coverage (medium)** — see §11; no component test harness exists today.

## 11. Testing strategy note

- `frontend/package.json` has **no `test` script**. Capability today:
  - `npm run build` → `tsc -b && vite build` (typecheck + build).
  - `npm run lint` → `eslint .`.
  - `node --test` on `frontend/tests/*.mjs` (`users.test.mjs`, `userState.test.mjs`) — pure-JS transform tests, run directly, not wired into npm.
- `openspec/config.yaml` declares **no strict-TDD flag**.
- **Implication:** "green" for each slice = `tsc -b` clean + `eslint` clean + existing `node --test` suites pass + manual smoke of the affected page. The proposal should decide whether to introduce a minimal component test harness (e.g. Vitest + Testing Library) as slice 1 work, or proceed on static + smoke evidence only.

---

**Status:** explore complete. **Recommended next phase:** `proposal` (with a product-question round before locking, given redesign + IA ambiguity).
