# 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 ``. - `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 `

`–`

`/`

`/`` + Tailwind `text-* font-*` | none needed | | `Box` | `

` + flex/grid utilities | none needed | | `Stack` | `
` (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` | `
` 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` + `

` | **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 ` (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 ``; `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 `

` 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` → `` (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).