Files
manage/.pi-tmp/slice8-review.md
T
Developer a8dfbd5dc6 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).
2026-06-26 20:11:02 +00:00

180 lines
8.5 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.
# Slice 8 Review — Message compose + WidgetConfigDialog mobile forms
**Change:** `mobile-responsive-parity` · **Slice:** 8 (R4.1, R4.2, R4.4)
**Reviewer mode:** fresh adversarial · **Date:** 2026-06-26
**Verdict: commit** (no blockers; two non-blocking suggestions)
---
## Verification commands
```
cd frontend && npm run lint → 0 errors, 2 warnings (PRE-EXISTING, confirmed via git stash)
cd frontend && npm run build → ✓ built (tsc -b + vite), 1977 modules
cd frontend && npm run test → 28 files, 116 tests passed
```
The two lint warnings (`react-hooks/exhaustive-deps` on `baseRows`/`rows` useMemo,
UsersPage.impl.tsx:150/189) exist on the committed Slice 7 tree and are unrelated
to this diff.
---
## 1. Desktop non-regression — CONFIRMED for BOTH components
### UsersPage compose (`UsersPage.impl.tsx`)
- The shared `composeBody` const (IIFE, lines ~820985) bundles exactly the same
children the desktop `DialogContent` rendered before: `Progress` (when pending)
followed by `<div className="flex flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">`
containing the error/success alerts, queue banner, recipient badges, subject input,
formatting toolbar, body textarea, preview iframe, and attachment UI.
- Desktop branch (lines ~10291072) renders `<DialogHeader>``{composeBody}`
`<DialogFooter>` with the same Cancel / `Send message` buttons, same `disabled`
condition (`sendUserMessage.isPending || !selectedDeliverableRows.length || !subject.trim()`),
and same `onClick={handleSend}`. Token-identical to the pre-slice output.
- **768900px range preserved:** the desktop branch still applies
`isComposeMobile` (`useComposeViewport("(max-width: 900px)")`, line 139) as the
fullscreen className on `DialogContent`. In that band `isMobile` (768px) is false
and `isComposeMobile` (900px) is true → Dialog renders fullscreen. Unchanged.
### WidgetConfigDialog (`WidgetConfigDialog.tsx`)
- `draftBody` (lines ~284470) is the shared const covering both the draft branch
and the list branch. Desktop path renders `<DialogHeader><DialogTitle>{dialogTitle}</DialogTitle></DialogHeader>`
then `{draftBody}`.
- `dialogTitle` (line ~459) reproduces the exact original ternary:
`draft ? (draft.id ? "Edit widget" : "Add widget") : "Dashboard widgets"`.
- The inline Back/Save buttons in the draft branch are gated by `{!isMobile ? (...) : null}`
(line ~332). On desktop `isMobile=false` → they render identically to before.
List branch content (sorted instance rows, reorder/enable/edit/delete actions,
Add-widget buttons, help text) is byte-for-byte the same JSX, only re-indented.
- Confirmed token-identical desktop output.
---
## 2. Compose SheetForm wiring — CONFIRMED
`UsersPage.impl.tsx` mobile branch (lines ~10061027):
- `title="Message selected users"`
- `onSave={handleSend}` ✓ — `handleSend` (line 365) does `await mutateAsync` then
`setComposeOpen(false)` + clears state → **R4.5 close-on-success satisfied**
- `onCancel={closeCompose}` ✓ — `closeCompose` (line 317) closes + `sendUserMessage.reset()`
- `isPending={sendUserMessage.isPending}`
- `saveDisabled={!selectedDeliverableRows.length || !subject.trim()}` ✓ (mirrors desktop)
- `saveLabel="Send message"`
- Attachment UI (Paperclip + remove badges) is inside `composeBody`, preserved ✓
---
## 3. WidgetConfigDialog two-mode SheetForm — CONFIRMED
Mobile branch (lines ~471488):
- `title={dialogTitle}` → "Dashboard widgets" (list) / "Add widget" | "Edit widget" (draft) ✓
- `onSave={draft ? saveDraft : () => handleClose(false)}` — list "Done" closes, draft saves ✓
- `onCancel={draft ? reset : () => handleClose(false)}`**draft Cancel = reset (back to list, NOT close)**, list Cancel closes ✓
- `saveLabel={draft ? "Save widget" : "Done"}`
- `isPending={draft ? saveWidget.isPending : false}`
- `onOpenChange={(next) => { if (!next) handleClose(next); }}`
- List↔draft↔save flow intact: `startAddBuiltIn`/`startAddService`/`startEdit` set
`draft` → footer/title reactive-swap to draft mode; `saveDraft` mutates then
`reset()` returns to list (sheet stays open); `reset` returns to list without closing ✓
- The "both close in list mode" redundancy (Done + Cancel both call `handleClose(false)`)
is functional and matches the documented intent ✓
---
## 4. Rules of Hooks — CONFIRMED clean
**WidgetConfigDialog:** all hooks (`useWidgetInstances`, `useServiceInstances`,
`useTasks`, `useSaveWidgetInstance`, `useDeleteWidgetInstance`, `useState`,
`useMemo`, `useIsMobile`) are called unconditionally at the top of the component
before the `if (isMobile) return <SheetForm>…` early return. `useIsMobile()` is
placed after `draftBinding` (a plain derived value, not a hook) — no ordering
violation. ESLint `react-hooks/rules-of-hooks` produced **0 errors**.
**UsersPage:** the compose branch uses an IIFE `{(() => { … })()}` that declares
`composeBody` as a JSX const (no hooks, no state) and returns either `<SheetForm>`
or `<Dialog>`. No hooks are called inside the IIFE; no state is introduced or leaked.
Clean.
---
## 5. IIFE pattern — CONFIRMED correct
The IIFE only constructs a local `composeBody` JSX expression and branches on the
already-computed `isMobile` boolean. It introduces no closures over hooks, performs
no side effects, and returns a single root element. It does not leak state. The only
cost is readability (a moderately large nested expression), which is acceptable.
---
## 6. Test quality — ADEQUATE (one suggestion)
- **UsersPage compose mobile test** (UsersPage.test.tsx:345376): real behavioral
assertion — selects a deliverable user via the mobile card checkbox, opens compose,
and asserts the SheetForm title ("Message selected users"), the "Send message"
footer button, and the Subject input render. Not a pure smoke test.
- **WidgetConfigDialog tests** (new file, 2 cases): desktop asserts the Dialog
heading "Dashboard widgets"; mobile asserts the SheetForm title + "Done" footer
button. These are **smoke-level only** — they do not exercise the draft-mode
footer ("Save widget"), the `reset`-back-to-list Cancel behavior, or the
list→draft→save round trip. See Suggestion S1.
All 3 new tests pass; AC8 (Vitest case per touched component at <768px and ≥768px)
is satisfied.
---
## 7. Diff size — CONFIRMED mostly re-indentation
`git diff --stat`: 472 insertions / 391 deletions across 3 files (~863 changed lines).
The actual behavioral delta is small and bounded:
- compose mobile `<SheetForm>` branch + `composeBody` extraction guard: ~25 lines
- WidgetConfigDialog mobile `<SheetForm>` branch + `draftBody` extraction + `!isMobile`
button guard + `dialogTitle`/`useIsMobile` lines: ~30 lines
- New + updated tests: ~65 lines
The remaining ~740 lines are extraction/re-indentation of unchanged JSX into the
shared consts, consistent with the task brief. No scope creep: no backend, no other
pages, no new dependencies.
---
## Suggestions (non-blocking)
**S1 — WidgetConfigDialog mobile draft-mode test.** Add one mobile test that opens
the dialog, taps an "Add widget" button, and asserts the footer swaps to "Save widget"
and that Cancel returns to the list view (title reverts to "Dashboard widgets")
without closing the sheet. This would cover the most error-prone part of the two-mode
wiring and is currently untested.
**S2 — R4.5 dirty-state outside-click confirm (cross-slice, not slice-8).**
`SheetForm` does not implement the spec'd "do not close on outside-click while the
form is dirty" guard; Radix `Sheet` dismisses the overlay by default, calling
`onOpenChange(false)`. This is a property of the shared primitive landed in Slice 1
and inherited by Slices 6, 7, and 8 — not a regression introduced here. Flagging as
a residual risk to be addressed when the SheetForm primitive is revisited (or accept
the deviation explicitly in the verify report).
---
## Residual risks / repo hygiene
- `swap-pane` and `.pi-tmp/` are untracked and unrelated to this slice; ensure only
the three intended files (`WidgetConfigDialog.tsx`, `UsersPage.impl.tsx`,
`UsersPage.test.tsx`) plus the new `components/__tests__/WidgetConfigDialog.test.tsx`
are staged for the Slice 8 commit.
- No staged files currently (`git diff --cached` empty). Good.
---
## Conclusion
Desktop output is token-identical for both components; mobile SheetForm wiring is
correct for compose (single mode) and WidgetConfigDialog (two-mode list/draft);
Rules of Hooks and the IIFE are clean; lint/build/test are green. **Verdict: commit.**