Fix email timing and show fallback data when Garmin sync pending
All checks were successful
Deploy / deploy (push) Successful in 2m31s

- Add 15-minute notification granularity (*/15 cron) so users get emails
  at their configured time instead of rounding to the nearest hour
- Add DailyLog fallback to most recent when today's log doesn't exist,
  preventing 100/100/Unknown default values before morning sync
- Show "Last synced" indicator when displaying stale data
- Change Garmin sync to 6-hour intervals (0,6,12,18 UTC) to ensure
  data is available before European morning notifications

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 09:56:41 +00:00
parent 0d5785aaaa
commit 092d8bb3dd
7 changed files with 402 additions and 31 deletions

View File

@@ -17,19 +17,40 @@ interface NotificationResult {
timestamp: string;
}
// Get the current hour in a specific timezone
function getCurrentHourInTimezone(timezone: string): number {
// Get current quarter-hour slot (0, 15, 30, 45) in user's timezone
function getCurrentQuarterHourSlot(timezone: string): {
hour: number;
minute: number;
} {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
minute: "numeric",
hour12: false,
});
return parseInt(formatter.format(new Date()), 10);
const parts = formatter.formatToParts(new Date());
const hour = Number.parseInt(
parts.find((p) => p.type === "hour")?.value ?? "0",
10,
);
const minute = Number.parseInt(
parts.find((p) => p.type === "minute")?.value ?? "0",
10,
);
// Round down to nearest 15-min slot
const slot = Math.floor(minute / 15) * 15;
return { hour, minute: slot };
}
// Extract hour from "HH:MM" format
function getNotificationHour(notificationTime: string): number {
return parseInt(notificationTime.split(":")[0], 10);
// Extract quarter-hour slot from "HH:MM" format
function getNotificationSlot(notificationTime: string): {
hour: number;
minute: number;
} {
const [h, m] = notificationTime.split(":").map(Number);
// Round down to nearest 15-min slot
const slot = Math.floor(m / 15) * 15;
return { hour: h, minute: slot };
}
// Map decision status to icon
@@ -95,11 +116,14 @@ export async function POST(request: Request) {
for (const user of users) {
try {
// Check if current hour in user's timezone matches their notification time
const currentHour = getCurrentHourInTimezone(user.timezone);
const notificationHour = getNotificationHour(user.notificationTime);
// Check if current quarter-hour slot in user's timezone matches their notification time
const currentSlot = getCurrentQuarterHourSlot(user.timezone);
const notificationSlot = getNotificationSlot(user.notificationTime);
if (currentHour !== notificationHour) {
if (
currentSlot.hour !== notificationSlot.hour ||
currentSlot.minute !== notificationSlot.minute
) {
result.skippedWrongTime++;
continue;
}