feat(frontend): slice 4 — migrate Dashboard + Applications to shadcn/Tailwind

Web UI rework.
- Migrate pages/Dashboard.tsx off @mui (shortcut CRUD dialogs, machine
  switcher, BackupDashboardWidget mount; shortcuts reuse ConfirmDialog/
  DialogFooter from slice 2)
- Migrate pages/Applications.tsx shell off @mui (tabs + library counts);
  keeps <Media/> child intact (Media.tsx still MUI, deferred to slice 7
  with its DataGrid)
- Behavioral tests for both pages

Gate: build + lint + test green.
This commit is contained in:
Developer
2026-06-17 13:15:51 +00:00
parent 77c6b62ee2
commit c721f0dece
6 changed files with 621 additions and 371 deletions
@@ -498,3 +498,121 @@ Dashboard + Applications surface** (depends on slices 2 + 3; reuses
Overall change `applyState` remains **blocked** on missing domain specs (legacy
flat `spec.md`); does not block Slice 3 (done) but should be resolved before
`sdd-verify`/archive.
## Slice 4 — Dashboard + Applications/Media surface (DONE)
Scope: migrate exactly two pages off MUI onto shadcn/ui + Tailwind —
`frontend/src/pages/Applications.tsx` and `frontend/src/pages/Dashboard.tsx`.
The still-MUI `<Media/>` child (DataGrid, slice 7) is **left untouched**;
only the Applications shell around it was migrated. All 5 Slice 4 task
checkboxes in `tasks.md` are now `- [x]` (38/71 → **33+5 = 38/71** overall;
remaining unchecked = Slices 58).
### Files changed
- `frontend/src/pages/Applications.tsx` — full rewrite (MUI → shadcn).
- `frontend/src/pages/Dashboard.tsx` — full rewrite (MUI → shadcn).
- `frontend/src/pages/__tests__/Applications.test.tsx` — new (1 test).
- `frontend/src/pages/__tests__/Dashboard.test.tsx` — new (3 tests).
- `openspec/changes/web-ui-rework/tasks.md` — 5 Slice 4 checkboxes `- [ ]` → `- [x]`.
No other files touched (App.tsx, Media.tsx, FileBrowser.impl.tsx,
Settings/Actions/UsersPage.impl.tsx, components/*, package.json all unchanged).
### Component mapping applied (per design §1)
Applications shell: `Alert`→`Alert`+`AlertDescription`; `Chip`→`Badge variant="outline"`;
`Card`/`CardContent` stat tiles → bordered `rounded-lg border bg-card` divs
(per design §1 Paper row, which permits bordered-surface div **or** Card);
`Grid`→responsive CSS grid (`grid-cols-2 md:grid-cols-4` counts,
`md:grid-cols-2` libraries); `Stack`→`flex flex-col gap-*`; `Tab`→`TabsTrigger`;
`Typography`→semantic text utilities.
Dashboard: `Alert`→`Alert`+`AlertDescription`; `Button`→shadcn `Button` (Open=`default`,
Edit=`outline`, Delete=`destructive` to preserve the `color="error"` cue);
`Card`/`CardContent`→shadcn `Card`/`CardContent`; `Chip`→`Badge variant="outline"`;
Dialog family→shadcn `Dialog`/`DialogContent`/`DialogHeader`/`DialogTitle`;
the **delete-confirm flow now reuses the shared `ConfirmDialog`** (slice 2)
instead of a raw MUI `Dialog`+`DialogFooter`; `Select`/`MenuItem`/`FormControl`/
`InputLabel`→shadcn `Select` family (Type picker + Jellyfin machine switcher);
`Switch`→shadcn `Switch` (`onCheckedChange`); `TextField`+`FormControlLabel`+
`FormHelperText`→`Input`+`Label`+muted `<p>` (factored into a local `Field` helper);
`Grid`→CSS grid; `Stack`→`flex`; `Typography`→text utilities.
### Parity preserved
- Dashboard shortcut CRUD (website/action/user types) — create/edit dialog,
open (website = `window.open`, action/user = `navigate`), edit, delete-confirm.
- Jellyfin machine switcher (Select when >1 machine, Badge when 1, nothing when 0).
- `NowPlaying` + `BackupDashboardWidget` composition unchanged (both reused as-is).
- Shortcut deep-links (`/actions?task=…`, `/users?user=…`) are byte-for-byte
preserved; the page mounts at the reconciled `/media` route (App.tsx, slice 3,
untouched). No backend contract changes.
- Applications tabs (Jellyfin/Nextcloud) + Jellyfin library counts grid preserved.
### How the still-MUI Media child is handled
`Applications.tsx` keeps `import { Media } from "./Media";` and renders
`<Media />` **exactly as before** inside the Jellyfin tab. Only the Applications
*shell* (tabs, library-counts grid, Nextcloud alert, header) was migrated.
`pages/Media.tsx` is unchanged and still imports `@mui/x-data-grid`/`@mui/material`
— that is expected and is removed in slice 7. The page compiles because Media.tsx
is untouched; the slice-4 Applications test mocks the child (`vi.mock("../Media")`)
so it does not pull the DataGrid into jsdom.
### Commands run + gates
- `grep -nE '@mui/(material|icons-material|x-data-grid)' Dashboard.tsx Applications.tsx`
→ **BOTH-MUI-FREE**.
- `npx tsc --noEmit` → exit 0.
- `npm run build` → exit 0 (built in 1.07s).
- `npm run lint` → exit 0 (0 errors; the 2 warnings are pre-existing in
`UsersPage.impl.tsx`, slice 6 — not this slice's files).
- `npm test` (vitest) → exit 0 (**17 files / 34 tests** pass; +4 new page tests).
- `npm run test:node` (`node --test tests/*.test.mjs`) → exit 0 (4/4).
**Note on `node --test tests`:** the Slice 4 gate text and the overall-change
exit gate list `node --test tests`, but on Node v22 that bare form resolves
`tests` as a single CommonJS module (`Cannot find module '…/tests'`) and fails
for **every** slice, including the pre-slice-4 baseline — it is a Node
invocation quirk, not a regression. The package's canonical node-suite command
is `npm run test:node` = `node --test tests/*.test.mjs`, which is green (4/4).
The slice is therefore gate-green under the package's own scripts.
### Deviations from design
- Stat/library tiles in Applications use bordered `div` surfaces instead of
nested shadcn `Card`s — explicitly permitted by design §1 ("Paper → bordered
surface `<div>` **or** `Card`"). Keeps the already-Card-wrapped `SectionCard`
interior light and avoids heavy nested-card chrome.
- Dashboard delete-confirm dialog switched from a raw MUI `Dialog`+`DialogFooter`
to the shared `ConfirmDialog` (slice 2). Behavior (title/message/confirm/cancel,
error cue) is identical and reuses an already-migrated shared component as the
task instructs.
### Slice boundary / PR
Single slice, well under the 400-line budget: ~2 page rewrites (~430 inserted /
~360 deleted across the two files) + 2 new test files (~150 lines). No 4a/4b
split needed. The parent owns the commit/PR; nothing committed here.
### Top risk for slice 5
**`Settings.tsx` and `Actions.tsx`** are the form-heavy pair (18 + 19 MUI
components each, incl. SSH-key management, SSH test/validation feedback,
saved-task editor with machine selection + run history, danger-zone reset). The
`ConfirmDialog`/`DialogFooter`/`HoverEditButton`/`SectionCard`/`SelectionRailCard`/
`TabbedCard` reuse pattern is now proven (Dashboard reuses ConfirmDialog cleanly);
the main slice-5 risk is preserving the controlled-`useState` form behavior + SSH
validation messages without introducing a form library, and keeping the
`@testing-library` tests exercisable without live SSH. Keep all form state as
plain `useState`; mirror the Dashboard `Field` helper for `Input`+`Label`+
helper-text triples.
### Structured status note
Overall change `applyState` is still reported **blocked** by the status engine
(domain specs missing/partial; legacy flat `spec.md`). This does not block the
Slice 4 migration itself — `design.md` §1 provided the authoritative component
mapping and `actionContext` is `repo-local` with `allowedEditRoots` covering the
workspace. Should be resolved before `sdd-verify`/archive, per the slice-3 note.