2025-10-16 00:32:48 -07:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2025-10-24 21:12:07 -07:00
|
|
|
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
|
2025-10-16 00:32:48 -07:00
|
|
|
import { getTeamQueue } from "./draft-queue";
|
2025-10-24 21:12:07 -07:00
|
|
|
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
|
|
|
|
import { getParticipantsForSeasonWithSports } from "./participant";
|
|
|
|
|
import { getSeasonSportsSimple } from "./season-sport";
|
|
|
|
|
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
import { getSocketIO } from "../../server/socket";
|
2025-10-16 00:32:48 -07:00
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
/**
|
|
|
|
|
* Check if the next team has autodraft enabled and immediately execute their pick
|
|
|
|
|
* This is called after a pick is made to chain autodraft picks
|
|
|
|
|
*/
|
|
|
|
|
export async function checkAndTriggerNextAutodraft(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
nextPickNumber: number;
|
|
|
|
|
totalTeams: number;
|
|
|
|
|
draftSlots: any[];
|
|
|
|
|
db?: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<void> {
|
|
|
|
|
const { seasonId, nextPickNumber, totalTeams, draftSlots, db: providedDb } = params;
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
// Calculate which team is next using snake draft logic
|
|
|
|
|
const nextRound = Math.ceil(nextPickNumber / totalTeams);
|
|
|
|
|
const isNextRoundEven = nextRound % 2 === 0;
|
|
|
|
|
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
|
|
|
|
|
|
|
|
|
|
// Apply snake draft reversal for even rounds
|
|
|
|
|
if (isNextRoundEven) {
|
|
|
|
|
nextPickInRound = totalTeams - nextPickInRound + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
|
|
|
|
|
if (!nextDraftSlot) return;
|
|
|
|
|
|
|
|
|
|
const nextTeamId = nextDraftSlot.teamId;
|
|
|
|
|
|
|
|
|
|
// Check if next team has autodraft enabled
|
|
|
|
|
const autodraftSettings = await db.query.autodraftSettings.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.autodraftSettings.seasonId, seasonId),
|
|
|
|
|
eq(schema.autodraftSettings.teamId, nextTeamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (autodraftSettings?.isEnabled) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${nextPickNumber}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Immediately execute autopick for this team
|
|
|
|
|
await executeAutoPick({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId: nextTeamId,
|
|
|
|
|
pickNumber: nextPickNumber,
|
|
|
|
|
triggeredBy: "timer", // Use "timer" to indicate automatic (not commissioner-forced)
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutodraftChain] Team ${nextTeamId} does not have autodraft enabled, waiting for manual pick`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
/**
|
|
|
|
|
* Auto-pick for a team when their timer runs out
|
2025-10-25 10:14:36 -07:00
|
|
|
* 1. Check queue - pick first eligible item if available (cleans up ineligible items)
|
2025-10-24 21:12:07 -07:00
|
|
|
* 2. If queue empty, pick highest EV participant not drafted from eligible sports
|
|
|
|
|
*
|
|
|
|
|
* Updated to respect Omni league draft eligibility rules
|
2025-10-16 00:32:48 -07:00
|
|
|
*/
|
2025-10-24 21:12:07 -07:00
|
|
|
export async function autoPickForTeam(
|
|
|
|
|
seasonId: string,
|
|
|
|
|
teamId: string,
|
|
|
|
|
draftRounds: number,
|
2025-10-26 20:35:55 -07:00
|
|
|
allTeamIds: string[],
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
2025-10-24 21:12:07 -07:00
|
|
|
) {
|
2025-10-26 20:35:55 -07:00
|
|
|
const db = providedDb || database();
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
// Calculate eligibility for this team
|
2025-10-26 20:35:55 -07:00
|
|
|
const allPicks = await getDraftPicksWithSports(seasonId, db);
|
|
|
|
|
const teamPicks = await getTeamDraftPicksWithSports(teamId, seasonId, db);
|
|
|
|
|
const allParticipants = await getParticipantsForSeasonWithSports(seasonId, db);
|
|
|
|
|
const seasonSports = await getSeasonSportsSimple(seasonId, db);
|
2025-10-24 21:12:07 -07:00
|
|
|
const allTeams = allTeamIds.map((id) => ({ id }));
|
|
|
|
|
|
|
|
|
|
const eligibility = calculateDraftEligibility(
|
|
|
|
|
teamId,
|
|
|
|
|
teamPicks,
|
|
|
|
|
allPicks,
|
|
|
|
|
allParticipants,
|
|
|
|
|
seasonSports,
|
|
|
|
|
draftRounds,
|
|
|
|
|
allTeams
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Team ${teamId} eligible sports:`,
|
|
|
|
|
Array.from(eligibility.eligibleSportIds)
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-24 21:12:07 -07:00
|
|
|
// Check queue first - filter by eligible sports
|
2025-10-26 20:35:55 -07:00
|
|
|
const queue = await getTeamQueue(teamId, db);
|
2025-10-24 21:12:07 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (queue.length > 0) {
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`);
|
|
|
|
|
|
|
|
|
|
// Get participant details for queue items to check eligibility
|
2025-10-24 21:12:07 -07:00
|
|
|
const queueParticipantIds = queue.map((item) => item.participantId);
|
|
|
|
|
const queueParticipants = await db.query.participants.findMany({
|
|
|
|
|
where: inArray(schema.participants.id, queueParticipantIds),
|
|
|
|
|
with: {
|
|
|
|
|
sportsSeason: {
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
const ineligibleQueueItemIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
// Try queue items in order, checking both drafted status and sport eligibility
|
2025-10-24 21:12:07 -07:00
|
|
|
for (const item of queue) {
|
|
|
|
|
const participant = queueParticipants.find((p) => p.id === item.participantId);
|
2025-10-25 10:14:36 -07:00
|
|
|
if (!participant) {
|
|
|
|
|
console.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
const sportId = participant.sportsSeason.sport.id;
|
|
|
|
|
const isEligible = eligibility.eligibleSportIds.has(sportId);
|
2025-10-28 23:40:29 -07:00
|
|
|
const isDrafted = await isParticipantDrafted(seasonId, item.participantId, db);
|
2025-10-24 21:12:07 -07:00
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
if (isDrafted) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove`
|
|
|
|
|
);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isEligible) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - not eligible for this team, will remove`
|
|
|
|
|
);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Found a valid pick from queue
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Selecting from queue: ${participant.name} (${participant.sportsSeason.sport.name})`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Clean up ineligible items from queue before returning
|
|
|
|
|
if (ineligibleQueueItemIds.length > 0) {
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
|
|
|
|
console.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
return item.participantId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All queue items were ineligible or drafted - clean them up
|
|
|
|
|
if (ineligibleQueueItemIds.length > 0) {
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue (all items were invalid)`
|
|
|
|
|
);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
// Queue is empty or all queued players drafted/ineligible
|
|
|
|
|
// Pick highest EV available from eligible sports
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`);
|
2025-10-26 20:35:55 -07:00
|
|
|
return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds, db);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the highest EV participant that hasn't been drafted yet
|
2025-10-24 21:12:07 -07:00
|
|
|
* Updated to filter by eligible sports
|
2025-10-16 00:32:48 -07:00
|
|
|
*/
|
2025-10-24 21:12:07 -07:00
|
|
|
export async function getTopAvailableParticipant(
|
|
|
|
|
seasonId: string,
|
2025-10-26 20:35:55 -07:00
|
|
|
eligibleSportIds?: Set<string>,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
2025-10-24 21:12:07 -07:00
|
|
|
) {
|
2025-10-26 20:35:55 -07:00
|
|
|
const db = providedDb || database();
|
2025-10-16 00:32:48 -07:00
|
|
|
|
|
|
|
|
// Get all drafted participant IDs
|
|
|
|
|
const draftedPicks = await db
|
|
|
|
|
.select({ participantId: schema.draftPicks.participantId })
|
|
|
|
|
.from(schema.draftPicks)
|
|
|
|
|
.where(eq(schema.draftPicks.seasonId, seasonId));
|
|
|
|
|
|
|
|
|
|
const draftedIds = draftedPicks.map((p: { participantId: string }) => p.participantId);
|
|
|
|
|
|
2025-10-24 21:12:07 -07:00
|
|
|
// Get all participants from season sports, filtered by eligible sports if provided
|
|
|
|
|
let seasonSportsData;
|
|
|
|
|
if (eligibleSportIds && eligibleSportIds.size > 0) {
|
|
|
|
|
// Filter to only eligible sports
|
|
|
|
|
seasonSportsData = await db
|
|
|
|
|
.select({
|
|
|
|
|
sportsSeasonId: schema.seasonSports.sportsSeasonId,
|
|
|
|
|
sportId: schema.sports.id,
|
|
|
|
|
})
|
|
|
|
|
.from(schema.seasonSports)
|
|
|
|
|
.innerJoin(
|
|
|
|
|
schema.sportsSeasons,
|
|
|
|
|
eq(schema.seasonSports.sportsSeasonId, schema.sportsSeasons.id)
|
|
|
|
|
)
|
|
|
|
|
.innerJoin(
|
|
|
|
|
schema.sports,
|
|
|
|
|
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
|
|
|
|
)
|
|
|
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
|
|
|
|
|
|
|
|
// Filter to only eligible sports
|
|
|
|
|
seasonSportsData = seasonSportsData.filter((s: { sportId: string }) =>
|
|
|
|
|
eligibleSportIds.has(s.sportId)
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// No filtering - get all sports
|
|
|
|
|
seasonSportsData = await db
|
|
|
|
|
.select({ sportsSeasonId: schema.seasonSports.sportsSeasonId })
|
|
|
|
|
.from(schema.seasonSports)
|
|
|
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sportsSeasonIds = seasonSportsData.map(
|
|
|
|
|
(s: { sportsSeasonId: string }) => s.sportsSeasonId
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (sportsSeasonIds.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get top available participant by EV
|
|
|
|
|
let query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
|
|
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
|
|
|
|
|
|
|
|
// Filter out drafted participants if any exist
|
|
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle multiple sports seasons
|
|
|
|
|
if (sportsSeasonIds.length > 1) {
|
|
|
|
|
// For simplicity, we'll query all and sort in memory
|
|
|
|
|
// In production, might want to optimize this
|
|
|
|
|
const allParticipants = [];
|
|
|
|
|
|
|
|
|
|
for (const sportsSeasonId of sportsSeasonIds) {
|
|
|
|
|
let participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
|
|
|
|
|
|
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const seasonParticipants = await participantQuery;
|
|
|
|
|
allParticipants.push(...seasonParticipants);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 11:26:40 -08:00
|
|
|
// Sort by EV desc, then name (expectedValue is a decimal string from DB)
|
2025-10-16 00:32:48 -07:00
|
|
|
allParticipants.sort((a, b) => {
|
2026-02-19 11:26:40 -08:00
|
|
|
const evA = parseFloat(String(a.expectedValue)) || 0;
|
|
|
|
|
const evB = parseFloat(String(b.expectedValue)) || 0;
|
|
|
|
|
if (evB !== evA) {
|
|
|
|
|
return evB - evA;
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return allParticipants[0]?.id || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [topParticipant] = await query;
|
|
|
|
|
return topParticipant?.id || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the current pick based on draft order and round
|
|
|
|
|
*/
|
|
|
|
|
export function calculatePickInfo(
|
|
|
|
|
pickNumber: number,
|
|
|
|
|
teamCount: number
|
|
|
|
|
): { round: number; pickInRound: number; teamIndex: number } {
|
|
|
|
|
const round = Math.ceil(pickNumber / teamCount);
|
|
|
|
|
const pickInRound = ((pickNumber - 1) % teamCount) + 1;
|
|
|
|
|
|
|
|
|
|
// Snake draft: odd rounds go forward, even rounds go backward
|
|
|
|
|
const isOddRound = round % 2 === 1;
|
|
|
|
|
const teamIndex = isOddRound ? pickInRound - 1 : teamCount - pickInRound;
|
|
|
|
|
|
|
|
|
|
return { round, pickInRound, teamIndex };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the team ID for a given pick number based on draft order
|
|
|
|
|
*/
|
|
|
|
|
export function getTeamForPick(
|
|
|
|
|
pickNumber: number,
|
|
|
|
|
draftOrder: { teamId: string; draftOrder: number }[]
|
|
|
|
|
): string | null {
|
|
|
|
|
const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder);
|
|
|
|
|
const teamCount = sortedOrder.length;
|
2025-10-25 10:14:36 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (teamCount === 0) return null;
|
2025-10-25 10:14:36 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
const { teamIndex } = calculatePickInfo(pickNumber, teamCount);
|
|
|
|
|
return sortedOrder[teamIndex]?.teamId || null;
|
|
|
|
|
}
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute an autopick for a team - unified function for both commissioner-forced and timer-based autopicks
|
|
|
|
|
*
|
|
|
|
|
* Selection Logic:
|
|
|
|
|
* 1. Uses the team's draft queue first (prioritizes manager's preferences)
|
|
|
|
|
* 2. Validates each queued participant for:
|
|
|
|
|
* - Not already drafted
|
|
|
|
|
* - Eligible based on draft rules (sport eligibility, flex spots, etc.)
|
|
|
|
|
* 3. Automatically removes ineligible participants from queue
|
|
|
|
|
* 4. If queue is empty or all items are ineligible, selects highest EV participant from eligible sports
|
|
|
|
|
*
|
|
|
|
|
* This ensures autopicks ALWAYS respect draft eligibility rules and cannot make illegal selections.
|
|
|
|
|
*
|
|
|
|
|
* @param params.seasonId - The season ID
|
|
|
|
|
* @param params.teamId - The team making the pick
|
|
|
|
|
* @param params.pickNumber - The current pick number
|
|
|
|
|
* @param params.triggeredBy - Who/what triggered the autopick ("commissioner" or "timer")
|
|
|
|
|
* @param params.commissionerUserId - User ID of commissioner (required if triggeredBy is "commissioner")
|
|
|
|
|
* @param params.autodraftSettings - Autodraft settings (used for timer-based picks)
|
|
|
|
|
* @param params.db - Database instance (optional, will use database() if not provided)
|
|
|
|
|
* @returns Result object with success status and pick data
|
|
|
|
|
*/
|
|
|
|
|
export async function executeAutoPick(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
teamId: string;
|
|
|
|
|
pickNumber: number;
|
|
|
|
|
triggeredBy: "commissioner" | "timer";
|
|
|
|
|
commissionerUserId?: string;
|
|
|
|
|
autodraftSettings?: any;
|
|
|
|
|
db?: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<{
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
pick?: any;
|
|
|
|
|
participant?: any;
|
|
|
|
|
nextPickNumber?: number;
|
|
|
|
|
isDraftComplete?: boolean;
|
|
|
|
|
}> {
|
|
|
|
|
const {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
pickNumber,
|
|
|
|
|
triggeredBy,
|
|
|
|
|
commissionerUserId,
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
db: providedDb,
|
|
|
|
|
} = params;
|
|
|
|
|
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Race condition protection - check if pick already made
|
|
|
|
|
const existingPick = await db.query.draftPicks.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.draftPicks.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftPicks.pickNumber, pickNumber)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existingPick) {
|
|
|
|
|
console.log(`[AutoPick] Pick ${pickNumber} already made, skipping`);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Pick already made",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get season details
|
|
|
|
|
const season = await db.query.seasons.findFirst({
|
|
|
|
|
where: eq(schema.seasons.id, seasonId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!season) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Season not found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get draft slots to calculate round/pickInRound and get all team IDs
|
|
|
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
|
|
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
|
|
|
|
orderBy: schema.draftSlots.draftOrder,
|
|
|
|
|
with: {
|
|
|
|
|
team: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const totalTeams = draftSlots.length;
|
|
|
|
|
if (totalTeams === 0) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "No draft slots found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allTeamIds = draftSlots.map((slot) => slot.teamId);
|
|
|
|
|
|
|
|
|
|
// Use autoPickForTeam to select participant (respects eligibility and queue)
|
|
|
|
|
const participantId = await autoPickForTeam(
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
season.draftRounds,
|
2025-10-26 20:35:55 -07:00
|
|
|
allTeamIds,
|
|
|
|
|
db
|
2025-10-25 10:14:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!participantId) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "No eligible participants available to pick",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get participant details
|
|
|
|
|
const participantToPick = await db.query.participants.findFirst({
|
|
|
|
|
where: eq(schema.participants.id, participantId),
|
|
|
|
|
with: {
|
|
|
|
|
sportsSeason: {
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!participantToPick) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Participant not found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate round and pickInRound with snake draft logic
|
|
|
|
|
const currentRound = Math.ceil(pickNumber / totalTeams);
|
|
|
|
|
const isEvenRound = currentRound % 2 === 0;
|
|
|
|
|
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
|
|
|
|
|
|
|
|
|
// Apply snake draft reversal for even rounds
|
|
|
|
|
if (isEvenRound) {
|
|
|
|
|
pickInRound = totalTeams - pickInRound + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine pickedByUserId based on trigger
|
|
|
|
|
const pickedByUserId = triggeredBy === "commissioner"
|
|
|
|
|
? (commissionerUserId || "")
|
|
|
|
|
: "";
|
|
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Fetch current timer before pick so we have the time remaining at decision point
|
|
|
|
|
const incrementTime = season.draftIncrementTime || 30;
|
|
|
|
|
const currentTimer = await db.query.draftTimers.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.draftTimers.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftTimers.teamId, teamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!currentTimer) {
|
|
|
|
|
console.warn(`[AutoPick] No timer found for team ${teamId} in season ${seasonId}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
// Create the draft pick
|
|
|
|
|
const [draftPick] = await db
|
|
|
|
|
.insert(schema.draftPicks)
|
|
|
|
|
.values({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
participantId: participantToPick.id,
|
|
|
|
|
pickNumber,
|
|
|
|
|
round: currentRound,
|
|
|
|
|
pickInRound,
|
|
|
|
|
pickedByUserId,
|
|
|
|
|
pickedByType: "auto",
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// timeRemaining at pick moment: 0 when timer expired, full bank for immediate autodraft
|
|
|
|
|
timeUsed: currentTimer ? currentTimer.timeRemaining : undefined,
|
2025-10-25 10:14:36 -07:00
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}`
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Calculate next pick info (before updating season)
|
2025-10-25 10:14:36 -07:00
|
|
|
const nextPickNumber = pickNumber + 1;
|
|
|
|
|
const totalPicks = totalTeams * season.draftRounds;
|
|
|
|
|
const isDraftComplete = nextPickNumber > totalPicks;
|
|
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Add increment to the team that just picked (post-pick reward)
|
2025-10-25 10:14:36 -07:00
|
|
|
if (currentTimer) {
|
|
|
|
|
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.draftTimers)
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
.set({ timeRemaining: newTimeRemaining, updatedAt: new Date() })
|
2025-10-25 10:14:36 -07:00
|
|
|
.where(eq(schema.draftTimers.id, currentTimer.id));
|
|
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Added ${incrementTime}s increment to team ${teamId} after pick: ${currentTimer.timeRemaining}s → ${newTimeRemaining}s`
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
try {
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
timeRemaining: newTimeRemaining,
|
|
|
|
|
currentPickNumber: pickNumber,
|
|
|
|
|
});
|
2025-10-25 10:14:36 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO timer-update error:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Next team's timer is unchanged — their bank carries forward as-is
|
2025-10-25 22:11:10 -07:00
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Update season's current pick number
|
2025-10-25 22:11:10 -07:00
|
|
|
await db
|
|
|
|
|
.update(schema.seasons)
|
|
|
|
|
.set({
|
|
|
|
|
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
status: isDraftComplete ? "active" : season.status,
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.seasons.id, seasonId));
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
// Remove from ALL team queues in this season (participant is now drafted)
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.draftQueue.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftQueue.participantId, participantToPick.id)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle autodraft settings for timer-based picks with "next_pick" mode
|
|
|
|
|
if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
|
|
|
|
|
console.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`);
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.autodraftSettings)
|
|
|
|
|
.set({
|
|
|
|
|
isEnabled: false,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
|
|
|
|
|
|
|
|
|
|
// Emit autodraft-updated event
|
|
|
|
|
try {
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
getSocketIO().to(`draft-${seasonId}`).emit("autodraft-updated", {
|
|
|
|
|
teamId,
|
|
|
|
|
isEnabled: false,
|
|
|
|
|
mode: autodraftSettings.mode,
|
|
|
|
|
});
|
2025-10-25 10:14:36 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO autodraft-updated error:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emit socket events
|
|
|
|
|
try {
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
const io = getSocketIO();
|
|
|
|
|
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team;
|
2025-10-25 10:14:36 -07:00
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Emit participant-removed-from-queues event
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", {
|
|
|
|
|
participantId: participantToPick.id,
|
|
|
|
|
});
|
2025-10-25 10:14:36 -07:00
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Emit pick-made event
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("pick-made", {
|
|
|
|
|
pick: {
|
|
|
|
|
...draftPick,
|
|
|
|
|
team,
|
|
|
|
|
participant: {
|
|
|
|
|
...participantToPick,
|
2025-10-25 10:14:36 -07:00
|
|
|
sport: participantToPick.sportsSeason.sport,
|
|
|
|
|
},
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
sport: participantToPick.sportsSeason.sport,
|
|
|
|
|
},
|
|
|
|
|
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
isDraftComplete,
|
|
|
|
|
});
|
2025-10-25 10:14:36 -07:00
|
|
|
|
Add draft clock UI, Fischer increment timer logic, and security fixes (#19)
- Add prominent clock badge to tab bar (lights up on your turn) with
correct Fischer increment chess-clock model: bank starts at initialTime,
+= incrementTime after each pick, other teams' banks untouched
- Extract pure timer helpers to app/lib/draft-timer.ts and add 29 unit
tests covering formatClockTime, calculateTimeAfterPick, getTimerColorClass,
and full snake-draft lifecycle regression scenarios
- Fix make-pick.ts: add status !== 'draft' and draftPaused server guards
- Fix draft-utils.ts: replace (global as any).__socketIO with getSocketIO(),
fix timeUsed to store actual timeRemaining at pick moment (not always 120),
add null timer warning, move timer fetch before pick insert
- Fix draft.start.ts: batch timer inserts, guard against empty draftSlots
- Fix DraftGridSection: memoize currentTeamId, widen teamTimers type to
Record<string, number | undefined>
- Fix duplicate animate-pulse (getTimerColorClass already includes it)
- Clamp negative seconds in formatClockTime to guard against timer drift
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:51:12 -08:00
|
|
|
// Emit draft-completed event if applicable
|
|
|
|
|
if (isDraftComplete) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("draft-completed");
|
2025-10-25 10:14:36 -07:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO events error:", error);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Check if next team has autodraft enabled and trigger immediately
|
|
|
|
|
if (!isDraftComplete) {
|
|
|
|
|
await checkAndTriggerNextAutodraft({
|
|
|
|
|
seasonId,
|
|
|
|
|
nextPickNumber,
|
|
|
|
|
totalTeams,
|
|
|
|
|
draftSlots,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
pick: draftPick,
|
|
|
|
|
participant: participantToPick,
|
|
|
|
|
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
isDraftComplete,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Error in executeAutoPick:", error);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|