Files
familysync/.planning/phases/11-per-event-reminders/11-04-SUMMARY.md
T
Lucas Berger 8cd4c0e2c2 docs(11-04): complete reminder picker plan — SUMMARY, STATE, ROADMAP, REQUIREMENTS
- 11-04-SUMMARY.md: allDay-aware select, edit pre-population, Custom-kept limitation, playwright smoke results
- STATE.md: advance plan counter, add key decisions, record session
- ROADMAP.md: phase 11 now Complete (4/4 summaries)
- REQUIREMENTS.md: CAL-13 + CAL-14 marked complete
2026-06-14 06:58:18 -04:00

192 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 11-per-event-reminders
plan: "04"
subsystem: ui
tags: [reminder, picker, EventForm, allDay, VALARM, client-types, playwright, cal-13, cal-14]
dependency_graph:
requires:
- "Plan 11-03 (CalendarOccurrence.reminderLeadMinutes, GET /api/events surfaces the field, eventFieldsSchema accepts reminderLeadMinutes)"
provides:
- "Reminder <select id=\"event-reminder\"> in EventForm with allDay-aware preset swap"
- "default None, reset-on-allDay-toggle behavior"
- "edit-mode pre-population (null→None / preset→option / off-list→synthetic / absolute-multi→Custom-kept)"
- "payload mapping: None→null, preset→integer, Custom-kept (unchanged)→field omitted (D-08 preserve)"
- "reminderLeadMinutes on CreateEventPayload + CalendarOccurrence in client.ts"
- "playwright-cli smoke: picker swap + edit-mode load assertions"
affects:
- "Plan 11-02 (reminder scheduler reads reminderLeadMinutes; client picker is its UI surface)"
- "Future plans touching EventForm or CreateEventPayload"
tech_stack:
added: []
patterns:
- "allDay-conditional preset swap: render two option sets from the same reminderValue state; swap on allDay change + reset to __none__"
- "Synthetic off-list option: append a computed <option> when occurrence value matches no preset, humanized label"
- "__custom__ sentinel: disabled read-only option for absolute/multi-VALARM events; omit reminderLeadMinutes from payload entirely when still __custom__"
- "Payload mapping: __none__ → null (clear), numeric string → parseInt (lead), __custom__ unchanged → field absent (D-08)"
- "Edit-mode classification on occurrence.reminderLeadMinutes: null→__none__, preset-match→preset, off-list positive→synthetic, (no pure multi-VALARM signal at this layer → null→__none__ fallback)"
key_files:
created: []
modified:
- apps/pwa/src/api/client.ts
- apps/pwa/src/components/EventForm.tsx
- apps/pwa/src/components/EventForm.test.tsx
- apps/pwa/src/components/EventDetailPopover.test.tsx
key_decisions:
- "D-CLIENT-TYPES: reminderLeadMinutes on CalendarOccurrence is required (number|null); on CreateEventPayload it is optional (?:number|null) — absent means no-change (D-08)"
- "D-PAYLOAD-ABSENT: __custom__ unchanged → field omitted from payload entirely; server's Object.prototype.hasOwnProperty.call check then preserves the existing VALARM (D-08)"
- "D-NULL-FALLBACK: occurrence.reminderLeadMinutes===null mapped to None; the occurrence type cannot distinguish absolute/multi-VALARM from genuine no-reminder (both come back null) — relied on server-side preserve-on-absent instead"
- "D-OFFLIST-HUMANIZE: off-list single-alarm positive values rendered as synthetic humanized option (minutes<60→'N min before', >=60→'N hours before')"
- "D-RESET-ON-TOGGLE: allDay toggle always resets reminderValue to __none__; no carry-over between preset sets"
requirements-completed: [CAL-13, CAL-14]
duration: "~60min (Tasks 1+2) + playwright smoke (Task 3)"
completed: "2026-06-14"
---
# Phase 11 Plan 04: Reminder Picker in EventForm Summary
**allDay-aware reminder `<select>` in EventForm with edit-mode pre-population, Custom-kept preserve path, and payload mapping (null/integer/absent) wired to CreateEventPayload**
## Performance
- **Duration:** ~60 min (Tasks 1+2 implementation) + playwright-cli smoke (Task 3)
- **Started:** 2026-06-14
- **Completed:** 2026-06-14
- **Tasks:** 3 (Tasks 1+2 autonomous; Task 3 checkpoint:human-verify — APPROVED)
- **Files modified:** 4 (client.ts, EventForm.tsx, EventForm.test.tsx, EventDetailPopover.test.tsx) + 7 prettier-only (style commit)
## Accomplishments
- `reminderLeadMinutes: number | null` added to `CalendarOccurrence`; `reminderLeadMinutes?: number | null` added to `CreateEventPayload` — four-state contract (absent/null/0/positive) mirrors the server schema (D-08)
- Reminder `<select id="event-reminder">` inserted after the Recurrence picker in EventForm: timed presets (None / 5 / 10 / 15 / 30 / 60 / 120 / 24h / 48h) when `!allDay`; day-granularity presets (None / Same day / 1d / 2d / 1wk) when `allDay`; default None; allDay toggle resets to None (no carry-over)
- Edit-mode pre-population from `occurrence.reminderLeadMinutes`: null→None; preset-match→matching option; off-list positive→synthetic humanized option; unchanged `__custom__` sentinel→field omitted from payload (D-08 preserve path)
- playwright-cli smoke passed all 5 assertions (A1A4b) against the dev stack
## Task Commits
1. **Task 1: Client types — reminderLeadMinutes on CreateEventPayload + CalendarOccurrence**`2c30afe` (feat)
2. **Task 2: Reminder picker in EventForm (swap, default None, reset-on-toggle, edit pre-population, Custom-kept, payload mapping)**`fe549ef` (feat)
3. **Task 3: playwright-cli smoke** — checkpoint:human-verify, APPROVED (no code commit; smoke screenshot at `.playwright-cli/page-2026-06-14T02-46-54-629Z.png`)
4. **Style fix: prettier on phase-11 modified files**`b9b3191` (style)
## Files Created/Modified
- `apps/pwa/src/api/client.ts``reminderLeadMinutes` added to `CalendarOccurrence` (required) and `CreateEventPayload` (optional)
- `apps/pwa/src/components/EventForm.tsx` — reminder picker block, allDay-aware option swap, reset-on-toggle, edit pre-population, Custom-kept sentinel, payload mapping
- `apps/pwa/src/components/EventForm.test.tsx` — component tests: default None, allDay swap + reset, edit pre-population (30→"30 minutes before", 1440 all-day→"1 day before (9 AM)", off-list 45→synthetic), payload mapping (None→null, preset→integer, Custom-kept→field absent)
- `apps/pwa/src/components/EventDetailPopover.test.tsx` — updated to cover CalendarOccurrence reminderLeadMinutes shape
## Picker Value → Payload Mapping
| Picker state | reminderValue | Payload field |
|---|---|---|
| None selected | `__none__` | `reminderLeadMinutes: null` (explicit clear) |
| Preset selected | `"30"` (string) | `reminderLeadMinutes: 30` (parsed integer) |
| Synthetic off-list | `"45"` (string) | `reminderLeadMinutes: 45` (parsed integer) |
| Custom-kept (unchanged) | `__custom__` | field **omitted** (server preserves original VALARM, D-08) |
| Same day all-day | `"0"` | `reminderLeadMinutes: 0` |
## Edit-Mode Classification
On mount (or when occurrence loads), `occurrence.reminderLeadMinutes` is classified:
| Value | Classification | Picker result |
|---|---|---|
| `null` | None / no reminder | Select `__none__` |
| Matches a preset in the current allDay set | Preset match | Select that option value |
| Positive integer not in preset set | Off-list single | Append synthetic humanized `<option>` and select it |
| (Absolute DATE-TIME or multiple VALARMs) | Falls back to null via expand.ts | Select `__none__` (see Known Limitation below) |
## Playwright Smoke Results (Task 3)
Assertions verified via playwright-cli against dev stack (DEV_AUTH_BYPASS=true, dev user id 1):
| Assertion | Result |
|---|---|
| A1: `#event-reminder` defaults to None | PASS |
| A2: timed presets visible (not all-day) | PASS |
| A3: allDay toggle swaps to day-granularity presets + resets to None | PASS |
| A4a: edit mock reminderLeadMinutes=30 → "30 minutes before" | PASS |
| A4b: edit mock reminderLeadMinutes=1440 all-day → "1 day before (9 AM)" | PASS |
Screenshot: `.playwright-cli/page-2026-06-14T02-46-54-629Z.png` (all-day edit form showing "1 day before (9 AM)")
## Known Limitation: Custom-Kept vs No-Reminder
The occurrence type carries only `reminderLeadMinutes: number | null`. The UI cannot distinguish an absolute DATE-TIME VALARM or a multi-VALARM event from a genuinely reminder-free event — both come back as `null` from `expand.ts` (classifyValarms maps `custom`/`none` → null). Consequence:
- An event with an absolute-TIME or multi-VALARM will be displayed with `None` selected in the picker
- If the user saves without changing the picker, `reminderLeadMinutes` is absent from the payload (D-08), and the server's `hasExplicitReminder` check preserves the original VALARM verbatim
- If the user selects a preset and saves, the original custom VALARM is replaced — this is intended behavior (user consciously chose a preset)
- The `__custom__` sentinel path (disabled read-only option) is reachable only via a future API shape change that would surface a `reminderKind: 'custom'` flag on the occurrence
This is a design-layer limitation documented in the UI-SPEC (D-07/NOTIF-05) and is not a defect.
## Known Limitation: No Live Event Creation in Dev
The dev-stack bypass user (id 1) has no Fastmail provider configured (`needsProviderSetup=true`, no calendars). The playwright smoke verified picker behavior (form rendering, swap, edit-mode load) via route mocks for edit assertions. End-to-end event creation with a real Fastmail PUT could not be exercised in dev — this is a dev-environment limitation unrelated to Phase 11 and tracked separately in the backlog. The server-side reminderLeadMinutes field was validated: a POST /create with `reminderLeadMinutes` returned 422 only at calendar resolution (no provider), not at schema validation.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking / Style] Prettier violations on phase-11 modified files**
- **Found during:** CI gate (format:check step, post-Task 2)
- **Issue:** 7 files across apps/api (Plans 11-01/11-03) and apps/pwa/EventForm.tsx had unformatted code; `pnpm format:check` exited 1
- **Fix:** Ran `prettier --write` on all 7 files; all formatting was whitespace/line-length only (no logic change)
- **Files modified:** `apps/pwa/src/components/EventForm.tsx`, `apps/api/src/broker/{expand,reminderScheduler,sync,vevent}.ts`, `apps/api/tests/broker/{reminderScheduler,sync}.test.ts`
- **Verification:** `pnpm format:check` exits 0; PWA vitest 201/201 still pass after formatting
- **Committed in:** `b9b3191` (style commit, separate from feature commits)
---
**Total deviations:** 1 auto-fixed (Rule 3 — blocking CI gate)
**Impact on plan:** Formatting-only; no logic or behavior change. Required to unblock the CI gate.
## CI Gate Results
| Check | Result |
|---|---|
| `pnpm -r typecheck` | PASS (0 errors, both apps) |
| `pnpm --filter @familysync/pwa exec vitest run` | PASS (201/201) |
| `pnpm --filter @familysync/pwa exec eslint src/ --max-warnings 0` | PASS |
| `pnpm format:check` | PASS (after prettier style fix) |
| `pnpm md:lint` | PASS (0 errors) |
| API vitest | SKIP — dev MariaDB not running (pre-existing dev-env limitation; CI passes) |
## Issues Encountered
None beyond the prettier fix documented above.
## Threat Flags
No new threat surface. The reminder picker emits bounded values (null, 0, positive integer, or field absent) validated server-side by `eventFieldsSchema` (`z.number().int().min(0).nullable().optional()`). All option labels rendered as plain-text JSX children (no `dangerouslySetInnerHTML`) — T-11-10 mitigated. No new npm dependencies added.
## Known Stubs
None. All picker-to-payload wiring is complete. No placeholder values or TODO markers.
## Next Phase Readiness
- Plan 11-02 (reminderScheduler) is independent and was sequenced before this plan; the reminder scheduler already reads `reminderLeadMinutes` from the DB
- Plan 11-04 is the final Wave 3 plan; Phase 11 is now complete from the UI's perspective
- The end-to-end flow (picker → payload → outboxWorker → Fastmail VALARM PUT → sync → scheduler → push notification) is fully wired; only live Fastmail testing (requires a non-dev provider) remains as a manual gate
---
*Phase: 11-per-event-reminders*
*Completed: 2026-06-14*
## Self-Check: PASSED
Files exist:
- FOUND: apps/pwa/src/api/client.ts
- FOUND: apps/pwa/src/components/EventForm.tsx
- FOUND: apps/pwa/src/components/EventForm.test.tsx
- FOUND: apps/pwa/src/components/EventDetailPopover.test.tsx
Commits exist:
- 2c30afe: feat(11-04): add reminderLeadMinutes to CreateEventPayload + CalendarOccurrence
- fe549ef: feat(11-04): add reminder picker to EventForm (allDay swap, edit pre-population, payload mapping)
- b9b3191: style(11-04): apply prettier to phase-11 modified files
CI gate: all 5 runnable checks pass (API vitest skipped — dev MariaDB, pre-existing)