Files
manage/openspec/changes/web-ui-rework/spec.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

17 KiB

Web UI Specification

Change: web-ui-rework · Domain: web-ui · Phase: spec Grounded in openspec/changes/web-ui-rework/proposal.md and exploration.md (verified inventory). Locked product decisions encoded as requirements: IA (Backups top-level; surface named "Media" at /media with /applications → redirect), TanStack visibility-only table parity, palette (#4f8cff kept + chart-1..5 repurposed), comfortable density, Vitest harness in slice 1.

Purpose

Define WHAT must be true of the Manage web frontend after the rework: a single coherent UI on shadcn/ui + Tailwind v4 + lucide-react, a thin-dashboard observability model (no in-app charts; Grafana deep-links preserved), TanStack Table data grids with visibility-only parity, a reconciled information architecture, and zero MUI/@emotion/recharts/d3/theme.ts residue — delivered as a chain of build-and-lint-green slices. This spec is acceptance-focused and verifiable; it deliberately does not prescribe implementation.

Requirements

Requirement: Single design system

The frontend MUST render all surfaces using only shadcn/ui primitives, Tailwind v4 @theme tokens, and lucide-react icons. The application MUST NOT introduce any additional component or styling library. The application MUST NOT retain any @mui/* or @emotion/* import.

Scenario: No MUI imports remain anywhere in source

  • GIVEN the web-ui-rework change is fully applied
  • WHEN a recursive search of frontend/src is performed for imports from @mui/material, @mui/icons-material, @mui/x-data-grid, @emotion/react, or @emotion/styled
  • THEN the search returns zero matches
  • AND @mui/material, @mui/icons-material, @mui/x-data-grid, @emotion/react, and @emotion/styled are absent from frontend/package.json

Scenario: No new design system adopted

  • GIVEN the rework is applied
  • WHEN frontend/package.json dependencies are inspected
  • THEN no component library other than the existing shadcn/ui + Tailwind + lucide-react stack is present

Requirement: Orphaned charting dependencies removed and no in-app charting

The frontend MUST remove the unused recharts and d3 dependencies from frontend/package.json. The frontend MUST NOT introduce any in-app chart, time-series, or log-stream visualization component. The no-op frontend/src/theme.ts shim MUST be deleted. Metric surfaces MUST be expressed as numbers or outbound Grafana links only.

Scenario: Charting dependencies are gone

  • GIVEN the final slice is applied
  • WHEN frontend/package.json is inspected
  • THEN neither recharts nor d3 is listed as a dependency
  • AND a recursive search of frontend/src for imports from recharts, d3, or d3-* returns zero matches

Scenario: No chart component is introduced

  • GIVEN the rework is applied
  • WHEN the migrated pages and components are inspected
  • THEN no component renders an in-app chart, sparkline, or graph canvas
  • AND observability metric surfaces render only as numeric values, status Badges, or outbound Grafana deep-link affordances

Scenario: Theme shim deleted

  • GIVEN the foundation slice is applied
  • WHEN the frontend/src/theme.ts file is checked for existence
  • THEN it does not exist
  • AND no import of theme/getAppTheme remains in frontend/src

Requirement: Palette — primary color kept and chart tokens repurposed

The primary color MUST remain #4f8cff. The existing chart-1..5 CSS tokens in frontend/src/index.css MUST be repurposed as Grafana-link and status color cues (success / info / warning / destructive / neutral) and MUST NOT be dropped from the token system. Status Badges MUST convey meaning through these semantic color cues.

Scenario: Primary color unchanged

  • GIVEN the rework is applied
  • WHEN frontend/src/index.css is inspected
  • THEN the primary color token resolves to #4f8cff in both light and dark themes

Scenario: Chart tokens retained and repurposed

  • GIVEN the rework is applied
  • WHEN frontend/src/index.css is inspected
  • THEN the chart-1 through chart-5 tokens are still defined
  • AND they are documented/applied as Grafana-link and status semantic cues (success / info / warning / destructive / neutral)
  • AND status Badges draw their variant colors from these cues

Requirement: Comfortable visual density across all surfaces

The redesign MUST use comfortable density on every surface: page padding p-4 md:p-6 (matching the migrated shell <main>) and card spacing gap-4. The application MUST NOT introduce a compact or dense mode for any table, list, or panel.

Scenario: Comfortable density on every page

  • GIVEN each migrated page is rendered
  • WHEN the page content container is inspected
  • THEN it uses comfortable padding consistent with the shell (p-4 md:p-6) and card gaps (gap-4)

Scenario: No compact mode

  • GIVEN the rework is applied
  • WHEN the frontend is inspected for a density/compact toggle or compact spacing classes on tables
  • THEN no compact or dense mode exists for Media, Users, Backups runs, or any other surface

Requirement: Status Badge semantics

The Badge component MUST provide variants that cover success (healthy/OK), info (default), neutral (secondary), warning, and destructive (error / firing alert). Status displays across Backups, Observability, and Users MUST map status values to these variants consistently.

Scenario: Healthy status uses a success cue

  • GIVEN a monitoring target or backup run is in a healthy/OK state
  • WHEN its status is rendered
  • THEN it displays a Badge using the success color cue (mapped from the repurposed chart-* tokens)

Scenario: Firing alert uses a destructive cue

  • GIVEN an Alertmanager alert is firing
  • WHEN its status is rendered
  • THEN it displays a destructive Badge

Requirement: Information architecture — Backups as a top-level navigation item

The Backups surface MUST be exposed as a top-level navigation item in the sidebar, linking to the existing /backups route.

Scenario: Backups is reachable from the sidebar

  • GIVEN the application shell is rendered
  • WHEN the sidebar navigation items are inspected
  • THEN a top-level "Backups" item is present
  • AND selecting it navigates to /backups and renders the Backups page

Requirement: Information architecture — Media surface naming and route reconciliation

The Media/Applications surface MUST be named "Media" going forward, and its canonical route MUST be /media. The legacy /applications route MUST redirect (replace) to /media, mirroring the existing /monitoring/observability redirect pattern. This change MUST NOT alter the OIDC authentication model or the react-router-dom routing structure beyond the renamed nav item, the /media route, and the /applications redirect.

Scenario: Media route is the canonical entry

  • GIVEN the rework is applied
  • WHEN the sidebar "Media" item is selected
  • THEN the browser navigates to /media
  • AND the Media/Applications surface is rendered under the name "Media"

Scenario: Legacy /applications redirects to /media

  • GIVEN the application is running
  • WHEN a user navigates directly to /applications
  • THEN the client router issues a replace redirect to /media
  • AND the Media surface is rendered (same pattern as /monitoring/observability)

Scenario: Auth and routing model unchanged

  • GIVEN the rework is applied
  • WHEN the authentication and routing setup is inspected
  • THEN the OIDC flow (auth.ts, react-oidc-context) and the react-router-dom structure are unchanged
  • AND only nav items, the /media route, and the /applications redirect differ from the prior state

Requirement: TanStack Table — visibility-only parity (no sorting, no resizing)

The Media and FileBrowser data grids MUST be rebuilt on @tanstack/react-table styled with the shadcn Table primitive, reproducing only the features currently in use: column visibility management, pagination (where present), row selection, and row click. This is visibility only parity — the smallest DataGrid scope — and the rebuilt tables MUST NOT add column sorting, and MUST NOT add column resizing.

Scenario: No sorting added

  • GIVEN the rebuilt Media and FileBrowser tables are rendered
  • WHEN the table headers and column definitions are inspected
  • THEN no sortable-column behavior is present
  • AND no sort affordance is rendered

Scenario: No column resizing added

  • GIVEN the rebuilt Media and FileBrowser tables are rendered
  • WHEN the column edges are inspected
  • THEN no column-resize handles or resizing behavior are present

Requirement: TanStack Table — column visibility management preserved

Both rebuilt tables MUST preserve the existing column-visibility management behavior, allowing the same set of columns to be shown or hidden as before the rework.

Scenario: Column visibility works on Media

  • GIVEN the rebuilt Media table is rendered
  • WHEN a user toggles the visibility of a column
  • THEN that column is shown or hidden accordingly
  • AND the set of toggleable columns matches the pre-rework DataGrid columns (title, series, season, episode, type, year, runtime, size, bitrate, hdr, video, resolution, date_added, library, path)

Scenario: Column visibility works on FileBrowser

  • GIVEN the rebuilt FileBrowser table is rendered
  • WHEN a user toggles the visibility of a column
  • THEN that column is shown or hidden accordingly
  • AND the set of toggleable columns matches the pre-rework columns (type, name, ext, size, modified)

Requirement: TanStack Table — pagination preserved where present

The Media table MUST preserve its pagination behavior (page-size selection, total-count display, and page navigation) as it existed in the pre-rework DataGrid.

Scenario: Media pagination parity

  • GIVEN the rebuilt Media table is rendered with more rows than one page
  • WHEN a user changes the page size and navigates between pages
  • THEN the visible rows, total count, and page index update consistently with the pre-rework behavior

Requirement: TanStack Table — row selection preserved

The rebuilt tables MUST preserve their existing row-selection behavior (checkbox selection model) where it existed pre-rework.

Scenario: Row selection works on Media

  • GIVEN the rebuilt Media table is rendered
  • WHEN a user selects and deselects rows via the selection control
  • THEN the selection state is maintained across pagination and matches the pre-rework row-selection model

Scenario: Row selection works on FileBrowser

  • GIVEN the rebuilt FileBrowser table is rendered
  • WHEN a user selects rows via the selection control
  • THEN the selection state is maintained and matches the pre-rework model

Requirement: TanStack Table — row click behavior preserved

The rebuilt tables MUST preserve their existing row-click behavior. On Media, clicking a row MUST open the file browser at the item's path. On FileBrowser, clicking a row MUST select that file for ffprobe preview, matching the pre-rework behavior.

Scenario: Media row click opens file browser

  • GIVEN the rebuilt Media table is rendered
  • WHEN a user clicks a media row
  • THEN navigation occurs to the file browser targeted at the clicked item's path, as before the rework

Scenario: FileBrowser row click selects file for preview

  • GIVEN the rebuilt FileBrowser table is rendered
  • WHEN a user clicks a file row
  • THEN that file is selected for ffprobe preview, as before the rework

Requirement: Observability parity preserved (thin dashboard)

The Observability surface MUST continue to render Alertmanager alerts, Prometheus target health, monitoring-machine status, and Grafana deep-link cards (kiosk iframe URLs and Explore links). The rework MUST NOT introduce any in-app chart, and MUST preserve all existing Grafana deep-link affordances.

Scenario: Alerts and target health still shown

  • GIVEN the Observability page is rendered
  • WHEN Alertmanager and Prometheus data are available
  • THEN Alertmanager alerts and Prometheus target health are displayed as before the rework
  • GIVEN the Observability page is rendered
  • WHEN a Grafana deep-link card is inspected
  • THEN the outbound kiosk/explore URL with instance variables is preserved
  • AND an external-link affordance is present and consistent across the surface

Scenario: No in-app chart on observability

  • GIVEN the Observability page is rendered
  • WHEN metric surfaces are inspected
  • THEN no in-app chart is rendered; metrics are numbers, status Badges, or Grafana deep-links only

Requirement: Component test harness introduced and maintained

Slice 1 MUST introduce a Vitest + @testing-library/react component test harness. Every component migrated by this rework MUST have real component tests asserting the migrated behavior. The existing node --test suites in frontend/tests (e.g. users.test.mjs, userState.test.mjs) MUST continue to run and pass.

Scenario: Vitest harness is present

  • GIVEN the foundation slice is applied
  • WHEN frontend/package.json and config are inspected
  • THEN Vitest and @testing-library/react are installed
  • AND a Vitest test script is configured

Scenario: Migrated components have component tests

  • GIVEN a slice migrates a component
  • WHEN that slice is applied
  • THEN at least one component test exercising the migrated behavior exists and passes

Scenario: Legacy node:test suites keep passing

  • GIVEN the rework is applied
  • WHEN the frontend/tests/*.mjs suites are executed with node --test
  • THEN all pre-existing assertions still pass

Requirement: Per-slice build and lint green gate

Every slice in the delivery chain MUST leave npm run build (which runs tsc -b then vite build) and npm run lint (eslint) green as a hard, non-negotiable gate. No slice may be merged with a failing build or lint.

Scenario: A slice leaves the build green

  • GIVEN any slice in the chain is applied in isolation
  • WHEN npm run build is executed in frontend/
  • THEN the command exits successfully with no TypeScript or Vite errors

Scenario: A slice leaves lint green

  • GIVEN any slice in the chain is applied in isolation
  • WHEN npm run lint is executed in frontend/
  • THEN the command exits successfully with no eslint errors

Requirement: Eight-slice delivery strategy (per-slice scope and gate)

The rework MUST be delivered as the chained eight-slice strategy at the requirement level, with each slice independently reviewable and leaving build + lint green: (1) Foundation — add the missing shadcn primitives and @tanstack/react-table, remove recharts/d3, delete theme.ts, introduce the Vitest harness; (2) Shared components — migrate the reusable building blocks (SectionCard, SelectionRailCard, TabbedCard, MetricCard, DiskSpaceCard, HoverEditButton, DialogFooter, ConfirmDialog, LibraryOverview, NowPlaying/SessionActivityPanel); (3) Backups cluster (and surface /backups in nav); (4) Dashboard + Media/Applications surface; (5) Settings + Actions; (6) Users (largest consumer; may sub-split); (7) DataGrid → TanStack Table (Media + FileBrowser; highest risk, sequenced last); (8) Cleanup + docs — remove @mui/* + @emotion/*, final typecheck/lint pass, update docs/REQUIREMENTS.md.

Scenario: Foundation slice enables later slices

  • GIVEN slice 1 is applied
  • WHEN the shared primitives, @tanstack/react-table, Vitest harness are present and recharts/d3/theme.ts are removed
  • THEN npm run build and npm run lint are green and the foundation is in place for subsequent slices

Scenario: Backups slice surfaces nav

  • GIVEN slice 3 is applied
  • WHEN the Backups cluster is migrated
  • THEN the /backups route is reachable as a top-level nav item and the build + lint are green

Scenario: Final cleanup slice removes all MUI

  • GIVEN slice 8 is applied
  • WHEN a recursive search for @mui/* and @emotion/* imports is performed in frontend/src
  • THEN zero matches remain
  • AND the cleanup slice leaves build + lint green

Requirement: Documentation updated to reflect the new UX and architecture

docs/REQUIREMENTS.md MUST be updated to reflect the post-rework UX and architecture (single design system, thin-dashboard observability, TanStack tables, reconciled IA, removed dependencies).

Scenario: Requirements doc reflects the rework

  • GIVEN the final slice is applied
  • WHEN docs/REQUIREMENTS.md is inspected
  • THEN it documents the shadcn/ui + Tailwind + lucide-react stack, the thin-dashboard observability model (no in-app charts), the /media route with /applications redirect, the Backups top-level nav item, and the removal of MUI/@emotion/recharts/d3

Requirement: No backend API or frontend data-contract changes

The rework MUST NOT change any backend API contract or any type in frontend/src/types/*. If a UI simplification forces a contract change, that change MUST be flagged separately and approved outside this spec.

Scenario: Frontend data contracts unchanged

  • GIVEN the rework is applied
  • WHEN frontend/src/types/* and the backend API surface are compared to the pre-rework state
  • THEN no backend endpoint, response shape, or shared frontend type is altered by this change (unless a separate, flagged change is approved)