From 16ac23547606ff44aee282635c44d1bd49a2c0c7 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sun, 14 Jun 2026 08:14:08 -0400 Subject: [PATCH] fix(11-05): CR-02 all-day-aware push body (no "Starts in 0 min") - humanizeLeadMinutes: add isAllDay=false param; all-day branch returns "Today" (lead=0), "Tomorrow" (1440), "In 1 week" (10080), "In N days" (other) - byKey map: store isAllDay flag (false for timed, true for all-day) - dispatch loop: pass event.isAllDay to humanizeLeadMinutes All-day same-day reminder push now reads "Today" instead of "Starts in 0 min". Timed event wording unchanged (isAllDay defaults to false). --- apps/api/src/broker/reminderScheduler.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/api/src/broker/reminderScheduler.ts b/apps/api/src/broker/reminderScheduler.ts index a6ca77a..39b4233 100644 --- a/apps/api/src/broker/reminderScheduler.ts +++ b/apps/api/src/broker/reminderScheduler.ts @@ -79,11 +79,23 @@ function yyyyMmDd(d: Date): string { /** * Humanize a lead time in minutes to a human-readable string. * + * CR-02 (Phase 11 Plan 05): added isAllDay parameter. All-day events use day-granularity + * wording ("Today", "Tomorrow", "In N days/1 week") because the lead is day-based + * and "Starts in 0 min" for a same-day all-day event (lead=0) is factually wrong. + * Timed events keep the existing "Starts in N min/hours/days" wording (default isAllDay=false). + * * D-09: "Starts in 30 min" / "Starts in 1 hour" / "Starts in 1 day" etc. * Branch order is important — check < 120 before the hours calculation to * prevent Math.round(90/60)=2 erroneously giving "2 hours" for a 90-min lead. */ -export function humanizeLeadMinutes(leadMinutes: number): string { +export function humanizeLeadMinutes(leadMinutes: number, isAllDay = false): string { + if (isAllDay) { + // Day-granularity wording for all-day event reminders + if (leadMinutes === 0) return 'Today'; + if (leadMinutes <= 1440) return 'Tomorrow'; + if (leadMinutes === 10080) return 'In 1 week'; + return `In ${Math.round(leadMinutes / 1440)} days`; + } if (leadMinutes < 60) return `Starts in ${leadMinutes} min`; if (leadMinutes < 120) return 'Starts in 1 hour'; if (leadMinutes < 1440) return `Starts in ${Math.round(leadMinutes / 60)} hours`; @@ -186,6 +198,7 @@ export async function runReminderCheck(now = new Date()): Promise { dtstartMs: number; // key component — used for compound dedup key pruneMs: number; // when to prune: dtstartUtc for timed; end-of-event-date for all-day reminderLeadMinutes: number; + isAllDay: boolean; // CR-02 (Plan 05): needed for allDay-aware humanizeLeadMinutes dateStr: string; subs: SubRow[]; } @@ -214,6 +227,7 @@ export async function runReminderCheck(now = new Date()): Promise { dtstartMs, pruneMs: dtstartMs, // timed: prune when event has started (dtstartUtc <= now) reminderLeadMinutes: lead, + isAllDay: false, // CR-02: timed events use the existing "Starts in N..." wording dateStr: yyyyMmDd(dtstartUtc), subs: [], }); @@ -262,6 +276,7 @@ export async function runReminderCheck(now = new Date()): Promise { dtstartMs, pruneMs, reminderLeadMinutes: lead, + isAllDay: true, // CR-02: all-day events use day-granularity wording dateStr: dtstartDate, subs: [], }); @@ -289,7 +304,8 @@ export async function runReminderCheck(now = new Date()): Promise { // Null-safe title fallback (D-02 / NOTIF-01): use uid if title is NULL. title: event.title ?? event.uid, // D-09: humanized body driven by the configured lead (DB ground truth), not live delta. - body: humanizeLeadMinutes(event.reminderLeadMinutes), + // CR-02 (Plan 05): pass isAllDay so all-day events get day-granularity wording. + body: humanizeLeadMinutes(event.reminderLeadMinutes, event.isAllDay), tag: `reminder-${event.uid}`, navigate: `/calendar?date=${event.dateStr}&event=${event.uid}`, };