Files
familysync/.planning/milestones/v1.1-phases/20-admin-member-editor-form-declutter/20-CONTEXT.md
T
2026-06-18 22:21:38 -04:00

9.1 KiB

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_refs>

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.tsAdminMember 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). </canonical_refs>

<code_context>

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. </code_context>
## 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