--- phase: 20-admin-member-editor-form-declutter reviewed: 2026-06-18T14:30:00Z depth: deep files_reviewed: 5 files_reviewed_list: - apps/api/src/routes/admin.ts - apps/api/tests/routes/admin.test.ts - apps/pwa/src/api/client.ts - apps/pwa/src/components/MemberEditorSheet.tsx - apps/pwa/src/routes/AdminPage.tsx findings: critical: 0 warning: 0 info: 0 total: 0 status: clean --- # Phase 20: Code Review Report (Deep Re-Review — Iteration 2) **Reviewed:** 2026-06-18 **Depth:** deep (cross-file, call-chain, state-machine analysis) **Files Reviewed:** 5 **Status:** clean ## Summary All 11 findings from the prior pass (2 Critical, 6 Warning, 3 Info) are genuinely resolved — not superficially patched. Verification traces are below. One new Info-level issue was introduced by the no-op guard fix: the "Profile saved." toast fires even when the admin clicks Save without changing anything, because `mutationFn` returns early (no network call) but `onSuccess` still runs unconditionally. --- ## Prior Finding Verification ### CR-01 — Last-admin guard now atomic: RESOLVED `apps/api/src/routes/admin.ts:258–286` The guard and UPDATE are wrapped in a single `db.transaction()` call. Inside the transaction, the target row is re-read with a plain (non-locking) SELECT. The locking read is then a raw `tx.execute(sql\`SELECT COUNT(*) AS count FROM ... WHERE is_admin = true FOR UPDATE\`)`. Under InnoDB REPEATABLE READ (MariaDB default), `FOR UPDATE` acquires exclusive row locks on all qualifying rows, serialising concurrent demotion transactions: the second PATCH blocks until the first commits, then re-reads a count of 1 and trips the guard. The `tx.execute()` call uses the transaction's dedicated connection (confirmed via drizzle-orm 0.45.2 `mysql2/session.js`: the transaction callback receives a `MySql2Transaction` whose session holds the connection obtained by `pool.getConnection()` — the same connection that issued `BEGIN`). The FOR UPDATE lock is therefore in-scope for the transaction. The COUNT result is destructured as `[[{ count }]]` from the raw execute result `[RowDataPacket[], FieldPacket[]]`. The cast is correct. `Number(count)` safely handles both `number` and `string` returns from MariaDB. The 409 response shape `{ error: 'Cannot remove the last admin' }` is unchanged. The client (`client.ts:262`) maps 409 → `throw new Error('last-admin')`, and the sheet's `onError` checks `msg === 'last-admin'`. The chain is intact. Test C and Test D exercise the single-request guard paths and still pass. No concurrent-scenario test exists, but the fix is structurally correct and cannot be unit-tested against a single in-process MariaDB without intentional sleep-based race staging. --- ### CR-02 — Stale editorMember: RESOLVED `apps/pwa/src/routes/AdminPage.tsx:87, 122–127` `AdminPage` now stores only `editorMemberId: number | null` (line 87) and derives `editorMember` as a computed value on every render: ```typescript const editorMember = editorMemberId !== null ? (membersQuery.data?.members.find((m) => m.id === editorMemberId) ?? null) : null; ``` `openEditorForMember` calls `setEditorMemberId(member.id)` (line 254). After `profileMutation.onSuccess` invalidates `['admin', 'members']` and the query refetches, `editorMember` is rederived from fresh data on the next render. The `useEffect` in `MemberEditorSheet` (line 235–239) depends on `[member?.id, member?.displayName, member?.isAdmin]` and re-syncs form state immediately. The stale-snapshot overwrite path is closed. --- ### WR-01 — Profile mutation sends diff-only payload: RESOLVED `apps/pwa/src/components/MemberEditorSheet.tsx:261–275` `mutationFn` now builds a partial payload: `displayName` is added only when `trimmed !== (member.displayName ?? '')`, and `isAdmin` only when `isAdmin !== member.isAdmin`. An admin toggling only the admin flag on a null-displayName member sends `{ isAdmin: true/false }` with no `displayName` field — the Zod schema accepts this (both optional, refine requires at least one). The Save button remains enabled as long as `displayName.trim().length > 0` (or the existing displayName is non-null and unchanged). Toggle-only saves on null-displayName members are now unblocked. --- ### WR-02 — Error revert uses explicit value: RESOLVED `apps/pwa/src/components/MemberEditorSheet.tsx:290` `setIsAdmin(member!.isAdmin)` replaces the accidental `?? true` default. The `member!` non-null assertion is safe here: `mutationFn` at line 262 throws `Error('no-member')` before any API call when `member` is undefined, so the 409 error path can only be reached with a non-null `member`. The revert is now semantically explicit. --- ### WR-03 — Phone bottom-sheet overflow: RESOLVED `apps/pwa/src/components/MemberEditorSheet.tsx:395–413` The phone branch of `sheetStyle` now has `maxHeight: '90dvh'` and `overflowY: 'auto'` (lines 404–405). All three sections scroll within the 90dvh cap on short phones. --- ### WR-04 — handleClose stale closure: RESOLVED `apps/pwa/src/components/MemberEditorSheet.tsx:214–232` `handleClose` dependency array is now `[onClose, triggerRef]` — it no longer captures `member?.displayName` or `member?.isAdmin`. Only ephemeral fields (password inputs, error states, create-mode fields) are reset in `handleClose`. Member-derived fields (`displayName`, `isAdmin`) are owned exclusively by the `useEffect` at lines 235–239, which fires whenever the live `member` prop changes. Cancel after a successful save now resets to the saved (fresh) values, not the pre-save snapshot. --- ### WR-05 — Admin toggle aria-describedby: RESOLVED `apps/pwa/src/components/MemberEditorSheet.tsx:544` The admin toggle `