- []**CAL-13**: User can choose a reminder lead time when creating or editing an event from a preset list (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d), with **"None" as the default**; the choice is serialized as a VALARM on the event written back to Fastmail.
- []**CAL-14**: Editing an event **preserves any existing reminder/VALARM** set in another client (Fastmail or native) — reminders are never silently stripped on round-trip.
- [x]**CAL-13**: User can choose a reminder lead time when creating or editing an event from a preset list (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d), with **"None" as the default**; the choice is serialized as a VALARM on the event written back to Fastmail.
- [x]**CAL-14**: Editing an event **preserves any existing reminder/VALARM** set in another client (Fastmail or native) — reminders are never silently stripped on round-trip.
- [x]**CAL-15**: A created, edited, or deleted event reaches Fastmail within ~2 seconds (event-driven outbox drain) instead of up to ~15s, while preserving the optimistic-202 accept and all outbox durability guarantees (create-before-delete ordering, drain concurrency guard, fresh-etag-before-PUT, per-uid exactly-once).
@@ -29,7 +29,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
- [x]**Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11)
- [x]**Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee (completed 2026-06-12)
- [x]**Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar (completed 2026-06-13)
- []**Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice
- [x]**Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice (completed 2026-06-14)
- [ ]**Phase 12: Initial Setup Wizard** - First-run validated bootstrap of env/VAPID/DB/OIDC + first app password, reusing the admin route surface
- [x]**Phase 13: Real Lint Gate (ESLint)** - Wire ESLint flat config (typescript-eslint + React) across both apps so the Phase 8 CI lint slot actually fails on violations instead of no-op'ing (completed 2026-06-12)
- [x]**Phase 14: Desktop E2E Coverage** - Add a Desktop Chrome Playwright profile + make the mobile-authored specs desktop-safe so the Phase 8 regression gate validates desktop, not just mobile (completed 2026-06-12)
@@ -422,7 +422,7 @@ At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx`
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
- "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__"
- "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)"
- "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"
# 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**
- `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)
| 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.
| 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
- 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)
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.