- 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
12 KiB
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 |
|
|
|
|
|
|
~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 | nulladded toCalendarOccurrence;reminderLeadMinutes?: number | nulladded toCreateEventPayload— 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) whenallDay; 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 (A1–A4b) against the dev stack
Task Commits
- Task 1: Client types — reminderLeadMinutes on CreateEventPayload + CalendarOccurrence —
2c30afe(feat) - Task 2: Reminder picker in EventForm (swap, default None, reset-on-toggle, edit pre-population, Custom-kept, payload mapping) —
fe549ef(feat) - 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) - Style fix: prettier on phase-11 modified files —
b9b3191(style)
Files Created/Modified
apps/pwa/src/api/client.ts—reminderLeadMinutesadded toCalendarOccurrence(required) andCreateEventPayload(optional)apps/pwa/src/components/EventForm.tsx— reminder picker block, allDay-aware option swap, reset-on-toggle, edit pre-population, Custom-kept sentinel, payload mappingapps/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
Noneselected in the picker - If the user saves without changing the picker,
reminderLeadMinutesis absent from the payload (D-08), and the server'shasExplicitRemindercheck 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 areminderKind: '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:checkexited 1 - Fix: Ran
prettier --writeon 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:checkexits 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
reminderLeadMinutesfrom 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 + CalendarOccurrencefe549ef: 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)