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).
This commit is contained in:
Lucas Berger
2026-06-14 08:14:08 -04:00
parent 1caa2e36d2
commit 16ac235476
+18 -2
View File
@@ -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<void> {
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<void> {
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<void> {
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<void> {
// 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}`,
};