Files
manage/openspec/changes/web-ui-rework/exploration.md
T
Developer dd778d8850 feat(frontend): slice 1 — foundation for MUI->shadcn migration
Web UI rework (openspec/changes/web-ui-rework). Foundation slice:
- Add @tanstack/react-table; remove orphaned recharts, d3
- Vendor shadcn primitives: tabs table dialog input label checkbox
  switch progress separator avatar textarea dropdown-menu scroll-area
- Add Vitest + @testing-library/react + jsdom harness; npm test script
- Delete no-op theme.ts shim; remove getAppTheme references
- Smoke test proves the harness

Gate: build + lint + test green.
2026-06-17 12:11:05 +00:00

222 lines
18 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.
# 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 (300700 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 (lowmedium)** — 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).