From 91948919a8a7aeeb22e3d7c0a8ab825eafe966f1 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 18 Jun 2026 16:42:19 -0400 Subject: [PATCH] docs(20): capture phase context --- .../20-CONTEXT.md | 112 ++++++++++++++++++ .../20-DISCUSSION-LOG.md | 83 +++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 .planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md create mode 100644 .planning/phases/20-admin-member-editor-form-declutter/20-DISCUSSION-LOG.md diff --git a/.planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md b/.planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md new file mode 100644 index 0000000..fe9e36e --- /dev/null +++ b/.planning/phases/20-admin-member-editor-form-declutter/20-CONTEXT.md @@ -0,0 +1,112 @@ +# 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* diff --git a/.planning/phases/20-admin-member-editor-form-declutter/20-DISCUSSION-LOG.md b/.planning/phases/20-admin-member-editor-form-declutter/20-DISCUSSION-LOG.md new file mode 100644 index 0000000..7a684e5 --- /dev/null +++ b/.planning/phases/20-admin-member-editor-form-declutter/20-DISCUSSION-LOG.md @@ -0,0 +1,83 @@ +# Phase 20: Admin Member Editor & Form Declutter - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-06-18 +**Phase:** 20-admin-member-editor-form-declutter +**Areas discussed:** Editor field scope, Edit affordance, Editor layout & save model, Add-member declutter, Admin toggle safety + +--- + +## Editor field scope + +| Option | Description | Selected | +|--------|-------------|----------| +| Core 3 only | Display name + local login password + app password; defer admin toggle/color/OIDC/remove. | | +| Core 3 + admin toggle | Also flip `is_admin` from the editor, with a last-admin guard. | ✓ | + +**User's choice:** "The entire scope plus admin toggle" — core 3 fields plus the `is_admin` toggle. +**Notes:** Color, OIDC link/unlink, and remove-member stay deferred. Display name + admin toggle require one new within-`requireAdmin` route (no member-update route exists today). + +--- + +## Edit affordance + +| Option | Description | Selected | +|--------|-------------|----------| +| Whole-row tap + chevron | Tapping anywhere on the member row opens the editor; trailing chevron signals it. | ✓ | +| Name link + pencil button | Literal roadmap wording — name link + dedicated edit icon. | | + +**User's choice:** Whole-row tap + chevron. +**Notes:** Removes the per-row `Rotate`/`Add credential` + `Reset password` button cluster. + +--- + +## Editor layout & save model + +| Option | Description | Selected | +|--------|-------------|----------| +| One sheet, per-section save | Name+toggle save; "Set new password"; "Set app password" — each independent, maps 1:1 to an endpoint. | ✓ | +| One sheet, single combined Save | One Save writes every changed field; needs partial-failure handling for CalDAV validation. | | + +**User's choice:** One sheet, per-section save. +**Notes:** Avoids partial-failure ambiguity when CalDAV validation fails mid-save. Passwords stay write-only (blank = unchanged). + +--- + +## Add-member declutter + +| Option | Description | Selected | +|--------|-------------|----------| +| Open as a sheet | "Add member" opens a sheet — ideally the same Member sheet in create mode. | ✓ | +| Expand inline form | Button toggles the existing inline form visible/hidden in place. | | + +**User's choice:** Open as a sheet. +**Notes:** Prefer the compose_event-style one-component create-vs-edit pattern so the panel collapses to a clean list + one button. + +--- + +## Admin toggle safety + +| Option | Description | Selected | +|--------|-------------|----------| +| Block demoting last admin | Server rejects toggling `is_admin` off when they're the only admin; self-demotion only if another admin exists. | ✓ | +| Warn but allow | Confirm dialog when demoting the last admin/yourself, but permit it; rely on break-glass CLI. | | + +**User's choice:** Block demoting the last admin (server-enforced). +**Notes:** Phase 19 break-glass CLI/host command remains the true lockout-recovery path; no new role model. + +--- + +## Claude's Discretion + +- Exact verb/path/shape of the new member-profile update route (`displayName` + `is_admin`). +- Whether the Member editor and Add-member sheet are literally one component (mode prop) vs two siblings on a shared base. +- Whether the app-password section prefills the stored Fastmail email (read-only) or requires re-entry in edit mode. +- Chevron/edit icon choice for the row affordance. + +## Deferred Ideas + +- Editable member color (no stored per-member color — derived by row index). +- Admin-driven OIDC link/unlink (Phase 19 D-12: self-service only). +- Remove / delete member (destructive, cascade concerns).