feat(11-03): sync.ts derives reminderLeadMinutes from VALARM classification (D-07/NOTIF-05)

- 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)
This commit is contained in:
Lucas Berger
2026-06-13 22:18:46 -04:00
parent cdca93094a
commit 1cc0d7278a
+16
View File
@@ -22,6 +22,7 @@ import { and, eq, notInArray } from 'drizzle-orm';
import { db } from '../db/client.js'; import { db } from '../db/client.js';
import { calendars, calendarEvents } from '../db/schema.js'; import { calendars, calendarEvents } from '../db/schema.js';
import type { EventChange } from '../lib/eventChangeDispatcher.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, * 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 = const locationValue: string | null =
(vevent.getFirstPropertyValue('location') as string | null) ?? 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. // 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. // One indexed lookup on (calendarId, uid) — cheap, covered by uniq_calendar_uid.
// D-13: this read is from MariaDB cache, not Fastmail. // D-13: this read is from MariaDB cache, not Fastmail.
@@ -154,6 +166,8 @@ export async function syncCalendar(
dtstartDate: dtstartDateValue, dtstartDate: dtstartDateValue,
allDay, allDay,
hasRrule: isRecurring, hasRrule: isRecurring,
// Phase 11: ground-truth VALARM lead for the scheduler (D-07/NOTIF-05, T-11-07)
reminderLeadMinutes: reminderLeadMinutesValue,
}) })
.onDuplicateKeyUpdate({ .onDuplicateKeyUpdate({
set: { set: {
@@ -165,6 +179,8 @@ export async function syncCalendar(
dtstartDate: dtstartDateValue, dtstartDate: dtstartDateValue,
allDay, allDay,
hasRrule: isRecurring, hasRrule: isRecurring,
// Phase 11: keep reminderLeadMinutes current on re-sync (native client may change VALARM)
reminderLeadMinutes: reminderLeadMinutesValue,
updatedAt: new Date(), updatedAt: new Date(),
}, },
}); });