The rollback endpoint was deleting all timers but only recreating one for the team currently on the clock. After that team made their pick, the next team had no timer row, causing the timer system to log "No timer found" and skip indefinitely, freezing the draft. Fix: recreate timers for ALL teams after a rollback, each starting at draftInitialTime. Also add a defensive fallback in the timer system: if a timer row is missing for the current team during an active draft, create it at draftInitialTime instead of skipping, so the draft can never get permanently stuck due to a missing timer. https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import { getAuth } from "@clerk/react-router/server";
|
|
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
import { eq, and, gte } from "drizzle-orm";
|
|
import { getSocketIO } from "../../../server/socket";
|
|
|
|
export async function action(args: any) {
|
|
const { request } = args;
|
|
const auth = await getAuth(args);
|
|
const userId = (auth as any).userId as string | null;
|
|
|
|
if (!userId) {
|
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
|
|
const formData = await request.formData();
|
|
const seasonId = formData.get("seasonId") as string;
|
|
const pickNumber = parseInt(formData.get("pickNumber") as string);
|
|
|
|
if (!seasonId || isNaN(pickNumber) || pickNumber < 1) {
|
|
return Response.json({ error: "Missing required fields" }, { status: 400 });
|
|
}
|
|
|
|
const db = database();
|
|
|
|
const season = await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, seasonId),
|
|
});
|
|
|
|
if (!season) {
|
|
return Response.json({ error: "Season not found" }, { status: 404 });
|
|
}
|
|
|
|
const isCommissioner = await db.query.commissioners.findFirst({
|
|
where: and(
|
|
eq(schema.commissioners.leagueId, season.leagueId),
|
|
eq(schema.commissioners.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!isCommissioner) {
|
|
return Response.json({ error: "Only commissioners can roll back the draft" }, { status: 403 });
|
|
}
|
|
|
|
if (season.status !== "draft") {
|
|
return Response.json(
|
|
{ error: "Cannot roll back a draft that is not in progress" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
|
orderBy: schema.draftSlots.draftOrder,
|
|
});
|
|
|
|
const totalTeams = draftSlots.length;
|
|
|
|
// Calculate which team is on the clock at the rollback pick (snake draft)
|
|
const round = Math.ceil(pickNumber / totalTeams);
|
|
const isEvenRound = round % 2 === 0;
|
|
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
|
if (isEvenRound) {
|
|
pickInRound = totalTeams - pickInRound + 1;
|
|
}
|
|
const rollbackSlot = draftSlots.find((slot) => slot.draftOrder === pickInRound);
|
|
|
|
// Delete picks from pickNumber onwards
|
|
await db
|
|
.delete(schema.draftPicks)
|
|
.where(
|
|
and(
|
|
eq(schema.draftPicks.seasonId, seasonId),
|
|
gte(schema.draftPicks.pickNumber, pickNumber)
|
|
)
|
|
);
|
|
|
|
// Clear all timers and recreate them for every team so no team is left without a timer
|
|
await db
|
|
.delete(schema.draftTimers)
|
|
.where(eq(schema.draftTimers.seasonId, seasonId));
|
|
|
|
const initialTime = season.draftInitialTime || 120;
|
|
|
|
await db.insert(schema.draftTimers).values(
|
|
draftSlots.map((slot) => ({
|
|
seasonId,
|
|
teamId: slot.teamId,
|
|
timeRemaining: initialTime,
|
|
}))
|
|
);
|
|
|
|
// Update season: reset pick number and unpause
|
|
await db
|
|
.update(schema.seasons)
|
|
.set({
|
|
currentPickNumber: pickNumber,
|
|
draftPaused: false,
|
|
})
|
|
.where(eq(schema.seasons.id, seasonId));
|
|
|
|
// Emit socket events
|
|
try {
|
|
const io = getSocketIO();
|
|
|
|
io.to(`draft-${seasonId}`).emit("draft-rolled-back", {
|
|
seasonId,
|
|
pickNumber,
|
|
teamId: rollbackSlot?.teamId,
|
|
});
|
|
|
|
if (rollbackSlot) {
|
|
io.to(`draft-${seasonId}`).emit("timer-update", {
|
|
seasonId,
|
|
teamId: rollbackSlot.teamId,
|
|
timeRemaining: initialTime,
|
|
currentPickNumber: pickNumber,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Socket.IO error:", error);
|
|
}
|
|
|
|
return Response.json({ success: true, pickNumber });
|
|
}
|