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

12 KiB
Raw Blame History

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, key_decisions, requirements-completed, duration, completed
phase plan subsystem tags dependency_graph tech_stack key_files key_decisions requirements-completed duration completed
11-per-event-reminders 04 ui
reminder
picker
EventForm
allDay
VALARM
client-types
playwright
cal-13
cal-14
requires provides affects
Plan 11-03 (CalendarOccurrence.reminderLeadMinutes, GET /api/events surfaces the field, eventFieldsSchema accepts reminderLeadMinutes)
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
Plan 11-02 (reminder scheduler reads reminderLeadMinutes; client picker is its UI surface)
Future plans touching EventForm or CreateEventPayload
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)
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
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
CAL-13
CAL-14
~60min (Tasks 1+2) + playwright smoke (Task 3) 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 + CalendarOccurrence2c30afe (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 filesb9b3191 (style)

Files Created/Modified

  • apps/pwa/src/api/client.tsreminderLeadMinutes 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)