brackt/app/lib/overnight-pause.ts
Chris Parsons dbc23f14ce
Add overnight pause for draft timers (#335)
* Add overnight pause feature for draft timers

Protects players from having their pick timer expire while asleep.
Admins configure a nightly window (league-wide or per-user timezone);
the timer freezes during that window while autodraft can still fire.

Fixes #66

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix TS error: guard getUserDisplayName against undefined user

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 22:31:52 -07:00

96 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Utilities for overnight pause windows.
* Works in both Node.js and browser — uses only Intl, no extra dependencies.
*/
function getLocalMinutes(timezone: string, now: Date): number {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
const parts = formatter.formatToParts(now);
const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
// Intl hour12:false can return "24" for midnight — normalize to 0
return (hour === 24 ? 0 : hour) * 60 + minute;
}
/**
* Build a UTC Date representing "the given date at HH:MM in the given timezone."
* Corrects for the UTC offset by comparing what local time the naive UTC produces.
*/
function utcForLocalTime(
timezone: string,
year: number,
month: number, // 1-indexed
day: number,
hour: number,
minute: number
): Date {
// Start with a naive UTC (treating target H:M as UTC)
const approxUTC = Date.UTC(year, month - 1, day, hour, minute, 0);
const probe = new Date(approxUTC);
const localMins = getLocalMinutes(timezone, probe);
const targetMins = hour * 60 + minute;
// Shift: if local is behind target, add the gap; if ahead, subtract it
return new Date(approxUTC + (targetMins - localMins) * 60 * 1000);
}
/**
* Returns true if `now` falls inside the overnight pause window defined by
* startHHMMendHHMM in the given IANA timezone.
*
* The window almost always spans midnight (e.g. "23:00""07:00"), but a
* same-day window (e.g. "10:00""18:00") is also handled correctly.
*/
export function isInOvernightWindow(
timezone: string,
startHHMM: string,
endHHMM: string,
now = new Date()
): boolean {
const currentMinutes = getLocalMinutes(timezone, now);
const [sh, sm] = startHHMM.split(":").map(Number);
const [eh, em] = endHHMM.split(":").map(Number);
const startMinutes = sh * 60 + sm;
const endMinutes = eh * 60 + em;
if (startMinutes > endMinutes) {
// Spans midnight: in window if current >= start OR current < end
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
}
// Same-day window: in window if start <= current < end
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
}
/**
* Returns the next UTC Date when endHHMM will occur in the given timezone.
* Used to tell clients when the overnight window will end.
*/
export function getOvernightResumeUTC(
timezone: string,
endHHMM: string,
now = new Date()
): Date {
const [eh, em] = endHHMM.split(":").map(Number);
// Get current calendar date in the target timezone
const dateFmt = new Intl.DateTimeFormat("en-CA", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
const [year, month, day] = dateFmt.format(now).split("-").map(Number);
// Candidate: today at endHHMM in the target timezone
const candidate = utcForLocalTime(timezone, year, month, day, eh, em);
// If this moment has already passed, advance one day
if (candidate.getTime() <= now.getTime()) {
return new Date(candidate.getTime() + 24 * 60 * 60 * 1000);
}
return candidate;
}