From 77c0f475525fd0e179ab581a3872fc92ea6272e0 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 6 Jun 2026 16:39:53 -0700 Subject: [PATCH] Fix draft pause clock: use Math.ceil consistently via shared msToSeconds helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pause snapshot used Math.floor while the client display used Math.ceil, causing the visible clock to round down by 1s on pause click - draft-state-sync also used Math.floor for reconnecting clients - Extract msToSeconds() to app/lib/draft-timer.ts as the single source of truth for all ms→seconds conversions across server and client Co-Authored-By: Claude Sonnet 4.6 --- app/lib/draft-timer.ts | 5 +++++ app/routes/leagues/$leagueId.draft.$seasonId.tsx | 6 +++--- server/socket.ts | 3 ++- server/timer.ts | 5 +++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/lib/draft-timer.ts b/app/lib/draft-timer.ts index 68bea2d..2268ea5 100644 --- a/app/lib/draft-timer.ts +++ b/app/lib/draft-timer.ts @@ -113,6 +113,11 @@ export function getInitialDraftSpeed(season: DraftSpeedSource): string { 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. * Use this everywhere we convert timeRemaining → an expiresAt timestamp so the diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 5161aaf..d62959a 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -42,7 +42,7 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications"; import { NotificationSettings } from "~/components/NotificationSettings"; import { AutodraftBadgeWithPopover, AutodraftSettings } from "~/components/AutodraftSettings"; 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 type { Route } from "./+types/$leagueId.draft.$seasonId"; @@ -509,7 +509,7 @@ export default function DraftRoom() { // without skew, then re-anchor to client clock. const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > loaderTimerNow); 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) }; }); @@ -517,7 +517,7 @@ export default function DraftRoom() { if (!pickTimerExpiresAt) return; const { teamId, expiresAt } = pickTimerExpiresAt; 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 }); }; tick(); // immediate update so display is accurate on mount diff --git a/server/socket.ts b/server/socket.ts index 283b5a4..65f385d 100644 --- a/server/socket.ts +++ b/server/socket.ts @@ -7,6 +7,7 @@ import { checkOvernightPause } from "./overnight-pause-check"; import { calculatePickInfo } from "~/models/draft-utils"; import { logger } from "./logger"; import { db } from "./db"; +import { msToSeconds } from "~/lib/draft-timer"; // Socket event types interface ServerToClientEvents { @@ -327,7 +328,7 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { const expiresAt = t.picksExpiresAt?.getTime(); const computedRemaining = expiresAt && expiresAt > nowMs - ? Math.max(0, Math.floor((expiresAt - nowMs) / 1000)) + ? msToSeconds(expiresAt - nowMs) : t.timeRemaining; return { teamId: t.teamId, diff --git a/server/timer.ts b/server/timer.ts index 15c7f79..b84728b 100644 --- a/server/timer.ts +++ b/server/timer.ts @@ -7,6 +7,7 @@ import { checkOvernightPause, evictOvernightPauseCache, overnightPauseCacheKeys, import { logger } from "./logger"; import { db } from "./db"; import { startDraft } from "~/services/draft-autostart"; +import { msToSeconds } from "~/lib/draft-timer"; // Per-season in-memory state const pickTimeouts = new Map(); // seasonId → active setTimeout @@ -259,7 +260,7 @@ async function _schedulePickForSeason(seasonId: string): Promise { } 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. try { @@ -427,7 +428,7 @@ export async function onDraftPaused(seasonId: string): Promise t.picksExpiresAt !== null) .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 .update(schema.draftTimers) .set({ timeRemaining: remaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() })