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

18 KiB
Raw Blame History

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.tsxDataGrid, GridColDef
  • pages/FileBrowser.impl.tsxDataGrid, GridColDef, GridRowSelectionModel

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

File Icons → lucide-react equivalent
components/HoverEditButton.tsx EditOutlinedPencil
pages/UsersPage.impl.tsx CloseX · AttachFilePaperclip · FormatBoldBold · FormatItalicItalic · LinkLink · FormatListBulletedList · MailOutlinedMail · SendSend · DeleteOutlinedTrash2

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.1no from "recharts" anywhere. Removable.
  • d3: ^7.9.0no 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).
  • /mediaApplications (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 clusterBackupAlertsTable, BackupJobsTable, BackupRunsTable, BackupsPage, BackupDashboardWidget (+ surface /backups in nav).
  4. Dashboard + ApplicationsDashboard.tsx, Applications.tsx.
  5. Settings + ActionsSettings.tsx, Actions.tsx.
  6. UsersUsersPage.impl.tsx (largest MUI consumer: 25 components + 9 icons + Drawer + Table + rich-text compose). May sub-split.
  7. DataGrid → TanStack TableMedia.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 buildtsc -b && vite build (typecheck + build).
    • npm run linteslint ..
    • 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).