docs(10): revise phase plan per plan-checker feedback (3 blockers, 2 warnings)
This commit is contained in:
@@ -20,6 +20,7 @@ must_haves:
|
||||
- "An admin sees an Admin nav entry, reaches /admin, can list members + credential status, rotate a member credential via the sheet, and pick the shared calendar"
|
||||
- "A non-admin never sees the Admin nav entry and is redirected from /admin to /calendar"
|
||||
- "A member with needsProviderSetup=true sees the self-service SetupBanner and can add their own credential via the same sheet (member-scoped)"
|
||||
- "After a successful credential save the SetupBanner clears (needsProviderSetup→false via ['me'] invalidation), there being no dismiss button"
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/routes/AdminPage.tsx"
|
||||
provides: "/admin page: Members section + Shared-Calendar picker, wired to /api/admin/*"
|
||||
@@ -108,8 +109,11 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
||||
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 3 (credential sheet: heading variants, member subtitle, password field type="password"/autocomplete="new-password"/never pre-filled, helper text + Fastmail app-password link target="_blank" rel="noopener noreferrer", "Validating against CalDAV…" Loader2 spinner, failure copy, Save/Cancel actions) + Surface 4 (self-service banner: KeyRound icon, copy, "Set up now" CTA, no X/dismiss) + the Copywriting Contract (exact strings) + Accessibility Contracts + Color/Typography/Spacing (all via var(--token), 44px touch targets)
|
||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/components/CredentialSheet.tsx` + §`apps/pwa/src/components/SetupBanner.tsx` (structural + mutation + style excerpts) + §Shared Patterns "CSS token inline style pattern"
|
||||
</read_first>
|
||||
<behavior>
|
||||
- SetupBanner dismissal (success-only): there is NO dismiss/X button per UI-SPEC Surface 4 — the ONLY way the banner clears is a successful credential save. After a successful save the CredentialSheet mutation's `onSuccess` invalidates the `['me']` query → /api/me refetches → `needsProviderSetup` becomes `false` → SetupBanner unmounts on the next rerender. A test/behavior assertion: given `needsProviderSetup=true` the banner renders; after a successful save (mocked) that flips /api/me to `needsProviderSetup=false`, the banner is no longer in the DOM. (No interaction other than success clears it.)
|
||||
</behavior>
|
||||
<action>
|
||||
Build `CredentialSheet.tsx` (shared by admin rotation AND self-service per D-07) following the SettingsSheet bottom-sheet pattern: props for mode (admin-rotate / admin-add / self-service), target member (admin) or current user (self-service), open/close. Render the heading variant per UI-SPEC Copywriting Contract ("Rotate Credential" / "Add Credential" / "Add your calendar credential"), the member-name subtitle, a `type="password" autoComplete="new-password"` field NEVER pre-filled, the helper text with the Fastmail app-password link (exact URL + copy from UI-SPEC, opens in new tab), the "Validating against CalDAV…" inline state (Loader2 size 16) during the mutation, the failure error copy on a CalDAV 400, and Cancel (ghost) + Save Credential (accent-filled) actions. Use a TanStack `useMutation` that calls `saveCredential` (admin) or `saveMyCredential` (self-service) and on success invalidates `['admin','members']` + `['me']` (so needsProviderSetup refreshes) and closes the sheet. Build `SetupBanner.tsx` following PermissionDeniedBanner: render only when `meQuery.data?.user.needsProviderSetup === true`, `role="status" aria-live="polite"`, KeyRound icon, the exact heading/body/CTA copy, "Set up now" opening the CredentialSheet in self-service mode; NO dismiss button (it clears when needsProviderSetup becomes false). All styling via `var(--token)`; every interactive element minWidth/minHeight 44px. Never log/echo the password.
|
||||
Build `CredentialSheet.tsx` (shared by admin rotation AND self-service per D-07) following the SettingsSheet bottom-sheet pattern: props for mode (admin-rotate / admin-add / self-service), target member (admin) or current user (self-service), open/close. Render the heading variant per UI-SPEC Copywriting Contract ("Rotate Credential" / "Add Credential" / "Add your calendar credential"), the member-name subtitle, a `type="password" autoComplete="new-password"` field NEVER pre-filled, the helper text with the Fastmail app-password link (exact URL + copy from UI-SPEC, opens in new tab), the "Validating against CalDAV…" inline state (Loader2 size 16) during the mutation, the failure error copy on a CalDAV 400, and Cancel (ghost) + Save Credential (accent-filled) actions. Use a TanStack `useMutation` that calls `saveCredential` (admin) or `saveMyCredential` (self-service) and on success invalidates `['admin','members']` + `['me']` (so needsProviderSetup refreshes and the SetupBanner clears) and closes the sheet. Build `SetupBanner.tsx` following PermissionDeniedBanner: render only when `meQuery.data?.user.needsProviderSetup === true`, `role="status" aria-live="polite"`, KeyRound icon, the exact heading/body/CTA copy, "Set up now" opening the CredentialSheet in self-service mode; NO dismiss button (it clears ONLY when needsProviderSetup becomes false after a successful save — the success-only dismissal behavior above). All styling via `var(--token)`; every interactive element minWidth/minHeight 44px. Never log/echo the password.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -8 && grep -l "autocomplete=\"new-password\"\|autoComplete=\"new-password\"" apps/pwa/src/components/CredentialSheet.tsx</automated>
|
||||
@@ -119,11 +123,12 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
||||
- The helper text contains the Fastmail app-password link with `target="_blank"` and `rel="noopener noreferrer"`.
|
||||
- The success mutation invalidates both `['admin','members']` and `['me']` (`grep -c "invalidateQueries" apps/pwa/src/components/CredentialSheet.tsx` >= 2).
|
||||
- `SetupBanner.tsx` renders conditionally on `needsProviderSetup`, uses `role="status"`/`aria-live`, and has NO dismiss/X button.
|
||||
- SetupBanner success-only dismissal holds: when /api/me reports `needsProviderSetup=false` (the state after a successful save invalidates `['me']`), the banner does not render — there is no code path that hides it other than the `needsProviderSetup` flag flipping to false.
|
||||
- Exact UI-SPEC Copywriting Contract strings are present (e.g. "Set up your calendar", "Validating against CalDAV…", "Save Credential").
|
||||
- No hard-coded color/spacing px except the 44px/48px touch-target minimums; values reference `var(--...)`.
|
||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
||||
</acceptance_criteria>
|
||||
<done>CredentialSheet (admin + self-service) and SetupBanner match the UI-SPEC contract, accessible, token-styled, typecheck clean.</done>
|
||||
<done>CredentialSheet (admin + self-service) and SetupBanner match the UI-SPEC contract, accessible, token-styled, success-only banner dismissal wired via ['me'] invalidation, typecheck clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="execute">
|
||||
@@ -134,12 +139,18 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
||||
- apps/pwa/src/routes/ListsIndex.tsx (page-level component analog for AdminPage: TanStack Query + sections layout)
|
||||
- apps/pwa/src/components/AppNav.tsx (NavLink + Lucide pattern lines 14–15, the DesktopNav section) + apps/pwa/src/components/BottomTabBar.tsx (tab pattern lines 27–98) — add the conditional Admin entry (ShieldCheck) to both
|
||||
- apps/pwa/e2e/calendar.spec.ts + layout.spec.ts + e2e/README.md (existing spec idioms, selectors, the dev-bypass admin user seeded in global-setup as id=1 is_admin=true from Plan 01)
|
||||
- .claude/skills/playwright-cli/SKILL.md (how to drive the global playwright-cli binary for the browser verification)
|
||||
- .claude/skills/playwright-cli/SKILL.md (how to drive the global playwright-cli binary for the supplementary browser verification)
|
||||
- .planning/phases/10-admin-role-settings/10-UI-SPEC.md Surface 1 (/admin page: AppNav persistent, ShieldCheck size 18, content maxWidth 640px centered desktop, var(--space-12) vertical padding, "Admin Settings" heading) + Surface 2 (Members section) + Surface 5 (Shared Calendar picker: radio group, "Currently shared" label, two-tap Save, empty state) + Copywriting Contract
|
||||
- .planning/phases/10-admin-role-settings/10-PATTERNS.md §`apps/pwa/src/routes/AdminPage.tsx` + §`apps/pwa/src/App.tsx` + §Shared Patterns "NavLink + Lucide icon"
|
||||
</read_first>
|
||||
<action>
|
||||
Build `AdminPage.tsx` (analog ListsIndex): "Admin Settings" heading (18px/600), a MEMBERS section listing members from `fetchAdminMembers` (avatar swatch + name + credential status badge per UI-SPEC Surface 2 + a "Rotate"/"Add credential" button opening CredentialSheet in admin mode for that member), and a SHARED CALENDAR section (Surface 5) using `fetchAdminCalendars`: an exclusive single-select radio group (D-06), the "Currently shared" label on the active one, a two-tap "Save" button (disabled until selection differs) calling `setSharedCalendar`, and the empty state ("No calendars synced yet") when none synced. Desktop: centered column maxWidth 640px. In `App.tsx`: add the `/admin` Route (D-02: new dedicated gated route) gated by `meQuery.data?.user.isAdmin ? <AdminPage/> : <Navigate to="/calendar" replace/>` — the client redirect is UX-only per D-03 (the server-side 403 from Plan 03 is the real boundary); add a loading gate so an in-flight meQuery doesn't flash-redirect (planner's call per 10-PATTERNS.md note); mount `<SetupBanner/>` above the calendar content (renders only on needsProviderSetup). In `AppNav.tsx` (desktop) and `BottomTabBar.tsx` (mobile): add an Admin entry (ShieldCheck icon, `aria-label="Admin settings"`) rendered ONLY when `meQuery.data?.user.isAdmin === true` (D-03 UX gating). Write `apps/pwa/e2e/admin.spec.ts`: with the dev-bypass admin user (seeded id=1 is_admin=true), assert the Admin nav entry is visible and /admin renders "Admin Settings" + the Members section. Add a non-admin assertion by route-mocking GET /api/me to `isAdmin:false` (per [[dev-data-user1-no-calendars]] route-mock idiom and the lists.spec page.route precedent) and asserting the Admin nav entry is absent and /admin redirects to /calendar. THEN run a playwright-cli browser check against the dev stack to confirm the guard + nav gating + sheet open behaviors interactively (use the playwright-cli skill — this replaces a human-verify checkpoint since it is desktop-Chromium-drivable per CLAUDE.md). All styling via `var(--token)`; 44px touch targets.
|
||||
Build `AdminPage.tsx` (analog ListsIndex): "Admin Settings" heading (18px/600), a MEMBERS section listing members from `fetchAdminMembers` (avatar swatch + name + credential status badge per UI-SPEC Surface 2 + a "Rotate"/"Add credential" button opening CredentialSheet in admin mode for that member), and a SHARED CALENDAR section (Surface 5) using `fetchAdminCalendars`: an exclusive single-select radio group (D-06), the "Currently shared" label on the active one, a two-tap "Save" button (disabled until selection differs) calling `setSharedCalendar`, and the empty state ("No calendars synced yet") when none synced. Desktop: centered column maxWidth 640px. In `App.tsx`: add the `/admin` Route (D-02: new dedicated gated route) gated by `meQuery.data?.user.isAdmin ? <AdminPage/> : <Navigate to="/calendar" replace/>` — the client redirect is UX-only per D-03 (the server-side 403 from Plan 03 is the real boundary); add a loading gate so an in-flight meQuery doesn't flash-redirect (planner's call per 10-PATTERNS.md note); mount `<SetupBanner/>` above the calendar content (renders only on needsProviderSetup). In `AppNav.tsx` (desktop) and `BottomTabBar.tsx` (mobile): add an Admin entry (ShieldCheck icon, `aria-label="Admin settings"`) rendered ONLY when `meQuery.data?.user.isAdmin === true` (D-03 UX gating).
|
||||
|
||||
Verification has two layers, kept distinct:
|
||||
1. AUTOMATED GATE (the verify command): write `apps/pwa/e2e/admin.spec.ts` and run `pnpm --filter @familysync/pwa test:e2e -- admin`. With the dev-bypass admin user (seeded id=1 is_admin=true), assert the Admin nav entry is visible and /admin renders "Admin Settings" + the Members section. Add a non-admin assertion by route-mocking GET /api/me to `isAdmin:false` (per [[dev-data-user1-no-calendars]] route-mock idiom and the lists.spec page.route precedent) and asserting the Admin nav entry is absent and /admin redirects to /calendar. THIS e2e SPEC IS THE GATE.
|
||||
2. SUPPLEMENTARY (not the gate): run an interactive `playwright-cli` check against the running dev stack at the `/admin` route to confirm the guard redirect + nav gating + CredentialSheet opens — drive the global `/usr/local/bin/playwright-cli` binary per `.claude/skills/playwright-cli/SKILL.md` (navigate to /admin as the dev-bypass admin, confirm "Admin Settings" + open the credential sheet; then with a route-mocked non-admin confirm the redirect to /calendar). Record the playwright-cli observations in the SUMMARY. This is an optional supplementary confirmation; if the dev stack is not up it does not block the plan — the e2e spec is the binding proof.
|
||||
|
||||
All styling via `var(--token)`; 44px touch targets.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit 2>&1 | tail -6 && pnpm --filter @familysync/pwa test:e2e -- admin 2>&1 | tail -20</automated>
|
||||
@@ -149,10 +160,10 @@ Output: Extended client.ts, AdminPage.tsx, CredentialSheet.tsx, SetupBanner.tsx,
|
||||
- `AppNav.tsx` and `BottomTabBar.tsx` render the Admin entry (ShieldCheck, `aria-label="Admin settings"`) ONLY when isAdmin is true (`grep -c "ShieldCheck" apps/pwa/src/components/AppNav.tsx apps/pwa/src/components/BottomTabBar.tsx` >= 2).
|
||||
- `AdminPage.tsx` renders "Admin Settings", a MEMBERS list, and a SHARED CALENDAR exclusive radio group with a two-tap Save and an empty state.
|
||||
- `apps/pwa/e2e/admin.spec.ts` asserts: admin sees the nav entry + reaches /admin; a non-admin (route-mocked isAdmin:false) does NOT see it and /admin redirects to /calendar.
|
||||
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes; the playwright-cli interactive check confirms the guard + nav gating (recorded in the SUMMARY).
|
||||
- The e2e spec (`pnpm --filter @familysync/pwa test:e2e -- admin`) passes and IS the gate; the playwright-cli interactive check is a supplementary confirmation (guard + nav gating + sheet open) recorded in the SUMMARY, not the binding proof.
|
||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` exits 0.
|
||||
</acceptance_criteria>
|
||||
<done>/admin route gated, AdminPage wired to the admin API, conditional nav entries + SetupBanner mounted, e2e + playwright-cli verification green.</done>
|
||||
<done>/admin route gated, AdminPage wired to the admin API, conditional nav entries + SetupBanner mounted, e2e gate green; playwright-cli supplementary check recorded in the SUMMARY.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -189,17 +200,19 @@ New symbols/files created by this plan (excluded from drift verification):
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` passes.
|
||||
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes (admin sees nav + /admin; non-admin redirected, no nav entry).
|
||||
- playwright-cli interactive check confirms the guard + nav gating + sheet open (recorded in SUMMARY).
|
||||
- `pnpm --filter @familysync/pwa test:e2e -- admin` passes (admin sees nav + /admin; non-admin redirected, no nav entry) — this is the binding gate.
|
||||
- playwright-cli interactive check confirms the guard + nav gating + sheet open (supplementary, recorded in SUMMARY).
|
||||
- Run the full CI fast-checks gate locally before declaring done (lint + typecheck + test + format:check + md:lint + PWA tests) per [[feedback-run-full-ci-gate-before-push]].
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Admin sees the Admin section, lists members + credential status, rotates a credential via the sheet, picks the shared calendar (Success Criteria 1, 2, 3).
|
||||
- Non-admin never sees the entry and is redirected from /admin (Success Criterion 1; client-side UX over the server 403).
|
||||
- needsProviderSetup member sees the SetupBanner and can self-serve their own credential (D-07).
|
||||
- needsProviderSetup member sees the SetupBanner and can self-serve their own credential; the banner clears on a successful save (no dismiss button) (D-07).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/10-admin-role-settings/10-04-SUMMARY.md` when done.
|
||||
</output>
|
||||
</content>
|
||||
</invoke>
|
||||
|
||||
Reference in New Issue
Block a user