Fix draft timer creation for teams without existing timers (#26)

* Create timer when adding time to a team with no existing timer

When a commissioner tries to add time to a team that has no draft timer,
instead of returning a 404 error, create a new timer record with the
specified amount of time. Removing time still returns a 404 if no timer
exists (nothing to remove from).

https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc

* Fix silent timer failures and missing seasonId filter in timer model

- draft.make-pick.ts: create a timer with the increment amount instead of
  logging a warning and silently skipping when no timer exists for the
  picking team
- draft.force-manual-pick.ts: same fix for the commissioner force-pick path;
  also unconditionally emit the timer-update socket event so clients always
  see the updated time
- models/draft-timer.ts: add seasonId parameter to getTeamTimer and
  updateTeamTimer so they cannot match the wrong season's timer when a team
  participates in multiple seasons

https://claude.ai/code/session_016VpJKZZFNQQqzfmLu8pHoc

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-02-22 17:27:16 -08:00 committed by GitHub
parent a3ec556ecc
commit 34da0594d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 41 deletions

View file

@ -1,6 +1,6 @@
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
export async function initializeDraftTimers(seasonId: string, teams: { id: string }[], initialTime: number) { export async function initializeDraftTimers(seasonId: string, teams: { id: string }[], initialTime: number) {
const timerData = teams.map((team) => ({ const timerData = teams.map((team) => ({
@ -8,17 +8,17 @@ export async function initializeDraftTimers(seasonId: string, teams: { id: strin
teamId: team.id, teamId: team.id,
timeRemaining: initialTime, timeRemaining: initialTime,
})); }));
const db = database(); const db = database();
await db.insert(schema.draftTimers).values(timerData); await db.insert(schema.draftTimers).values(timerData);
} }
export async function getTeamTimer(teamId: string) { export async function getTeamTimer(seasonId: string, teamId: string) {
const db = database(); const db = database();
const [timer] = await db const [timer] = await db
.select() .select()
.from(schema.draftTimers) .from(schema.draftTimers)
.where(eq(schema.draftTimers.teamId, teamId)); .where(and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId)));
return timer; return timer;
} }
@ -30,12 +30,12 @@ export async function getSeasonTimers(seasonId: string) {
.where(eq(schema.draftTimers.seasonId, seasonId)); .where(eq(schema.draftTimers.seasonId, seasonId));
} }
export async function updateTeamTimer(teamId: string, timeRemaining: number) { export async function updateTeamTimer(seasonId: string, teamId: string, timeRemaining: number) {
const db = database(); const db = database();
const [timer] = await db const [timer] = await db
.update(schema.draftTimers) .update(schema.draftTimers)
.set({ timeRemaining, updatedAt: new Date() }) .set({ timeRemaining, updatedAt: new Date() })
.where(eq(schema.draftTimers.teamId, teamId)) .where(and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId)))
.returning(); .returning();
return timer; return timer;
} }

View file

@ -61,17 +61,26 @@ export async function action(args: any) {
) )
); );
let newTime: number;
if (!currentTimer) { if (!currentTimer) {
return Response.json({ error: "Timer not found for this team" }, { status: 404 }); if (adjustment <= 0) {
return Response.json({ error: "Timer not found for this team" }, { status: 404 });
}
newTime = adjustment;
await db.insert(schema.draftTimers).values({
seasonId,
teamId,
timeRemaining: newTime,
});
} else {
newTime = Math.max(0, currentTimer.timeRemaining + adjustment);
await db
.update(schema.draftTimers)
.set({ timeRemaining: newTime, updatedAt: new Date() })
.where(eq(schema.draftTimers.id, currentTimer.id));
} }
const newTime = Math.max(0, currentTimer.timeRemaining + adjustment);
await db
.update(schema.draftTimers)
.set({ timeRemaining: newTime, updatedAt: new Date() })
.where(eq(schema.draftTimers.id, currentTimer.id));
try { try {
getSocketIO() getSocketIO()
.to(`draft-${seasonId}`) .to(`draft-${seasonId}`)

View file

@ -148,9 +148,9 @@ export async function action(args: any) {
), ),
}); });
const incrementTime = season.draftIncrementTime || 30;
const newTimeRemaining = (currentTimer?.timeRemaining ?? 0) + incrementTime;
if (currentTimer) { if (currentTimer) {
const incrementTime = season.draftIncrementTime || 30;
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
await db await db
.update(schema.draftTimers) .update(schema.draftTimers)
.set({ .set({
@ -158,18 +158,24 @@ export async function action(args: any) {
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(schema.draftTimers.id, currentTimer.id)); .where(eq(schema.draftTimers.id, currentTimer.id));
} else {
await db.insert(schema.draftTimers).values({
seasonId,
teamId,
timeRemaining: newTimeRemaining,
});
}
// Emit timer update to all clients // Emit timer update to all clients
try { try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId, seasonId,
teamId, teamId,
timeRemaining: newTimeRemaining, timeRemaining: newTimeRemaining,
currentPickNumber: pickNumber, currentPickNumber: pickNumber,
}); });
} catch (error) { } catch (error) {
console.error("Socket.IO timer-update error:", error); console.error("Socket.IO timer-update error:", error);
}
} }
// Initialize timer for the next team BEFORE updating pick number (prevents race condition) // Initialize timer for the next team BEFORE updating pick number (prevents race condition)

View file

@ -186,25 +186,29 @@ export async function action(args: any) {
), ),
}); });
if (!currentTimer) { const newTimeRemaining = (currentTimer?.timeRemaining ?? 0) + incrementTime;
console.warn(`[Pick] No timer found for team ${currentDraftSlot.teamId} in season ${seasonId}`); if (currentTimer) {
} else {
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
await db await db
.update(schema.draftTimers) .update(schema.draftTimers)
.set({ timeRemaining: newTimeRemaining, updatedAt: new Date() }) .set({ timeRemaining: newTimeRemaining, updatedAt: new Date() })
.where(eq(schema.draftTimers.id, currentTimer.id)); .where(eq(schema.draftTimers.id, currentTimer.id));
} else {
await db.insert(schema.draftTimers).values({
seasonId,
teamId: currentDraftSlot.teamId,
timeRemaining: newTimeRemaining,
});
}
try { try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId, seasonId,
teamId: currentDraftSlot.teamId, teamId: currentDraftSlot.teamId,
timeRemaining: newTimeRemaining, timeRemaining: newTimeRemaining,
currentPickNumber, currentPickNumber,
}); });
} catch (error) { } catch (error) {
console.error("Socket.IO timer-update error:", error); console.error("Socket.IO timer-update error:", error);
}
} }
// Next team's timer is unchanged — their bank carries forward as-is // Next team's timer is unchanged — their bank carries forward as-is