# Phase 20: Admin Member Editor & Form Declutter - Context
**Gathered:** 2026-06-18
**Status:** Ready for planning
## Phase Boundary
Rework the **admin Members panel** (`apps/pwa/src/routes/AdminPage.tsx`, "Members & Accounts" tab) so an admin edits all of a member's details from **one editor** instead of scattered per-row action buttons:
- Replace the per-row `Rotate` / `Add credential` button **and** the separate `Reset password` button with a single member editor opened from the row.
- The editor changes: **display name**, **local-login password**, the **Fastmail/CalDAV app password** (calendar credential), and the member's **admin flag (`is_admin`)** — using clear, non-jargon labels that **retire the "Rotate" term**.
- Collapse the always-open inline **Add member** form behind a single "Add member" trigger.
Primarily a client-side `AdminPage` + `CredentialSheet` rework over the existing `/api/admin` surface. **No new auth/authorization boundary** — everything stays behind `requireAdmin`. One small new *route* (member-profile update) is in scope; it is not a new boundary. Seeded by the gripe that "Rotate" for the app password is unintuitive.
**Out of scope (deferred):** editable member color, admin-driven OIDC link/unlink, member deletion/removal.
## Implementation Decisions
### Editor field scope
- **D-01:** The editor exposes **four** things: display name, local-login password, Fastmail/CalDAV app password, and the **admin toggle (`is_admin`)**. Color, OIDC link/unlink, and remove-member are explicitly deferred (see Deferred Ideas).
- **D-02:** **Editing display name + `is_admin` needs one new route** within the existing `requireAdmin` boundary — today `displayName` is only written at member-create (`POST /members`) and there is no member-update route. Recommended shape: a single `PATCH /api/admin/members/:id` (or `POST`) accepting `displayName` and/or `is_admin`; exact verb/shape is the planner's call. `AdminMember` (`apps/pwa/src/api/client.ts:563`) and the `GET /members` select (`apps/api/src/routes/admin.ts:102`) must surface `isAdmin` for the toggle's initial state.
### Admin toggle safety
- **D-03:** **Server blocks demoting the last admin.** Toggling `is_admin` off is rejected (409/422) when the target is the only remaining admin; self-demotion is permitted only when another admin exists. The client surfaces this as a clear inline error. The Phase 19 break-glass CLI/host command remains the true lockout-recovery path ([[19-CONTEXT]] D-13) — no new role/capability model.
### Edit affordance
- **D-04:** **Whole-row tap opens the editor**, with a trailing chevron / edit icon as the affordance signal. The current per-row action buttons (`Rotate`/`Add credential`, `Reset password`) are removed from `MemberRow`. Big mobile tap target; matches the low-friction, warm aesthetic.
### Editor layout & save model
- **D-05:** **One sheet, per-section save** — not a single combined Save. Sections:
1. **Profile** — display name input + admin toggle, with one Save (writes the new member-profile route; subject to D-03 guard).
2. **Set new password** — optional, **write-only** (blank = unchanged), with confirm; maps to existing `POST /members/:id/password` (`admin.ts:225`). Only shown for members with a local credential (`hasLocalCredential`).
3. **Set app password** — optional, **write-only**; collects Fastmail email + app password, **CalDAV-validated** before store; maps to existing `POST /credentials` (`admin.ts:279`).
Each section maps 1:1 to an endpoint, avoiding partial-failure ambiguity when CalDAV validation fails. Passwords are never prefilled/returned to the client (preserve T-10-15/T-10-16).
- **D-06:** **Retire "Rotate" copy** everywhere; use plain labels (e.g. "Set app password" / "Update calendar password"). The standalone `ResetPasswordSheet` (currently in `AdminPage.tsx`) is **folded into** the editor's "Set new password" section — no separate reset sheet remains.
### Add-member declutter
- **D-07:** **"Add member" opens a sheet**, not an inline-expanded form. Preferred: the **same Member sheet in a create mode** (compose_event-style — one component, create vs edit), so the panel collapses to a clean member list + a single "Add member" trigger. Create mode keeps today's fields (display name, username, initial password + confirm → `POST /members`).
### Claude's Discretion
- Exact new-route verb/path/shape for the member-profile update (D-02).
- Whether the Member editor and Add-member sheet are literally one component with a mode prop vs two siblings sharing a base — planner's call, but D-07 prefers unification.
- In edit mode, whether the app-password section prefills/display the stored Fastmail email (read-only) or requires re-entry — minor UX detail for planning; note the stored `fastmailEmail` exists on the credential.
- Icon choice for the row chevron/edit affordance (lucide, consistent with existing `CheckCircle`/`AlertCircle` usage).
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Phase definition
- `.planning/ROADMAP.md` §"Phase 20: Admin Member Editor & Form Declutter" — goal + the open-scope note this discussion resolved.
### Code being reworked
- `apps/pwa/src/routes/AdminPage.tsx` — the Members panel, `MemberRow`, the inline Add-member form, and the standalone `ResetPasswordSheet` being consolidated.
- `apps/pwa/src/components/CredentialSheet.tsx` — dialog/focus-trap/Escape + CalDAV-validation sheet to generalize into the Member editor (and Add-member create mode).
- `apps/pwa/src/api/client.ts` — `AdminMember` type (`:563`), admin fetchers (`fetchAdminMembers`, `fetchCreateMember`, `fetchAdminResetPassword`, `saveCredential`); add the new member-profile fetcher + `isAdmin` field here.
- `apps/api/src/routes/admin.ts` — existing endpoints: `GET /members` (`:102`), `POST /members` (`:143`), `POST /members/:id/password` (`:225`), `POST /credentials` (`:279`); add the member-profile update route here behind the same `requireAdmin`.
### Prior decisions that constrain this phase
- `.planning/phases/19-local-auth-no-oidc-mode/19-CONTEXT.md` — D-11 (password lifecycle = self-change + admin-reset, no email reset), D-12 (OIDC link is self-service only, deletes local credential), D-13 (single `is_admin`, break-glass = CLI/host, no role split).
## Existing Code Insights
### Reusable Assets
- **`CredentialSheet`**: full dialog scaffold (role=dialog, `aria-modal`, `useFocusTrap`, Escape-to-close, focus-return-to-trigger, phone bottom-sheet vs desktop modal, CalDAV-validating mutation). Generalize into the Member editor + Add-member create mode.
- **`ResetPasswordSheet`** (in `AdminPage.tsx`): password + confirm + mismatch validation logic — folds into the editor's "Set new password" section (D-06).
- **`useFocusTrap`, `useIsPhone`** hooks — reuse for the new sheet.
- **Existing endpoints** cover login-password reset and app-password set; only the member-profile (displayName + is_admin) write is new.
### Established Patterns
- Two-tab `AdminPage` ("Members & Accounts" / "Settings") with roving-tabindex tabs — keep; this phase only restructures the Members tab body.
- Mutations invalidate `['admin','members']` (+ `['me']` for credential changes) on success; success toast via `showToast` (D-08 pattern). Reuse for editor saves.
- Write-only password handling: never prefill, never log, `autoComplete="new-password"` (T-10-15/16).
### Integration Points
- New `PATCH/POST /api/admin/members/:id` mounts on `adminRouter` behind `requireAdmin` (no new boundary).
- `GET /members` select + `AdminMember` type gain `isAdmin` so the editor's toggle has initial state.
- `MemberRow` becomes a single tappable row (chevron affordance), dropping its action-button cluster.
## Specific Ideas
- "Same sheet, create vs edit mode" is explicitly modeled on the Fastmail `compose_event` pattern (one widget, `id` present = edit, absent = create) — apply that shape to the Member sheet.
- Labels must read for a non-technical household member: retire "Rotate"; prefer "Set app password" / "Set new password" / plain "Save".
## Deferred Ideas
- **Editable member color** — colors are currently derived by row index (`var(--color-member-N)`); there is no stored per-member color to edit. Would need schema + assignment UX. → backlog / future phase.
- **Admin-driven OIDC link/unlink** — Phase 19 D-12 makes OIDC linking a self-service action performed *as that user*, never by an admin. Admin-side link/unlink is a different security model. → out of scope.
- **Remove / delete member** — destructive, with cascade concerns (events, lists, credentials, last-admin). Not part of the gripe-seeded scope. → backlog / future phase.
### Reviewed Todos (not folded)
- "Gitea CI — full regression + Docker publish" (score 0.6) — stale keyword match (already delivered as Phase 8); unrelated to this UI phase.
- "PWA phone layout — BottomTabBar overlaps FAB + legend" (score 0.4) — already addressed in Phase 17; unrelated.
---
*Phase: 20-admin-member-editor-form-declutter*
*Context gathered: 2026-06-18*