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
+73
View File
@@ -0,0 +1,73 @@
# Slice 8 — Message compose + WidgetConfigDialog mobile forms (worker output)
## Files changed
| File | Status | Lines |
|------|--------|-------|
| `frontend/src/pages/UsersPage.impl.tsx` | modified | +245 / -200 (extraction + re-indent) |
| `frontend/src/components/WidgetConfigDialog.tsx` | modified | +215 / -178 (extraction + re-indent) |
| `frontend/src/pages/__tests__/UsersPage.test.tsx` | modified | +33 / -0 |
| `frontend/src/components/__tests__/WidgetConfigDialog.test.tsx` | new | 57 lines |
**Diff stat total:** 484 insertions, 387 deletions across 3 tracked + 1 new file. The diff is large because the compose body and WidgetConfigDialog body were extracted into shared `const` variables (so both SheetForm and Dialog can consume them). The actual **behavioral delta** is ~80 lines of new code (SheetForm branches + dynamic props); the rest is structural re-indentation of existing, token-identical content.
**Over the 400-line budget.** The overrun is inherent to the extraction pattern: sharing a form body between Dialog and SheetForm requires lifting it into a const, which inflates the diff with movement. Both changes were prioritized per the task instruction ("prioritize the compose dialog… keep WidgetConfigDialog changes minimal but correct").
## What was implemented
### 8.1 — Message compose SheetForm (UsersPage.impl.tsx)
Below `md` (`isMobile === true`), the compose dialog renders inside a `<SheetForm>` instead of a `<Dialog>`:
- **Body extracted** into a `composeBody` const (Progress bar, error/success alerts, queue banner, selected-users info, subject input, formatting toolbar, HTML textarea, email preview iframe, attachments). Same content renders inside both SheetForm (mobile) and Dialog (desktop).
- **SheetForm wiring:** title="Message selected users", onSave=handleSend, onCancel=closeCompose, isPending=sendUserMessage.isPending, saveDisabled=!selectedDeliverableRows.length || !subject.trim(), saveLabel="Send message".
- **Send semantics preserved:** handleSend already calls setComposeOpen(false) on success (R4.5 satisfied).
- **Attachment UI preserved** inside the SheetForm body (iOS Safari upload deferred to Slice 10 manual pass per the task note).
- **Desktop (md+)**: the Dialog renders with the exact same composeBody + DialogHeader + DialogFooter. isComposeMobile (900px) fullscreen styling still applies for 768900px.
### 8.2 — WidgetConfigDialog SheetForm
Below `md`, the widget config dialog renders inside a `<SheetForm>` with **dynamic props based on the two-mode flow**:
- **List mode** (no draft): title="Dashboard widgets", onSave=()=>handleClose(false) (closes dialog), onCancel=()=>handleClose(false), saveLabel="Done". Both footer buttons close the dialog.
- **Draft mode** (add/edit): title="Edit widget" / "Add widget", onSave=saveDraft, onCancel=reset (back to list, NOT close), saveLabel="Save widget", isPending=saveWidget.isPending.
- **Draft inline Back/Save hidden on mobile** (`{!isMobile ? <Back/Save> : null}`) since the SheetForm footer provides Cancel=reset + Save=saveDraft.
- **Body extracted** into a `draftBody` const shared between both branches. List view (reorder/toggle/edit/delete + add-widget buttons) and draft view (Title/SortOrder/Enabled/config editor) are unchanged.
- **Desktop (md+)**: the Dialog renders with the same draftBody. The draft's inline Back/Save buttons are present (isMobile=false).
### 8.3 — Tests
**UsersPage.test.tsx:** +1 test in the slice-5 mobile describe block:
- "renders compose in a SheetForm below md with send button" — selects a user, opens compose, asserts title + Send button + Subject input are present.
**WidgetConfigDialog.test.tsx** (new): 2 tests:
- Desktop: renders Dialog with "Dashboard widgets" heading.
- Mobile: renders SheetForm with "Dashboard widgets" title + "Done" button.
## Validation
```
npm run lint → 0 errors, 2 pre-existing warnings (UsersPage.impl.tsx exhaustive-deps, pre-existing)
npm run build → ✓ built (tsc -b + vite)
npm run test → 28 files / 116 tests passed (was 113; +3 new)
```
## Deviations from design
1. **IIFE pattern for compose branch.** The compose dialog sits inside the component's main return. Extracting the body and branching required either an IIFE (`{(() => { ... })()}`) or a separate helper component. Used the IIFE to keep the compose logic inline with the component's state/handlers (it references 15+ local variables: subject, htmlBody, attachments, sendUserMessage, etc.). A helper component would need all of these as props, which is worse.
2. **WidgetConfigDialog dynamic SheetForm props.** The design said "reorder list and per-widget config render inside SheetForm." The two-mode flow (list → draft) doesn't map to SheetForm's single onSave/onCancel cleanly. Solved with conditional props: list mode = Done/close, draft mode = Save-widget/back-to-list. The "Done" button in list mode is slightly redundant with Cancel (both close), but it's functional and the footer is always present.
3. **Over 400-line budget.** The extraction pattern inflates the diff. Both changes were completed; the alternative (CSS-only `hidden md:block` on two separate copies of the form body) would duplicate ~200 lines of form JSX.
## skill_resolution
`none` — no project/user SKILL.md paths were injected, and no `.atl/skill-registry.md` was found.
## Residual risks
- **R4.5 dirty-state outside-click confirm** still not implemented at the SheetForm level. Same deferred concern as Slices 67. Flag for verify pass.
- **WidgetConfigDialog "Done" + Cancel redundancy.** In list mode, both footer buttons close the dialog. A single "Done" button would be cleaner but would require a SheetForm API change (hide Cancel). Non-blocking.
- **Diff over budget.** Flagging for parent decision: accept the extraction overhead, or request the IIFE pattern be replaced with CSS-only branching (which would duplicate form JSX).