From 1cc0d7278a1127ec902a6bb5fc46b006a4c3a18c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 22:18:46 -0400 Subject: [PATCH] feat(11-03): sync.ts derives reminderLeadMinutes from VALARM classification (D-07/NOTIF-05) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import classifyValarms from vevent.ts - Derive reminderLeadMinutesValue: preset/offlist → leadMinutes; custom/none → null - Add reminderLeadMinutes to .values() and .onDuplicateKeyUpdate({ set: {} }) - Scheduler now has ground truth for native-client VALARMs (T-11-07 mitigated) --- apps/api/src/broker/sync.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/api/src/broker/sync.ts b/apps/api/src/broker/sync.ts index b8b6cd1..c045167 100644 --- a/apps/api/src/broker/sync.ts +++ b/apps/api/src/broker/sync.ts @@ -22,6 +22,7 @@ import { and, eq, notInArray } from 'drizzle-orm'; import { db } from '../db/client.js'; import { calendars, calendarEvents } from '../db/schema.js'; import type { EventChange } from '../lib/eventChangeDispatcher.js'; +import { classifyValarms } from './vevent.js'; /** * Fetches all calendar objects for a given DAVCalendar, parses VEVENTs with ical.js, @@ -128,6 +129,17 @@ export async function syncCalendar( const locationValue: string | null = (vevent.getFirstPropertyValue('location') as string | null) ?? null; + // Phase 11: derive reminderLeadMinutes from the VALARM classification (D-07, NOTIF-05). + // classifyValarms wraps ICAL.parse in try/catch (T-11-07 mitigated). + // preset/offlist → single relative lead in minutes (scheduler ground truth). + // custom (absolute DATE-TIME or multiple VALARMs) → null (can't resolve a single lead). + // none → null (no VALARM present). + const alarmClass = classifyValarms(obj.data as string); + const reminderLeadMinutesValue: number | null = + alarmClass.kind === 'preset' || alarmClass.kind === 'offlist' + ? alarmClass.leadMinutes + : null; + // NOTIF-03: look up the existing row so we can classify add vs update. // One indexed lookup on (calendarId, uid) — cheap, covered by uniq_calendar_uid. // D-13: this read is from MariaDB cache, not Fastmail. @@ -154,6 +166,8 @@ export async function syncCalendar( dtstartDate: dtstartDateValue, allDay, hasRrule: isRecurring, + // Phase 11: ground-truth VALARM lead for the scheduler (D-07/NOTIF-05, T-11-07) + reminderLeadMinutes: reminderLeadMinutesValue, }) .onDuplicateKeyUpdate({ set: { @@ -165,6 +179,8 @@ export async function syncCalendar( dtstartDate: dtstartDateValue, allDay, hasRrule: isRecurring, + // Phase 11: keep reminderLeadMinutes current on re-sync (native client may change VALARM) + reminderLeadMinutes: reminderLeadMinutesValue, updatedAt: new Date(), }, });