dd778d8850
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.
322 lines
17 KiB
Markdown
322 lines
17 KiB
Markdown
# 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
|
|
|
|
#### Scenario: Grafana deep-links preserved
|
|
|
|
- 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)
|