Phase 20: Admin Member Editor & Form Declutter #25
@@ -0,0 +1,112 @@
|
|||||||
|
# Phase 20: Admin Member Editor & Form Declutter - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-06-18
|
||||||
|
**Status:** Ready for planning
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## 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.
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## 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).
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<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.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).
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## 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".
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 20-admin-member-editor-form-declutter*
|
||||||
|
*Context gathered: 2026-06-18*
|
||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user