Files
manage/openspec/changes/web-ui-rework/apply-progress.md
T
Developer 77c6b62ee2 feat(frontend): slice 3 — Backups cluster migration + nav/IA
Web UI rework.
- Migrate BackupAlertsTable, BackupJobsTable, BackupRunsTable,
  BackupsPage, BackupDashboardWidget off @mui (shadcn Table + Badge
  severity variants: success=chart-2, warning=chart-3, destructive)
- App.tsx IA: Backups now top-level nav (DatabaseBackup icon);
  Media surface primary at /media; /applications -> /media redirect
  in both route trees (mirrors /monitoring -> /observability)

Gate: build + lint + test green.
2026-06-17 12:53:36 +00:00

501 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Apply Progress — web-ui-rework
> Cumulative apply log. Updated by `sdd-apply` after each slice/sub-slice.
> Strict TDD is **not** active for this change (no strict-tdd directive in
> `openspec/config.yaml`); this run used standard mode against `design.md` / `tasks.md`.
## Status context consumed
- `schemaName: gentle-pi.sdd-status`, `changeName: web-ui-rework`.
- `applyState` reported by the status engine: **blocked** (`blockedReasons`: domain
specs missing/partial; legacy flat `spec.md` present without domain specs). This is a
**planning-completeness** gap, not a safety/`actionContext` blocker.
- `actionContext`: `mode: repo-local`, `workspaceRoot: /home/user/Manage_01`,
`allowedEditRoots: ["/home/user/Manage_01"]`, `warnings: []` — safe.
- This run executed the explicitly delegated **Slice 1 (Foundation)** scope per the
parent acceptance contract. The foundation work is fully specified in `tasks.md`
(Slice 1) and does not depend on the missing domain specs, so it proceeds under the
parent's explicit delegation. The `instructions.apply` line itself says
"Implement only unchecked tasks from the tasks artifact."
- `artifactStore: openspec`; persisted task checkboxes updated in `tasks.md` (13 → `[x]`).
## Slice 1 — Foundation (primitives, TanStack Table, Vitest, token/Badge cleanup) — COMPLETE
All 13 Slice-1 tasks in `tasks.md` are marked `- [x]`. Summary below.
### Completed tasks (persisted checkboxes updated)
- [x] Added the missing shadcn primitives (13): `tabs table dialog input label checkbox
switch progress separator avatar textarea dropdown-menu scroll-area` via
`npx shadcn@latest add …`. The 9 already-present primitives
(`button tooltip sheet card badge alert select skeleton collapsible`) were **not** re-added.
- [x] Installed `@tanstack/react-table` (`^8.21.3`, dependency).
- [x] Removed orphaned charting deps: `recharts`, `d3`, and the companion `@types/d3`
from `package.json` (zero `from "recharts"` / `from "d3"` / `from "d3-*"` imports
existed in `frontend/src`).
- [x] Deleted `frontend/src/theme.ts` (no-op `getAppTheme` shim). Grep confirmed the
only reference was the file itself; no source file imported it.
- [x] Added the `success` Badge variant (`chart-2`) and the `warning` variant (`chart-3`)
to `frontend/src/components/ui/badge.tsx`, mirroring the `destructive` soft-cue pattern
exactly per design §2.3.
- [x] Documented the `chart-1..5` role mapping as an inline comment block above the
tokens in **both** the `@theme` block and `.dark` block of `frontend/src/index.css`
(chart-1=info/brand, chart-2=success, chart-3=warning, chart-4=destructive,
chart-5=neutral-accent). **No token value changed; no token dropped. Primary `#4f8cff`.**
- [x] Created `frontend/vitest.config.ts` (separate from `vite.config.ts`):
`vitest/config` `defineConfig`, `@vitejs/plugin-react`, `environment: "jsdom"`,
`globals: true`, `setupFiles: ["./src/test/setup.ts"]`, the `@` path alias from
`tsconfig.app.json`, and `include: ["src/**/*.{test,spec}.{ts,tsx}"]` (does NOT claim
the `frontend/tests/*.mjs` node suites).
- [x] Created `frontend/src/test/setup.ts` registering `@testing-library/jest-dom`
matchers (via the `/vitest` entry for runtime + TS augmentation).
- [x] Installed dev deps: `vitest@^4.1.9`, `@testing-library/react@^16.3.2`,
`@testing-library/jest-dom@^6.9.1`, `@testing-library/user-event@^14.6.1`,
`jsdom@^29.1.1`.
- [x] Added npm scripts: `"test": "vitest run"`, `"test:watch": "vitest"`,
`"test:node": "node --test tests/*.test.mjs"` (see Deviations).
- [x] Added one trivial passing component test: `src/components/ui/__tests__/badge.test.tsx`
(asserts a `success` Badge renders with the `chart-2` cue).
- [x] Verified lucide-react `1.14.0` exports every name used by later slices:
`Pencil, X, Paperclip, Bold, Italic, Link, List, Mail, Send, Trash2, DatabaseBackup,
ExternalLink`. **`DatabaseBackup` IS exported — no fallback needed** (slice-3 nav will
use `DatabaseBackup`).
- [x] **Exit gate green:** `npm run build` + `npm run lint` + `npm test` + `npm run
test:node` all pass; no `theme`/`getAppTheme` imports remain.
### Files changed (tracked)
Modified:
- `frontend/package.json` (added `@tanstack/react-table`, vitest/testing-library/jsdom
dev deps, test scripts; removed `recharts`, `d3`, `@types/d3`)
- `frontend/package-lock.json` (regenerated by npm)
- `frontend/src/components/ui/badge.tsx` (added `success` + `warning` variants)
- `frontend/src/components/ui/tabs.tsx` (vendored primitive; un-exported
`tabsListVariants` to satisfy `react-refresh/only-export-components` — see Deviations)
- `frontend/src/index.css` (chart-1..5 role-mapping comment blocks in `@theme` + `.dark`)
Deleted:
- `frontend/src/theme.ts`
Added (new):
- `frontend/vitest.config.ts`
- `frontend/src/test/setup.ts`
- `frontend/src/components/ui/__tests__/badge.test.tsx`
- 13 new shadcn primitives under `frontend/src/components/ui/`:
`tabs.tsx, table.tsx, dialog.tsx, input.tsx, label.tsx, checkbox.tsx, switch.tsx,
progress.tsx, separator.tsx, avatar.tsx, textarea.tsx, dropdown-menu.tsx,
scroll-area.tsx`
Untouched (no-unintended-edits respected): the 22 MUI page/component files in
Slice 1's scope (all `frontend/src/pages/*` and `frontend/src/components/*.tsx` outside
`components/ui/`) — `git status --porcelain frontend/src/pages 'frontend/src/components/*.tsx'
| grep -v 'components/ui/'` returns nothing.
Also touched (gitignored generated docs, not tracked changes): `frontend/src/.pi-map.md`
and `frontend/src/.pi-map.index.md` were refreshed to remove the now-deleted `theme.ts` /
`getAppTheme` references (the `project_map_patch` tool is unavailable in this session, so
the stale generated maps were minimally reconciled to match source).
### Commands run (validation)
- `npx shadcn@latest add tabs table dialog input label checkbox switch progress separator avatar textarea dropdown-menu scroll-area --yes` → created 14 files (13 primitives + a duplicate button written to a stray `@/` path; stray dir removed, dup button discarded).
- `npm i @tanstack/react-table` → ok.
- `npm i -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom` → ok.
- `npm uninstall recharts d3 @types/d3` → ok.
- `cd frontend && npm run build` → **PASS** (`tsc -b` + `vite build`).
- `cd frontend && npm run lint` → **PASS** (0 errors, 0 errors; 2 pre-existing `react-hooks/exhaustive-deps` warnings in `UsersPage.impl.tsx` that are out of Slice-1 scope).
- `cd frontend && npm test` → **PASS** (Vitest: 1 file, 1 test passed).
- `cd frontend && npm run test:node` → **PASS** (legacy node:test suites green).
### Deviations from design / task text (documented)
1. **`tabs.tsx` variant export.** The vendored shadcn `tabs.tsx` exported
`tabsListVariants` (a `cva` function) alongside components, which violates
`react-refresh/only-export-components` and broke `npm run lint`. The project's own
convention (`button.tsx`, `select.tsx`) is to **not** export the `cva` variant helper,
so `tabsListVariants` was removed from the `export { … }` statement (the `const` is
retained for internal use by `TabsList`). This keeps the component API intact for all
later slices (TabbedCard uses `<TabsList variant=…>`, not the helper).
2. **`test:node` script form.** The task text specifies `"test:node": "node --test tests"`,
but on **Node v22.22.2** a bare directory argument is resolved as a CommonJS module
entry point (`Error: Cannot find module '…/tests'`). The functional equivalent
`"test:node": "node --test tests/*.test.mjs"` is used instead, which targets exactly
the legacy node:test suites (`users.test.mjs`, `userState.test.mjs`) and passes.
`node --test` auto-discovery also passes; the glob form is the explicit, minimal choice.
3. **shadcn CLI path artifact.** The CLI resolved the `@/` alias to a literal
`frontend/@/` directory. All 13 primitives were moved to `frontend/src/components/ui/`
and the stray `frontend/@/` tree was deleted; the duplicate generated `button.tsx`
was discarded to keep the existing tracked `button.tsx` unchanged.
4. **Stale generated `.pi-map.md`** files referenced the deleted `theme.ts`. Since the
`project_map_patch` tool is unavailable this session, they were minimally reconciled to
match source (these are gitignored generated docs, not tracked changes).
### Slice boundary / PR
- Slice 1 is the foundation slice (~vendored primitives dominate the line count).
Per the Review Workload Forecast this slice is "at/over" 400 lines and may be taken as
a vendored-primitives **size exception** or sub-split 1a/1b. The majority of the diff is
generated shadcn primitives (mechanical, vendored). **No delivery decision was provided
in this run's delegation** (the parent delegated a single foundation slice); this run
implemented the whole foundation in one slice. The parent should confirm the PR shape
(1a/1b split vs. generated-code size exception) when opening the PR.
## Remaining tasks (Slices 28, 58 unchecked)
Slice 1 is the only completed slice. The remaining unchecked work (Slices 28) is listed
verbatim in `tasks.md`. The first unchecked items are (Slice 2 — Shared components):
- [ ] Migrate `frontend/src/components/SectionCard.tsx` …
- [ ] Migrate `frontend/src/components/SelectionRailCard.tsx` …
- [ ] … (see `tasks.md` Slice 2 → Slice 8)
Dependency order is fixed (DAG in `tasks.md`): `1 → 2 → {3,4,5,6}`; `4 ← 3`;
`{3,4,5,6} → 7`; `{1..7} → 8`. **Next recommended:** Slice 2 (Shared components) — it is
unblocked now that all primitives, TanStack Table, the Vitest harness, the `success`/
`warning` Badge variants, and the cleared `theme.ts` are in place.
## Risks / notes for the parent
- The overall change `applyState` is **blocked** on **missing domain specs** (legacy flat
`spec.md` present without domain specs). This does not block Slice 1 (done) but should
be resolved before `sdd-verify`/archive. Recommended: convert/supplement `spec.md` into
domain specs, or have the orchestrator confirm the flat spec is acceptable.
- The two `react-hooks/exhaustive-deps` **warnings** in `UsersPage.impl.tsx` are
pre-existing and out of Slice-1 scope (lint still passes — warnings, not errors).
- `npm audit` reports 8 vulnerabilities in the dependency tree (pre-existing across the
MUI/emotion/react stack); not introduced by this slice and out of scope.
- `DatabaseBackup` from lucide-react is available — Slice 3 nav should use it directly.
## Slice 2 — Shared components (lock the building-block language) — COMPLETE
All 12 Slice-2 task lines in `tasks.md` are now `- [x]`. The 11 shared components
are MUI-free and the exported APIs are preserved so every consuming page still
compiles unchanged (no `frontend/src/pages/*` file was edited this slice).
### Status context consumed
- `applyState` reported by the status engine: **blocked** (`blockedReasons`:
domain specs missing/partial; legacy flat `spec.md` present without domain
specs). This is a **planning-completeness** gap, not a safety/`actionContext`
blocker. `actionContext`: `mode: repo-local`, `allowedEditRoots: ["/home/user/Manage_01"]`,
`warnings: []` — safe.
- This run executed the explicitly delegated **Slice 2 (Shared components)**
scope per the parent acceptance contract. Slice-2 work is fully specified in
`tasks.md` + `design.md` (§1 mapping table, §2 typography ramp, §2.3 success
Badge variant) and does not depend on the missing domain specs. The
`instructions.apply` line says "Implement only unchecked tasks from the tasks
artifact." → proceeded under the parent's explicit slice delegation.
- `artifactStore: openspec`; persisted task checkboxes updated in `tasks.md`
(Slice 2: 0 → 12 `[x]`). Cumulative change task progress: 13 → **25/71** complete.
### What was migrated (10 files rewritten; 1 already-clean)
- `SectionCard.tsx` → shadcn `Card`/`CardContent`; `gap-4` comfortable density;
section title `text-base font-semibold`, description `text-sm text-muted-foreground`.
- `SelectionRailCard.tsx` → `Card` with titled header / scrollable body / footer.
Preserved `minHeight` (inline style), `footer`, scrollable body. Legacy
`contentSx`/`bodySx` MUI-sx props **retained in the interface as no-ops** so
Actions/Settings compile unchanged.
- `TabbedCard.tsx` → shadcn `Tabs` (`TabsList variant="line"`) on a `Card`.
`value`/`onChange` stay string-typed (controlled); `tabs` stays `ReactElement[]`;
legacy `contentSx`/`tabsSx` retained as no-op props.
- `MetricCard.tsx` → `Card`/`CardContent` on the design ramp: label `text-sm`,
value `text-lg font-semibold`, subtext `text-xs text-muted-foreground`.
- `DiskSpaceCard.tsx` → `Card` + CSS grid (`grid-cols-1 sm:grid-cols-3 gap-4`)
- shadcn `Progress`; used/free/total/percent breakdown preserved; the progress
color cue (chart-2 success / chart-3 warning / destructive) is emitted via full
static `[&_[data-slot=progress-indicator]]:bg-*` class strings (verified present
in the built CSS).
- `HoverEditButton.tsx` → `Button variant="ghost" size="icon-sm"` + lucide `Pencil`.
Kept the `rail-edit` class + `opacity-0`/`transition-opacity` base so the existing
hover-reveal rules in Actions/Settings (`&:hover .rail-edit { opacity: 1 }`)
still target it until those pages migrate (slices 5). MUI `IconButton`+
`EditOutlined` removed.
- `DialogFooter.tsx` → horizontal `Button` row (`flex flex-row items-center
gap-2`). **All legacy props preserved**: `confirmColor` (`error`→`destructive`
variant) and `confirmVariant` (`outlined`→`outline`, `text`→`ghost`, else
`default`) are mapped internally onto shadcn variants; `confirmBusyLabel`,
`confirmDisabled`, `confirmStartIcon`, `secondaryAction`, `cancelLabel` all honored.
- `ConfirmDialog.tsx` → shadcn `Dialog` family (`DialogContent`/`DialogHeader`/
`DialogTitle`/`DialogDescription`) reusing the migrated `DialogFooter`. Esc /
overlay click routes to `onCancel` via `onOpenChange`. Same exported props.
- `LibraryOverview.tsx` → `Card`/`CardContent` on a responsive CSS grid
(`grid grid-cols-1 md:grid-cols-2 gap-4`); movie/TV counts render verbatim.
- `SessionActivityPanel.tsx` → shadcn `Table` family on a bordered rounded
scrollable surface + `Badge` (status variant map per design §2.3: `playing`→
`success`/chart-2, `paused`→`warning`/chart-3, idle/other→`secondary`) +
`Button` for the action. Row-click + action-button callbacks, status summary
row, and transcoding formatting preserved.
- `NowPlaying.tsx` → **already MUI-free** (it only imports `../types` +
`./SessionActivityPanel`); left unchanged. Its empty-state message contract
("No recent user activity sessions right now.") is exercised by a new test.
### Component tests added (11 files, co-located under `src/components/__tests__/`)
- `MetricCard` (label/value/subtext + subtext-omitted), `DiskSpaceCard` (used/
free/total + percent headline), `HoverEditButton` (onClick fires, custom label),
`DialogFooter` (cancel/confirm callbacks, busy label, error→destructive,
secondary action), `ConfirmDialog` (title/message render + confirm/cancel,
closed renders nothing), `SectionCard` (title/description/action/children),
`SelectionRailCard` (title/body/footer + `minHeight` applied), `TabbedCard`
(renders triggers + reports selection change), `LibraryOverview` (movie/TV
cards + counts), `SessionActivityPanel` (status→Badge variant mapping
playing/paused/idle, empty-state, row-click + action-button callbacks),
`NowPlaying` (dashboard empty-state message contract).
### Files changed (tracked)
Modified (10 components):
- `frontend/src/components/{SectionCard,SelectionRailCard,TabbedCard,MetricCard,
DiskSpaceCard,HoverEditButton,DialogFooter,ConfirmDialog,LibraryOverview,
SessionActivityPanel}.tsx`
- `openspec/changes/web-ui-rework/tasks.md` (Slice 2 checkboxes 0 → 12 `[x]`)
Added (new, 11 test files):
- `frontend/src/components/__tests__/{MetricCard,DiskSpaceCard,HoverEditButton,
DialogFooter,ConfirmDialog,SectionCard,SelectionRailCard,TabbedCard,
LibraryOverview,SessionActivityPanel,NowPlaying}.test.tsx`
Untouched (no-unintended-edits respected): **no `frontend/src/pages/*` file
edited this slice** — `git status --porcelain frontend/src/pages` is empty.
`NowPlaying.tsx` is unchanged (already MUI-free). No `components/ui/*` primitive
was modified.
### Commands run (validation) — all green
- `grep -rlE '@mui/(material|icons-material)' <11 files>` → **ALL 11 MUI-FREE**.
- `cd frontend && npm run build` → **PASS** (`tsc -b` + `vite build`).
- `cd frontend && npm run lint` → **PASS** (0 errors; the only 2 items are the
pre-existing `react-hooks/exhaustive-deps` **warnings** in `UsersPage.impl.tsx`,
out of Slice-2 scope).
- `cd frontend && npm test` → **PASS** (Vitest: **12 files, 22 tests** passed;
11 new component tests + the slice-1 Badge smoke test).
- `cd frontend && npm run test:node` → **PASS** (legacy node:test: 4 tests, 0 fail).
- Verified Tailwind emitted the DiskSpaceCard `[data-slot=progress-indicator]`
chart-2/3/destructive utilities into the built CSS (arbitrary-variant classes
written as full static strings).
### API-preserving compromises (design constraints accepted to keep pages compiling)
1. **`SelectionRailCard` / `TabbedCard` legacy `*Sx` props.** Kept
`contentSx`/`bodySx`/`tabsSx` in the prop interfaces as documented no-ops
(MUI `sx` objects have no Tailwind equivalent). They are intentionally **not**
destructured into locals (avoids `no-unused-vars`) and are ignored at render.
Consumers (Actions, Settings) pass them today and compile unchanged.
2. **`DialogFooter` color/variant mapping.** `confirmColor`/`confirmVariant` are
MUI-only concepts; they are retained on the API and mapped to shadcn Button
variants (`error`→`destructive`, `outlined`→`outline`, `text`→`ghost`). All
current consumers pass at most `confirmColor="error"` and `secondaryAction`,
which map cleanly. No consumer passes `confirmVariant` explicitly today.
3. **`HoverEditButton` hover reveal.** Implemented with a Tailwind `opacity-0`/
`transition-opacity` base (not inline style) so the not-yet-migrated pages'
MUI-sx `&:hover .rail-edit { opacity: 1 }` rules still override it on hover
(higher specificity) during the interim. The `rail-edit` class is preserved.
4. **`SessionActivityPanel` status→Badge mapping.** `playing` (active/healthy)
→ `success` per the task's "healthy=`success`" + design §2.3; `paused`→`warning`;
idle/unknown→`secondary`. This is the documented cue map (no app semantics
changed — the prior MUI Chip used primary/warning/default coloring).
### Carry-over risk / top risk for slice 3 (and interim)
- **`TabbedCard` interim tab-bar rendering.** Until Applications (slice 4) and
Settings/Actions (slice 5) migrate, those pages still pass MUI `<Tab>` elements
as the `tabs` prop, which now render inside a shadcn `<TabsList>`. MUI `<Tab>`
outside an MUI `<Tabs>` does **not** throw (renders with a console warning) and
the page content still swaps via the page's external `value` state, but the
tab "active" highlight is cosmetic-only until those pages migrate. **Build /
lint / test are unaffected.** This is an expected interim state of the chained
model and resolves fully once slices 45 land. (Pages are intentionally not
edited in slice 2.)
- **Slice-2 PR budget.** Review churn ≈ 775 lines on the 10 rewritten components
(303 ins / 472 del; mostly MUI teardown) + 335 lines of new tests ≈ ~1,110
changed lines, **over the ≤400 budget**. The forecast authorizes sub-split 2a
(cards/buttons/dialogs) → 2b (tables/panels) on overrun. The parent delegated
the whole slice as one unit and owns the commit/PR, so this run delivered it
in one piece; the parent may split 2a/2b at PR time or take a size exception
(mirroring the slice-1 precedent). Functionally the slice is complete and all
gates are green.
- Overall change `applyState` remains **blocked** on missing domain specs (legacy
flat `spec.md`); does not block Slice 2 (done) but should be resolved before
`sdd-verify`/archive.
### Remaining tasks (Slices 38, 46 unchecked)
Slice 2 is complete (25/71 tasks). Next in dependency order: **Slice 3 — Backups
cluster + nav/IA** (uses slice-2 `Table`/`Badge`/`Tabs`/cards and lands the
`/backups` nav item + `/applications`→`/media` redirect; `DatabaseBackup` icon
confirmed available). See `tasks.md` Slices 38 for the verbatim unchecked list.
## Slice 3 — Backups cluster + navigation/IA — COMPLETE
All 8 Slice-3 task lines in `tasks.md` are now `- [x]`. The 5 Backups components
are MUI-free and the reconciled IA (`/backups` top-level nav item, Media at
`/media`, `/applications` → redirect) is live. Cumulative change task progress:
25 → **33/71** complete.
### Status context consumed
- `applyState` reported by the status engine: **blocked** (`blockedReasons`:
domain specs missing/partial; legacy flat `spec.md` present without domain
specs). Same planning-completeness gap as slices 12 — **not** a safety or
`actionContext` blocker.
- `actionContext`: `mode: repo-local`, `workspaceRoot: /home/user/Manage_01`,
`allowedEditRoots: ["/home/user/Manage_01"]`, `warnings: []` — safe.
- This run executed the explicitly delegated **Slice 3 (Backups cluster +
nav/IA)** scope per the parent acceptance contract. Slice-3 work is fully
specified in `tasks.md` (Slice 3) + `design.md` (§1 mapping table, §2.3
status→Badge variant map, §4 IA) and does not depend on the missing domain
specs. `instructions.apply`: "Implement only unchecked tasks from the tasks
artifact." → proceeded under the parent's explicit slice delegation.
- `artifactStore: openspec`; persisted task checkboxes updated in `tasks.md`
(Slice 3: 0 → 8 `[x]`).
### Completed tasks (persisted checkboxes updated)
- [x] Migrated `BackupAlertsTable.tsx` → shadcn `Table` family on a bordered
rounded surface + `Badge` (`severityVariant`: `critical`→`destructive`,
`warning`→`warning`) + `Button variant="outline" size="sm"` acknowledge
(hidden when `acknowledged`). Preserve `formatTimestamp`, props, callback.
- [x] Migrated `BackupJobsTable.tsx` → shadcn `Table` + `Badge`
(`statusVariant`: `success`→`success`, `failure`→`destructive`,
`in_progress`→`warning`, unknown→`secondary`). Latest-run status, `formatInterval`,
last-run + next-expected timing preserved.
- [x] Migrated `BackupRunsTable.tsx` → shadcn `Select` (status filter,
`onValueChange`) + `Badge` (`success`/`destructive`/`warning`) + `Table`.
`formatDuration`/`formatBytes`/`formatTimestamp` + filter logic preserved.
- [x] Migrated `BackupDashboardWidget.tsx` → shadcn `Card`/`CardHeader`/
`CardTitle`/`CardContent` + `Badge` (active_alerts>0 → `destructive`) +
`text-destructive` last-failed line. total_jobs / 24h success rate / active
alerts / last-failed-time + loading state preserved.
- [x] Migrated `BackupsPage.tsx` → shadcn `Tabs` (controlled `value`/
`onValueChange`, string values `jobs|runs|alerts`) with `TabsContent` per tab;
jobs/runs/alerts behavior, latest-runs map, and the acknowledge mutation
preserved. In-page `<h1>` kept to match the already-migrated `ObservabilityPage`.
- [x] Applied the §4 IA nav + route edits in `App.tsx`: imported `DatabaseBackup`
from lucide-react; nav `Media` retargeted `/applications` → `/media`; added
top-level `{ path: "/backups", label: "Backups", icon: DatabaseBackup }` (after
Files, before Users); in **both** route blocks `/media` is the canonical
`<Applications />` element and `/applications` is `<Navigate to="/media"
replace />` (mirrors the existing `/monitoring` → `/observability` redirect).
`/backups` already rendered `<BackupsPage />`.
- [x] Added component tests: `BackupRunsTable` (status→Badge variant mapping +
formatted duration/size), `BackupAlertsTable` (severity→variant, acknowledge
callback fires with the id, acknowledged hides the button),
`BackupDashboardWidget` (loading state, stats render, active_alerts>0 →
destructive Badge + last-failed line). 8 new tests across 3 files.
- [x] **Exit gate green:** `/backups` reachable from the sidebar; `/applications`
redirects to `/media`; 5 Backups components MUI-free; `npm run build` +
`npm run lint` + `npm test` + `npm run test:node` all pass.
### Files changed (tracked)
Modified (6):
- `frontend/src/components/BackupAlertsTable.tsx`
- `frontend/src/components/BackupJobsTable.tsx`
- `frontend/src/components/BackupRunsTable.tsx`
- `frontend/src/components/BackupDashboardWidget.tsx`
- `frontend/src/components/BackupsPage.tsx`
- `frontend/src/App.tsx` (icon import, `navItems`, both route blocks)
Added (new, 3 test files):
- `frontend/src/components/__tests__/BackupRunsTable.test.tsx`
- `frontend/src/components/__tests__/BackupAlertsTable.test.tsx`
- `frontend/src/components/__tests__/BackupDashboardWidget.test.tsx`
Persisted artifact updated:
- `openspec/changes/web-ui-rework/tasks.md` (Slice 3 checkboxes 0 → 8 `[x]`)
Untouched (no-unintended-edits respected): **no `frontend/src/pages/*` file was
edited this slice** — `git status --porcelain frontend/src/pages` is empty.
Applications.tsx itself was **not** edited (slice 4 owns it); only its route +
nav entry changed. No `components/ui/*` primitive was modified. The pre-existing
`/monitoring` → `/observability` redirect is intact.
### Commands run (validation) — all green
- `grep -rlE '@mui/(material|icons-material)' <5 backups files>` → **ALL 5
BACKUPS MUI-FREE**.
- `cd frontend && npm run build` → **PASS** (`tsc -b` + `vite build`; the
>500 kB chunk-size warning is pre-existing and unrelated).
- `cd frontend && npm run lint` → **PASS** (0 errors; the only 2 items are the
pre-existing `react-hooks/exhaustive-deps` **warnings** in
`UsersPage.impl.tsx`, out of Slice-3 scope).
- `cd frontend && npm test` → **PASS** (Vitest: **15 files, 30 tests**; +3 files
and +8 tests vs slice-2 baseline of 12 files / 22 tests + the badge smoke).
- `cd frontend && npm run test:node` → **PASS** (legacy node:test: 4 tests,
0 fail).
- IA verification (`grep` of `App.tsx`): nav has `/media` Media + `/backups`
Backups; both route blocks have `/media` canonical + `/applications` Navigate
redirect + `/backups` BackupsPage; `/monitoring`→`/observability` intact.
### Design decisions / deviations
1. **Backups nav icon: `DatabaseBackup`.** Verified at the pinned
`lucide-react@^1.14.0` (slice-1 confirmation re-checked this run via
`node -e "…require('lucide-react').DatabaseBackup"` → object). Semantic fit,
no fallback needed. Placed after Files, before Users per design §4.1.
2. **`/applications` is a replace-redirect to `/media`** in both route blocks
(OIDC-configured branch + unauthenticated branch), mirroring the existing
`/monitoring` → `/observability` redirect. `/media` is the canonical
`<Applications />` route. React Router v6 ranks routes by specificity, so
order is cosmetic; rendered `/media` before the `/applications` redirect to
match design §4.2's example snippet. The `Applications.tsx` component file
is **not** renamed (out of scope — design §4 non-goal).
3. **`BackupsPage` keeps an in-page `<h1>`** ("Backups") to match the
already-migrated `ObservabilityPage` (`<h1 className="text-2xl font-bold
tracking-tight">`). The shell's `<main>` already provides `p-4 md:p-6`, so
the old `Box sx={{ p: 3 }}` double-padding was dropped per design §2.2.
4. **Status / severity → Badge variant** uses the `chart-N` cue map per design
§2.3 (success=chart-2/success, failure/critical=chart-4/destructive,
in_progress=chart-3/warning, unknown=secondary). Verified via the
`data-variant` attribute assertions in the new tests (mirrors the slice-2
`SessionActivityPanel` test pattern).
5. **`BackupRunsTable` status filter** uses the shadcn `Select` (`onValueChange`
with string values) instead of MUI `Select`/`FormControl`/`InputLabel`/
`MenuItem`; an `aria-label="Status filter"` is on the trigger for a11y. The
`all|success|failure|in_progress` filter set is unchanged.
6. **`latestRuns` map left un-memoized** in `BackupsPage` (verbatim from the
pre-migration source) to preserve behavior exactly and avoid an extra
`useMemo`/exhaustive-deps surface.
### Slice boundary / PR
- Slice 3 is forecast "likely OK" at ≤400 lines as a single PR (tasks §per-slice
table). Actual review churn: ~6 modified components/App (~250 ins / ~270 del)
- 3 new test files (~150 lines) ≈ **~400 changed lines**, right at the budget.
The parent delegated the whole slice as one unit and owns the commit/PR; if
the parent prefers, the nav/route edit in `App.tsx` is a clean split point.
All gates are green.
### Remaining tasks (Slices 48, 38 unchecked)
Slice 3 is complete (33/71 tasks). Next in dependency order: **Slice 4 —
Dashboard + Applications surface** (depends on slices 2 + 3; reuses
`BackupDashboardWidget` from this slice and the reconciled `/media` route; the
`Applications.tsx` component itself is migrated here). The first unchecked items:
- [ ] Migrate `frontend/src/pages/Applications.tsx` …
- [ ] Migrate `frontend/src/pages/Dashboard.tsx` …
- … (see `tasks.md` Slices 48 for the verbatim unchecked list)
### Top risk for slice 4
- **`Dashboard.tsx` is the heaviest single page** (20 distinct MUI components:
Dialog/FormControl/FormControlLabel/Grid/Select/Switch/TextField/Stack/Grid…)
and composes the slice-3 `BackupDashboardWidget` plus `NowPlaying`. It is
forecast "medium" (~300450 lines) and may need a 4a (Applications, smaller)
→ 4b (Dashboard) sub-split on overrun. The reconciled `/media` route + the
shortcut deep-links must be re-pointed to `/media` (any Dashboard shortcut
still linking `/applications` will rely on the new redirect until re-pointed).
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.