Cleanup: delete dead top-level pages + update docs (Slice 11)

Delete the old top-level page files whose content was migrated into
service-page tabs in slices 5-9:
- pages/Media.tsx, Applications.tsx (-> MediaTab)
- pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab)
- pages/Actions.tsx (-> ActionsTab)
- pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs)
- components/BackupsPage.tsx (-> JobsTab)
- components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs)
- hooks/useUsers.ts (orphaned after Users page deletion)
- the corresponding page test files (Media, FileBrowser, Applications,
  Actions, UsersPage) that tested the deleted pages directly.

The service-tab components are the live implementations; ServicePage
renders them. No live code references the deleted files.

Docs: append an Information Architecture section to REQUIREMENTS.md
documenting the services-as-hub model (nav shape, service-page tabs,
service type registry, Users->Authentik, Observability split, legacy
route 404s, empty state). Add a CHANGELOG entry under [Unreleased].

92 frontend tests pass (was 112; -20 deleted page tests); 271 backend
tests pass; lint/build green.

Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
This commit is contained in:
Developer
2026-06-26 20:11:02 +00:00
parent ec57eff59a
commit a8dfbd5dc6
45 changed files with 3441 additions and 4711 deletions
+95
View File
@@ -0,0 +1,95 @@
# Slice 5 — Users + Backups tables mobile layout (worker output)
## Files changed
| File | Status | Lines |
|------|--------|------|
| `frontend/src/hooks/useIsMobile.ts` | modified | +6 / -2 |
| `frontend/src/components/BackupAlertsTable.tsx` | modified | +45 / -0 |
| `frontend/src/components/BackupJobsTable.tsx` | modified | +48 / -0 |
| `frontend/src/components/BackupRunsTable.tsx` | modified | +38 / -1 |
| `frontend/src/components/__tests__/BackupAlertsTable.test.tsx` | modified | +39 / -1 |
| `frontend/src/components/__tests__/BackupRunsTable.test.tsx` | modified | +24 / -1 |
| `frontend/src/pages/UsersPage.impl.tsx` | modified | +58 / -4 |
| `frontend/src/pages/__tests__/UsersPage.test.tsx` | modified | +33 / -0 |
**Total: ~317 changed lines** (317 insertions, 10 deletions). Under the 400-line budget.
## What was implemented
### useIsMobile hardening (cross-cutting fix)
The hook now guards `typeof window.matchMedia === "function"` in both the `useState` initializer and the `useEffect`. Previously, jsdom environments without a matchMedia stub (the Backups component tests) would crash. This is a 6-line defensive fix matching the pattern the old UsersPage local hook already used.
### 5.1 — UsersPage card
Below `md`, the user table renders as `<MobileCardRow>` cards:
| Field | Key | Rationale |
|-------|-----|-----------|
| **Display name** (primary) | `name` | `userLabel(row)` — the primary identifier |
| Username | `username` | Falls back to `jellyfin_id` when username equals display_name |
| Activity | `activity` | `<Badge variant={activityBadgeVariant(...)}>` — visual at-a-glance status |
| Email | `email` | Falls back to "—" when absent |
**Selection wiring:** The checkbox renders in the `actions` slot of each card. `onClick={(e) => e.stopPropagation()}` prevents the card body tap (which opens the drawer via `onRowClick`) from also toggling selection. The checkbox uses the existing `toggleUserSelected(row.jellyfin_id)` handler and the `selectedIdSet` state — selection round-trips correctly. The checkbox has `className="mobile-touch-target"` for 44px min hit area.
**Drawer open:** `onRowClick={(r) => setSearchParams({ user: r.jellyfin_id })}` — same handler as the desktop table row click.
**Compose dialog:** The local `useIsMobile` (900px) was renamed to `useComposeViewport` to avoid collision with the shared 768px hook. The compose dialog still uses `isComposeMobile` (900px) for its fullScreen behavior. Compose is otherwise untouched (slice 8 scope).
### 5.2 — Backups cards (3 components)
**BackupAlertsTable** — primary = `alert.message`; fields = severity (Badge), type, created. Acknowledge button in actions slot (shortened to "Ack" for mobile space).
**BackupJobsTable** — primary = `job.name`; fields = source, schedule interval, last status (Badge). Uses an intermediate `JobCardRow` type to compose job + latest run status into a single row object for the card.
**BackupRunsTable** — primary = `run.job_id`; fields = status (Badge), duration, size, started. The status filter `<Select>` renders ABOVE both the card and table layouts (unchanged).
### 5.3 — Tests (4 new tests, 102 total)
- BackupAlertsTable: 2 new (mobile card render, acknowledge action on card)
- BackupRunsTable: 1 new (mobile card render with job_id primary)
- UsersPage: 1 new (mobile cards with display name + activity labels)
## Validation
```
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx, unrelated)
npm run build → ✓ built (tsc -b + vite)
npm run test → 25 files / 102 tests passed (was 98; +4 new)
```
## Mobile field lists (per component)
| Component | Primary | Fields | Rationale |
|-----------|---------|--------|-----------|
| UsersPage | `userLabel(row)` | username, activity (Badge), email | Identity + at-a-glance status + contact info |
| BackupAlertsTable | `alert.message` | severity (Badge), alert_type, created_at | Descriptive text first; severity/type/date for triage |
| BackupJobsTable | `job.name` | source, schedule, last status (Badge) | Job identity + config + health |
| BackupRunsTable | `run.job_id` | status (Badge), duration, size, started | Run identity + outcome + timing |
## Selection wiring on UsersPage cards
The checkbox is rendered in the `MobileCardRow` `actions` slot (right-aligned). `onClick={(e) => e.stopPropagation()}` prevents the card's `onRowClick` (drawer open) from firing when the checkbox is tapped. The checkbox calls `toggleUserSelected(r.jellyfin_id)`, which is the same handler used by the desktop table. The `selectedIdSet` (derived from `selectedUserIds` state) drives `checked` and updates reactively. Multi-select works correctly on mobile.
## Deviations from design
1. **`useIsMobile` hardening.** The shared hook from Slice 1 crashed in jsdom test environments that don't stub `matchMedia` (the Backups component tests). Added a `typeof window.matchMedia === "function"` guard to both the `useState` initializer and the `useEffect`. This matches the defensive pattern the old UsersPage local hook already used and prevents ALL consumers from needing a matchMedia stub for desktop behavior.
2. **`JobCardRow` intermediate type in BackupJobsTable.** The card needs both `BackupJob` and its latest run status. Rather than passing a tuple or doing lookups inside the render function, I compose a small `JobCardRow` interface (`{ job, status, run_started }`) and map jobs to it before passing to `MobileCardRow`.
3. **Compose hook rename.** Renamed the file-local `useIsMobile` (900px) to `useComposeViewport` to avoid collision with the imported shared `useIsMobile` (768px). No behavior change.
## skill_resolution
`none` — no project/user SKILL.md paths were injected, and no `.atl/skill-registry.md` was found.
## Residual risks
- **Nested checkbox inside button on UsersPage cards.** When `onRowClick` is set, `MobileCardRow` renders the card as a `<button>`. The checkbox (a Radix Checkbox, which renders a `<button>`) is inside it via the `actions` slot. This is technically invalid HTML (interactive content nested in button), but browsers handle it correctly: `stopPropagation` on the checkbox's `onClick` prevents the card's click handler. The existing desktop table uses the same pattern (`onClick={(event) => event.stopPropagation()}` on the checkbox inside a clickable `TableRow`). Acceptable.
- **No `BackupJobsTable.test.tsx` mobile test.** There is no existing `BackupJobsTable.test.tsx` file in the test directory, so I didn't create one (out of scope to add a new test file for a component that previously had no dedicated test). The component is exercised via integration in `BackupsPage.test.tsx`. Low risk.
## Review findings
No blockers identified during self-review. All validation commands green. No staged files.