Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.1 KiB
Phase 20: Admin Member Editor & Form Declutter - Context
Gathered: 2026-06-18 Status: Ready for planning
## Phase BoundaryRework 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 credentialbutton and the separateReset passwordbutton 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 DecisionsEditor 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_adminneeds one new route within the existingrequireAdminboundary — todaydisplayNameis only written at member-create (POST /members) and there is no member-update route. Recommended shape: a singlePATCH /api/admin/members/:id(orPOST) acceptingdisplayNameand/oris_admin; exact verb/shape is the planner's call.AdminMember(apps/pwa/src/api/client.ts:563) and theGET /membersselect (apps/api/src/routes/admin.ts:102) must surfaceisAdminfor the toggle's initial state.
Admin toggle safety
- D-03: Server blocks demoting the last admin. Toggling
is_adminoff 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 fromMemberRow. 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:
- Profile — display name input + admin toggle, with one Save (writes the new member-profile route; subject to D-03 guard).
- 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). - 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 inAdminPage.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
fastmailEmailexists on the credential. - Icon choice for the row chevron/edit affordance (lucide, consistent with existing
CheckCircle/AlertCircleusage).
<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 standaloneResetPasswordSheetbeing 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—AdminMembertype (:563), admin fetchers (fetchAdminMembers,fetchCreateMember,fetchAdminResetPassword,saveCredential); add the new member-profile fetcher +isAdminfield 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 samerequireAdmin.
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 (singleis_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(inAdminPage.tsx): password + confirm + mismatch validation logic — folds into the editor's "Set new password" section (D-06).useFocusTrap,useIsPhonehooks — 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 viashowToast(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/:idmounts onadminRouterbehindrequireAdmin(no new boundary). GET /membersselect +AdminMembertype gainisAdminso the editor's toggle has initial state.MemberRowbecomes a single tappable row (chevron affordance), dropping its action-button cluster. </code_context>
- "Same sheet, create vs edit mode" is explicitly modeled on the Fastmail
compose_eventpattern (one widget,idpresent = 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".
- 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