---
phase: 20-admin-member-editor-form-declutter
plan: "02"
subsystem: pwa-api-client
tags: [api-client, types, fetcher, admin, tdd]
status: complete

dependency_graph:
  requires:
    - "20-01: PATCH /api/admin/members/:id route (Plan 20-01, parallel worktree)"
  provides:
    - "updateMemberProfile fetcher consumed by Plan 20-03 MemberEditorSheet"
    - "AdminMember.isAdmin field for editor toggle initial state"
  affects:
    - "apps/pwa/src/api/client.ts"
    - "apps/pwa/src/api/client.test.ts"

tech_stack:
  added: []
  patterns:
    - "SessionExpiredError sentinel (existing convention) extended to new fetcher"
    - "last-admin sentinel error (new: mirrors existing 'conflict' pattern at line 206)"
    - "TDD RED→GREEN cycle on client.ts behavior"

key_files:
  modified:
    - path: apps/pwa/src/api/client.ts
      change: "Added isAdmin: boolean to AdminMember interface; added updateMemberProfile fetcher"
    - path: apps/pwa/src/api/client.test.ts
      change: "Added 9 TDD tests: 8 for updateMemberProfile behavior + 1 for AdminMember.isAdmin shape"

decisions:
  - "Followed PATCH verb for the member-profile update (idiomatic REST, consistent with updateEvent at line 496)"
  - "last-admin sentinel maps both 409 and 422 — PATTERNS.md notes server may return either; both handled"
  - "isAdmin placed after color and before hasCredential in AdminMember — matches PATTERNS.md excerpt"

metrics:
  duration_minutes: 4
  completed_date: "2026-06-18"
  tasks_completed: 2
  files_modified: 2
---

# Phase 20 Plan 02: PWA API Client — updateMemberProfile Fetcher + AdminMember.isAdmin Summary

**One-liner:** Thin API client glue: `updateMemberProfile` PATCH fetcher with `last-admin` 409/422 sentinel + `isAdmin: boolean` on `AdminMember`, enabling the Plan 20-03 editor.

## What Was Built

Added two changes to `apps/pwa/src/api/client.ts`:

1. **`AdminMember.isAdmin: boolean`** — new required field on the `AdminMember` interface (after `color`, before `hasCredential`). Feeds the editor toggle's initial state once Plan 20-01 lands (the `GET /api/admin/members` route already returns it after that plan's changes). No consumer code changes needed; Plan 20-03 reads it directly.

2. **`updateMemberProfile(memberId, body)`** — exported `async function` that issues `PATCH /api/admin/members/${memberId}` with `credentials: 'include'`, `redirect: 'manual'`, `Content-Type: application/json`, and JSON-stringified body `{ displayName?, isAdmin? }`. Error mapping:
   - `opaqueredirect` or `401` → `SessionExpiredError` (existing re-auth flow convention)
   - `409` or `422` → `new Error('last-admin')` (D-03 sentinel; editor branches on this message)
   - other non-ok → generic `Error`
   - `200` → resolves `void`

## TDD Gate Compliance

| Gate | Commit | Notes |
|------|--------|-------|
| RED | `18da7e9` | 8 `updateMemberProfile` behavior tests + 1 `AdminMember.isAdmin` shape test — all fail with `updateMemberProfile is not a function`; 42 existing tests pass |
| GREEN | `5bcd818` | All 50 tests pass after implementation; eslint + prettier + tsc --noEmit exit 0 |
| REFACTOR | N/A | No refactor needed — the implementation was minimal and clean on the first pass |

## Task Summary

| Task | Name | Commit | Files |
|------|------|--------|-------|
| RED | Add failing tests for updateMemberProfile + AdminMember.isAdmin | `18da7e9` | `client.test.ts` |
| GREEN | Add updateMemberProfile fetcher + AdminMember.isAdmin | `5bcd818` | `client.ts`, `client.test.ts` |

## Verification

- `grep -n "updateMemberProfile" apps/pwa/src/api/client.ts` → line 249 (export), line 263 (error throw)
- `grep -n "PATCH" apps/pwa/src/api/client.ts` → line 254 (`method: 'PATCH'`)
- `grep -nE "isAdmin: boolean" apps/pwa/src/api/client.ts` → line 597 (AdminMember)
- `grep -n "last-admin" apps/pwa/src/api/client.ts` → line 262 (the sentinel throw)
- `pnpm --filter @familysync/pwa exec tsc --noEmit` → exit 0
- `pnpm --filter @familysync/pwa exec eslint src/api/client.ts src/api/client.test.ts` → exit 0
- `pnpm exec prettier --check apps/pwa/src/api/client.ts apps/pwa/src/api/client.test.ts` → exit 0
- 50/50 tests pass

## Deviations from Plan

None — plan executed exactly as written.

- The PATTERNS.md excerpt was followed verbatim for the function shape.
- The eslint `require-await` issue in the test file was caught and fixed during Task 2 CI gates (test function did not need `async` — removed it). Not a plan deviation; it was a CI gate finding during Task 2 as specified.

## Known Stubs

None. This plan delivers typed wire code only; no UI rendering or data display.

## Threat Flags

No new security-relevant surface introduced. `updateMemberProfile` reuses the existing session-expiry path (T-20-05 mitigated) and does not introduce new trust boundaries (T-20-06 accepted per plan threat model).

## Self-Check: PASSED

| Check | Result |
|-------|--------|
| `apps/pwa/src/api/client.ts` exists | FOUND |
| `apps/pwa/src/api/client.test.ts` exists | FOUND |
| `20-02-SUMMARY.md` exists | FOUND |
| RED commit `18da7e9` | FOUND |
| GREEN commit `5bcd818` | FOUND |
