Merge pull request 'Fix draft pause clock: use Math.ceil consistently via shared msToSeconds helper' (#78) from fix/draft-pause-clock-rounding into main
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m29s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m34s
🚀 Deploy / 🐳 Build (push) Successful in 1m11s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s

Reviewed-on: #78
This commit is contained in:
chrisp 2026-06-06 23:49:15 +00:00
commit 6ba8adab5b
4 changed files with 13 additions and 6 deletions

View file

@ -113,6 +113,11 @@ export function getInitialDraftSpeed(season: DraftSpeedSource): string {
return "standard"; return "standard";
} }
/** Converts a millisecond duration to whole seconds, always rounding up. */
export function msToSeconds(ms: number): number {
return Math.max(0, Math.ceil(ms / 1000));
}
/** /**
* Anchors a server-provided remaining-seconds value to the local client clock. * Anchors a server-provided remaining-seconds value to the local client clock.
* Use this everywhere we convert timeRemaining an expiresAt timestamp so the * Use this everywhere we convert timeRemaining an expiresAt timestamp so the

View file

@ -42,7 +42,7 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { NotificationSettings } from "~/components/NotificationSettings"; import { NotificationSettings } from "~/components/NotificationSettings";
import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings"; import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings";
import { toast } from "sonner"; import { toast } from "sonner";
import { clientExpiresAt, formatClockTime, getTimerColorClass } from "~/lib/draft-timer"; import { clientExpiresAt, formatClockTime, getTimerColorClass, msToSeconds } from "~/lib/draft-timer";
import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react"; import { Users, LayoutGrid, ListChecks, Settings, ListOrdered } from "lucide-react";
import type { Route } from "./+types/$leagueId.draft.$seasonId"; import type { Route } from "./+types/$leagueId.draft.$seasonId";
@ -509,7 +509,7 @@ export default function DraftRoom() {
// without skew, then re-anchor to client clock. // without skew, then re-anchor to client clock.
const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow); const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow);
if (!active?.picksExpiresAt) return null; if (!active?.picksExpiresAt) return null;
const secondsRemaining = Math.ceil((active.picksExpiresAt.getTime() - loaderTimerNow) / 1000); const secondsRemaining = msToSeconds(active.picksExpiresAt.getTime() - loaderTimerNow);
return { teamId: active.teamId, expiresAt: clientExpiresAt(secondsRemaining) }; return { teamId: active.teamId, expiresAt: clientExpiresAt(secondsRemaining) };
}); });
@ -517,7 +517,7 @@ export default function DraftRoom() {
if (!pickTimerExpiresAt) return; if (!pickTimerExpiresAt) return;
const { teamId, expiresAt } = pickTimerExpiresAt; const { teamId, expiresAt } = pickTimerExpiresAt;
const tick = () => { const tick = () => {
const remaining = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)); const remaining = msToSeconds(expiresAt - Date.now());
setTeamTimers((prev) => prev[teamId] === remaining ? prev : { ...prev, [teamId]: remaining }); setTeamTimers((prev) => prev[teamId] === remaining ? prev : { ...prev, [teamId]: remaining });
}; };
tick(); // immediate update so display is accurate on mount tick(); // immediate update so display is accurate on mount

View file

@ -7,6 +7,7 @@ import { checkOvernightPause } from "./overnight-pause-check";
import { calculatePickInfo } from "~/models/draft-utils"; import { calculatePickInfo } from "~/models/draft-utils";
import { logger } from "./logger"; import { logger } from "./logger";
import { db } from "./db"; import { db } from "./db";
import { msToSeconds } from "~/lib/draft-timer";
// Socket event types // Socket event types
interface ServerToClientEvents { interface ServerToClientEvents {
@ -327,7 +328,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
const expiresAt = t.picksExpiresAt?.getTime(); const expiresAt = t.picksExpiresAt?.getTime();
const computedRemaining = const computedRemaining =
expiresAt && expiresAt > nowMs expiresAt && expiresAt > nowMs
? Math.max(0, Math.floor((expiresAt - nowMs) / 1000)) ? msToSeconds(expiresAt - nowMs)
: t.timeRemaining; : t.timeRemaining;
return { return {
teamId: t.teamId, teamId: t.teamId,

View file

@ -7,6 +7,7 @@ import { checkOvernightPause, evictOvernightPauseCache, overnightPauseCacheKeys,
import { logger } from "./logger"; import { logger } from "./logger";
import { db } from "./db"; import { db } from "./db";
import { startDraft } from "~/services/draft-autostart"; import { startDraft } from "~/services/draft-autostart";
import { msToSeconds } from "~/lib/draft-timer";
// Per-season in-memory state // Per-season in-memory state
const pickTimeouts = new Map<string, NodeJS.Timeout>(); // seasonId → active setTimeout const pickTimeouts = new Map<string, NodeJS.Timeout>(); // seasonId → active setTimeout
@ -259,7 +260,7 @@ async function _schedulePickForSeason(seasonId: string): Promise<void> {
} }
const msUntilExpiry = expiresAt.getTime() - now; const msUntilExpiry = expiresAt.getTime() - now;
const timeRemaining = Math.max(0, Math.ceil(msUntilExpiry / 1000)); const timeRemaining = msToSeconds(msUntilExpiry);
// Tell clients to start their local countdown. // Tell clients to start their local countdown.
try { try {
@ -427,7 +428,7 @@ export async function onDraftPaused(seasonId: string): Promise<Array<{ teamId: s
activeTimers activeTimers
.filter((t) => t.picksExpiresAt !== null) .filter((t) => t.picksExpiresAt !== null)
.map(async (timer) => { .map(async (timer) => {
const remaining = Math.max(0, Math.floor(((timer.picksExpiresAt?.getTime() ?? now) - now) / 1000)); const remaining = msToSeconds((timer.picksExpiresAt?.getTime() ?? now) - now);
await db await db
.update(schema.draftTimers) .update(schema.draftTimers)
.set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() }) .set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() })