Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.7 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 11-per-event-reminders | 03 | api/broker |
|
|
|
|
|
|
Phase 11 Plan 03: reminderLeadMinutes End-to-End Plumbing Summary
reminderLeadMinutes round-trips end-to-end: eventFieldsSchema ingress validation → outbox payload drain → buildVeventString VALARM emission → Fastmail PUT; sync.ts parses native VALARMs into the DB column (scheduler ground truth); CalendarOccurrence surfaces the value for edit-mode picker pre-population.
Tasks Completed
| Task | Description | RED Commit | GREEN Commit |
|---|---|---|---|
| 1 | Schema field + outbox worker preserve-on-edit + buildVeventString wiring | 79f6871 |
4f42b75 |
| 2 | sync.ts VALARM → reminderLeadMinutes upsert | cdca930 |
1cc0d72 |
| 3 | Surface reminderLeadMinutes on CalendarOccurrence + GET select | 7df11d2 |
e171431 |
Schema Field (eventFieldsSchema + outboxPayloadSchema)
Both schemas now have:
reminderLeadMinutes: z.number().int().min(0).nullable().optional()
Four-state semantics (D-08):
absent— field not present in payload; UPDATE branch preserves existing VALARM verbatim (D-08)null— explicit "None" → VALARM cleared on write-back0— same-day all-day (9 AM on event date); timed 0 = None (D-06)positive— N minutes before event start (timed) or N/1440 days before (all-day)
hasExplicitReminder Preserve-on-Edit Path (CAL-14)
Pattern mirrors the WR-01 hasExplicitRecurrence + _preservedRrule preserve path:
UPDATE branch computes:
const hasExplicitReminder = Object.prototype.hasOwnProperty.call(fields, 'reminderLeadMinutes')!hasExplicitReminder+ rawVevent has VALARMs →valarmsToPreserve = extractValarms(rawVevent)(preserve verbatim via addSubcomponent)hasExplicitReminder+ allDay + value →allDayAlertInstantUtcUpdate = computeAlertInstantUtc(start, lead/1440, tz)hasExplicitReminder+ null → clear (no valarms, reminderLeadMinutes=null passed to buildVeventString)
buildVeventString call extended with: reminderLeadMinutes: hasExplicitReminder ? fields.reminderLeadMinutes : undefined, valarms: valarmsToPreserve, allDayAlertInstantUtc: allDayAlertInstantUtcUpdate.
CREATE branch: no preserve path (new event always carries explicit picker value). Computes allDayAlertInstantUtcCreate from reminderLeadMinutes / 1440 when allDay.
sync.ts Derivation Rule
import { classifyValarms } from './vevent.js';
const alarmClass = classifyValarms(obj.data as string);
const reminderLeadMinutesValue: number | null =
alarmClass.kind === 'preset' || alarmClass.kind === 'offlist'
? alarmClass.leadMinutes
: null;
Written to both .values({...}) and .onDuplicateKeyUpdate({ set: {...} }). Classification rules:
presetorofflist→leadMinutes(scheduler ground truth)custom(absolute DATE-TIME or multiple VALARMs) →null(D-07/NOTIF-05)none→null(no VALARM)
No schema DDL change — reminder_lead_minutes column was added by Phase 10 migration.
CalendarOccurrence Propagation (D-10)
// CalendarOccurrence interface:
reminderLeadMinutes: number | null; // after hasRrule
Derived once per VEVENT in expandOccurrences via classifyValarms(rawVevent) (series-level, D-10). All occurrences inherit the master's value. Added to both non-recurring and recurring occurrence construction branches. GET /api/events select also includes calendarEvents.reminderLeadMinutes for edit-mode pre-population.
Verification Results
pnpm --filter @familysync/api exec vitest run tests/broker/ tests/broker/vevent.test.ts: 132/132 PASSpnpm --filter @familysync/api exec tsc --noEmit: CLEAN (0 errors)- No
drizzle-kit pushintroduced; no schema.ts DDL change - 13 new tests added: 4 (Task 1 outboxWorker), 5 (Task 2 sync), 4 (Task 3 expand)
Deviations from Plan
None — plan executed exactly as written.
Known Stubs
None. All wiring is complete end-to-end. No placeholder values or TODO markers.
Threat Flags
No new threat surface. All new fields are bounded integers validated by Zod at both ingress (eventFieldsSchema) and drain (outboxPayloadSchema) — T-11-06 mitigated. classifyValarms is try/catch safe — T-11-07 mitigated. The VALARM preserve path (extractValarms) rides the existing CR-02 scoped query on the writing member's calendar — T-11-08 unchanged.
Self-Check: PASSED
Files exist:
- FOUND: apps/api/src/routes/events.ts
- FOUND: apps/api/src/broker/outboxWorker.ts
- FOUND: apps/api/src/broker/sync.ts
- FOUND: apps/api/src/broker/expand.ts
- FOUND: apps/api/tests/broker/outboxWorker.test.ts
- FOUND: apps/api/tests/broker/sync.test.ts
- FOUND: apps/api/tests/broker/expand.test.ts
Commits exist:
79f6871: test(11-03) RED Task 14f42b75: feat(11-03) GREEN Task 1cdca930: test(11-03) RED Task 21cc0d72: feat(11-03) GREEN Task 27df11d2: test(11-03) RED Task 3e171431: feat(11-03) GREEN Task 3
Key exports verified:
- reminderLeadMinutes in eventFieldsSchema: CONFIRMED (grep:
reminderLeadMinutes: z.number()) - hasExplicitReminder in outboxWorker.ts: CONFIRMED
- reminderLeadMinutes on CalendarOccurrence: CONFIRMED
- classifyValarms import in sync.ts: CONFIRMED
- classifyValarms import in expand.ts: CONFIRMED