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.
18 KiB
Exploration — web-ui-rework
Phase: explore. Evidence-grounded mapping of the rework surface. No code changes. Recovered by the orchestrator after the
sdd-exploresubagent hit an MCP-bridge heap OOM; all figures below were re-derived directly from source with multiline-aware parsing and verified withgrep.
1. Scope and non-goals
In scope
- Finish the MUI v9 → shadcn/ui + Tailwind CSS + lucide-react migration for every remaining
@mui/*consumer infrontend/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) andfrontend/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-optheme.tsshim. - Add the missing shadcn primitives and
@tanstack/react-tableneeded for the migration. - Update
docs/REQUIREMENTS.mdto reflect the UX/architecture change (perAGENTS.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
@themetoken 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/styledare all removed by the final slice. - No auth/routing model change. OIDC flow (
auth.ts,react-oidc-context) andreact-router-domstructure 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,GridColDefpages/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.jsonpinslucide-react: ^1.14.0, an unusual major. The named icons above (Pencil,X,Paperclip,Bold,Italic,Link,List,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— nofrom "recharts"anywhere. Removable.d3: ^7.9.0— nofrom "d3"/from "d3-*"anywhere. Removable.frontend/src/theme.ts— no-op shim (getAppThemereturns{}); 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-60collapse),TopBar(sticky, backdrop-blur, version chips),MobileDrawer(shadcnSheet),useDarkModetogglesdarkclass on<html>.frontend/src/components/ObservabilityPage.tsx(575 lines) — uses shadcnCard/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 palettechart-1..5defined but unused (no charting) — keep for Grafana-link color cues, or drop. - Dark mode:
useDarkMode()inApp.tsxtoggles.darkon<html>;theme.tsis 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>inApp.tsx). Card gapgap-4. Section spacing viaSectionCardreplacement. - Typography ramp: headings
text-lg font-semibold(page title, perTopBar), section titlestext-base font-semibold, bodytext-sm, captionstext-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
Cardover ad-hocPaper; one border weight (border-border), one radius (rounded-lg). - Layout primitive: replace MUI
Grid/Stackwith 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)./backupsroute 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 isApplications); 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..5tokens and orphanedrecharts/d3deps 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 inObservabilityPage).
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:
- Foundation — add shadcn primitives (§5), add
@tanstack/react-table, removerecharts+d3frompackage.json, deletetheme.ts. Enables all later slices. - Shared components — migrate the reusable building blocks first to prevent drift:
SectionCard,SelectionRailCard,TabbedCard,MetricCard,DiskSpaceCard,HoverEditButton,DialogFooter,ConfirmDialog,LibraryOverview,NowPlaying/SessionActivityPanel. - Backups cluster —
BackupAlertsTable,BackupJobsTable,BackupRunsTable,BackupsPage,BackupDashboardWidget(+ surface/backupsin nav). - Dashboard + Applications —
Dashboard.tsx,Applications.tsx. - Settings + Actions —
Settings.tsx,Actions.tsx. - Users —
UsersPage.impl.tsx(largest MUI consumer: 25 components + 9 icons + Drawer + Table + rich-text compose). May sub-split. - DataGrid → TanStack Table —
Media.tsx+FileBrowser.impl.tsx(highest risk; sequenced last). - Cleanup + docs — remove
@mui/*+@emotion/*deps, final typecheck/lint pass, updatedocs/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
- DataGrid parity (high) — TanStack Table must reproduce pagination/selection/row-click/column-visibility; resizing/sorting scope undecided (§4).
- UsersPage size (high) — single largest migration; rich-text compose UI with formatting actions, attachments, drawer, table. Needs its own careful slice (possibly split).
- 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.
- lucide-react v1 pin (low–medium) — unusual major (
^1.14.0); verify all 10 mapped icon names exist before the UsersPage slice. - Form patterns (low) — current forms are uncontrolled/
useState; keep parity, no form library introduction (non-goal). - IA decisions (product) — Backups nav placement, Media/Applications naming (§7).
- Testing coverage (medium) — see §11; no component test harness exists today.
11. Testing strategy note
frontend/package.jsonhas notestscript. Capability today:npm run build→tsc -b && vite build(typecheck + build).npm run lint→eslint ..node --testonfrontend/tests/*.mjs(users.test.mjs,userState.test.mjs) — pure-JS transform tests, run directly, not wired into npm.
openspec/config.yamldeclares no strict-TDD flag.- Implication: "green" for each slice =
tsc -bclean +eslintclean + existingnode --testsuites 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).