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 * 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) {
const timerData = teams.map((team) => ({
@ -8,17 +8,17 @@ export async function initializeDraftTimers(seasonId: string, teams: { id: strin
teamId: team.id,
timeRemaining: initialTime,
}));
const db = database();
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 [timer] = await db
.select()
.from(schema.draftTimers)
.where(eq(schema.draftTimers.teamId, teamId));
.where(and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId)));
return timer;
}
@ -30,12 +30,12 @@ export async function getSeasonTimers(seasonId: string) {
.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 [timer] = await db
.update(schema.draftTimers)
.set({ timeRemaining, updatedAt: new Date() })
.where(eq(schema.draftTimers.teamId, teamId))
.where(and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId)))
.returning();
return timer;
}

View file

@ -61,17 +61,26 @@ export async function action(args: any) {
)
);
let newTime: number;
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 {
getSocketIO()
.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) {
const incrementTime = season.draftIncrementTime || 30;
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
await db
.update(schema.draftTimers)
.set({
@ -158,18 +158,24 @@ export async function action(args: any) {
updatedAt: new Date(),
})
.where(eq(schema.draftTimers.id, currentTimer.id));
} else {
await db.insert(schema.draftTimers).values({
seasonId,
teamId,
timeRemaining: newTimeRemaining,
});
}
// Emit timer update to all clients
try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId,
timeRemaining: newTimeRemaining,
currentPickNumber: pickNumber,
});
} catch (error) {
console.error("Socket.IO timer-update error:", error);
}
// Emit timer update to all clients
try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId,
timeRemaining: newTimeRemaining,
currentPickNumber: pickNumber,
});
} catch (error) {
console.error("Socket.IO timer-update error:", error);
}
// 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) {
console.warn(`[Pick] No timer found for team ${currentDraftSlot.teamId} in season ${seasonId}`);
} else {
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
const newTimeRemaining = (currentTimer?.timeRemaining ?? 0) + incrementTime;
if (currentTimer) {
await db
.update(schema.draftTimers)
.set({ timeRemaining: newTimeRemaining, updatedAt: new Date() })
.where(eq(schema.draftTimers.id, currentTimer.id));
} else {
await db.insert(schema.draftTimers).values({
seasonId,
teamId: currentDraftSlot.teamId,
timeRemaining: newTimeRemaining,
});
}
try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId: currentDraftSlot.teamId,
timeRemaining: newTimeRemaining,
currentPickNumber,
});
} catch (error) {
console.error("Socket.IO timer-update error:", error);
}
try {
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId: currentDraftSlot.teamId,
timeRemaining: newTimeRemaining,
currentPickNumber,
});
} catch (error) {
console.error("Socket.IO timer-update error:", error);
}
// Next team's timer is unchanged — their bank carries forward as-is