2025-10-16 00:32:48 -07:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2026-02-23 23:23:24 -08:00
|
|
|
import { eq, and, notInArray, desc, inArray, sql, asc } from "drizzle-orm";
|
2026-03-21 13:41:39 -07:00
|
|
|
import { logger } from "~/lib/logger";
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
import type { InferSelectModel } from "drizzle-orm";
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
import { getTeamQueue, getAllQueuesForSeason } 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
|
|
|
|
|
*/
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
type DraftSlot = { teamId: string; draftOrder: number };
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
export async function checkAndTriggerNextAutodraft(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
nextPickNumber: number;
|
|
|
|
|
totalTeams: number;
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
draftSlots: DraftSlot[];
|
2025-10-25 22:11:10 -07:00
|
|
|
db?: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<void> {
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
const { seasonId, totalTeams, draftSlots, db: providedDb } = params;
|
2025-10-25 22:11:10 -07:00
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
let currentPickNumber = params.nextPickNumber;
|
2025-10-25 22:11:10 -07:00
|
|
|
|
2026-02-23 23:23:24 -08:00
|
|
|
// Cap iterations at totalTeams: in the worst case every team has autodraft enabled,
|
|
|
|
|
// so we make at most totalTeams consecutive picks before handing back to the timer loop.
|
|
|
|
|
const maxIterations = params.totalTeams;
|
|
|
|
|
let iterations = 0;
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2026-02-23 23:23:24 -08:00
|
|
|
// Iteratively execute autodraft picks for consecutive teams with autodraft enabled
|
|
|
|
|
while (iterations < maxIterations) {
|
|
|
|
|
iterations++;
|
|
|
|
|
const { pickInRound: nextPickInRound } = calculatePickInfo(currentPickNumber, totalTeams);
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
|
|
|
|
|
if (!nextDraftSlot) return;
|
2025-10-25 22:11:10 -07:00
|
|
|
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
const nextTeamId = nextDraftSlot.teamId;
|
2025-10-25 22:11:10 -07:00
|
|
|
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
const autodraftSettings = await db.query.autodraftSettings.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.autodraftSettings.seasonId, seasonId),
|
|
|
|
|
eq(schema.autodraftSettings.teamId, nextTeamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
2025-10-25 22:11:10 -07:00
|
|
|
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
if (!autodraftSettings?.isEnabled) return;
|
2025-10-25 22:11:10 -07:00
|
|
|
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${currentPickNumber}`
|
2025-10-25 22:11:10 -07:00
|
|
|
);
|
|
|
|
|
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
const result = await executeAutoPick({
|
2025-10-25 22:11:10 -07:00
|
|
|
seasonId,
|
|
|
|
|
teamId: nextTeamId,
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
pickNumber: currentPickNumber,
|
|
|
|
|
triggeredBy: "timer",
|
2025-10-25 22:11:10 -07:00
|
|
|
autodraftSettings,
|
|
|
|
|
db,
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
chainEnabled: false,
|
2025-10-25 22:11:10 -07:00
|
|
|
});
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
|
|
|
|
if (!result.success || result.isDraftComplete || !result.nextPickNumber) return;
|
|
|
|
|
|
|
|
|
|
currentPickNumber = result.nextPickNumber;
|
2025-10-25 22:11:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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[],
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
providedDb?: ReturnType<typeof database>,
|
|
|
|
|
queueOnly?: boolean
|
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
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`);
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`);
|
2025-10-25 10:14:36 -07:00
|
|
|
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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove`
|
|
|
|
|
);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isEligible) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[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
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[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));
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.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));
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[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
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
if (queueOnly) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] No valid queue items and queueOnly constraint is active — will not fall back to highest EV`);
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 21:12:07 -07:00
|
|
|
// Pick highest EV available from eligible sports
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.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;
|
|
|
|
|
}
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
|
|
|
|
// Handle multiple sports seasons: query each and sort in memory
|
2025-10-16 00:32:48 -07:00
|
|
|
if (sportsSeasonIds.length > 1) {
|
|
|
|
|
const allParticipants = [];
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
for (const sportsSeasonId of sportsSeasonIds) {
|
|
|
|
|
let participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
const seasonParticipants = await participantQuery;
|
|
|
|
|
allParticipants.push(...seasonParticipants);
|
|
|
|
|
}
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2026-04-09 02:27:40 +00:00
|
|
|
// Sort by VORP desc, then name
|
2025-10-16 00:32:48 -07:00
|
|
|
allParticipants.sort((a, b) => {
|
2026-04-09 02:27:40 +00:00
|
|
|
const vorpA = parseFloat(String(a.vorpValue)) || 0;
|
|
|
|
|
const vorpB = parseFloat(String(b.vorpValue)) || 0;
|
|
|
|
|
if (vorpB !== vorpA) {
|
|
|
|
|
return vorpB - vorpA;
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
return allParticipants[0]?.id || null;
|
|
|
|
|
}
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
|
|
|
|
// Single sport season
|
|
|
|
|
let query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
2026-04-09 02:27:40 +00:00
|
|
|
.orderBy(desc(schema.participants.vorpValue), schema.participants.name);
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
|
|
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-09 02:27:40 +00:00
|
|
|
.orderBy(desc(schema.participants.vorpValue), schema.participants.name);
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
}
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
const [topParticipant] = await query;
|
|
|
|
|
return topParticipant?.id || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 23:23:24 -08:00
|
|
|
* Calculate the current pick based on draft order and round (snake draft).
|
|
|
|
|
* Returns pickInRound that is already snake-adjusted and matches draftOrder values.
|
2025-10-16 00:32:48 -07:00
|
|
|
*/
|
|
|
|
|
export function calculatePickInfo(
|
|
|
|
|
pickNumber: number,
|
|
|
|
|
teamCount: number
|
|
|
|
|
): { round: number; pickInRound: number; teamIndex: number } {
|
|
|
|
|
const round = Math.ceil(pickNumber / teamCount);
|
2026-02-23 23:23:24 -08:00
|
|
|
const rawPickInRound = ((pickNumber - 1) % teamCount) + 1;
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
// Snake draft: odd rounds go forward, even rounds go backward
|
|
|
|
|
const isOddRound = round % 2 === 1;
|
2026-02-23 23:23:24 -08:00
|
|
|
const teamIndex = isOddRound ? rawPickInRound - 1 : teamCount - rawPickInRound;
|
|
|
|
|
const pickInRound = teamIndex + 1; // snake-adjusted, 1-based, matches draftOrder
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
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 {
|
2026-03-21 09:44:05 -07:00
|
|
|
const sortedOrder = [...draftOrder].toSorted((a, b) => a.draftOrder - b.draftOrder);
|
2025-10-16 00:32:48 -07:00
|
|
|
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
|
|
|
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
/**
|
|
|
|
|
* After a pick is committed, recalculate draft eligibility for every team and remove
|
|
|
|
|
* any queued participants whose sport is no longer eligible for that team.
|
|
|
|
|
*
|
|
|
|
|
* This handles cases like: a team has a snooker player queued but has now filled their
|
|
|
|
|
* last flex slot that snooker could have used — the pick should be proactively removed
|
|
|
|
|
* rather than silently failing or pausing the draft later.
|
|
|
|
|
*
|
|
|
|
|
* Returns the per-team removals so the caller can emit socket events.
|
|
|
|
|
*/
|
|
|
|
|
export async function pruneIneligibleQueueItems(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
draftRounds: number;
|
|
|
|
|
allTeamIds: string[];
|
|
|
|
|
db: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<{ teamId: string; removedParticipantIds: string[] }[]> {
|
|
|
|
|
const { seasonId, draftRounds, allTeamIds, db } = params;
|
|
|
|
|
|
|
|
|
|
const [allPicks, allParticipants, seasonSports, allQueues] = await Promise.all([
|
|
|
|
|
getDraftPicksWithSports(seasonId, db),
|
|
|
|
|
getParticipantsForSeasonWithSports(seasonId, db),
|
|
|
|
|
getSeasonSportsSimple(seasonId, db),
|
|
|
|
|
getAllQueuesForSeason(seasonId, db),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Build a fast lookup: participantId → sportId
|
|
|
|
|
const participantSportMap = new Map<string, string>();
|
|
|
|
|
for (const p of allParticipants) {
|
|
|
|
|
participantSportMap.set(p.id, p.sport.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allTeams = allTeamIds.map((id) => ({ id }));
|
|
|
|
|
const results: { teamId: string; removedParticipantIds: string[] }[] = [];
|
|
|
|
|
|
|
|
|
|
for (const teamId of allTeamIds) {
|
|
|
|
|
const queue = allQueues.get(teamId) ?? [];
|
|
|
|
|
if (queue.length === 0) continue;
|
|
|
|
|
|
|
|
|
|
const teamPicks = allPicks.filter((p) => p.teamId === teamId);
|
|
|
|
|
const eligibility = calculateDraftEligibility(
|
|
|
|
|
teamId,
|
|
|
|
|
teamPicks,
|
|
|
|
|
allPicks,
|
|
|
|
|
allParticipants,
|
|
|
|
|
seasonSports,
|
|
|
|
|
draftRounds,
|
|
|
|
|
allTeams
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ineligible: { id: string; participantId: string }[] = [];
|
|
|
|
|
for (const item of queue) {
|
|
|
|
|
const sportId = participantSportMap.get(item.participantId);
|
|
|
|
|
if (sportId === undefined) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.warn(
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
`[QueuePrune] Team ${teamId}: queue item ${item.id} references participant ${item.participantId} not found in season sports — skipping`
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!eligibility.eligibleSportIds.has(sportId)) {
|
|
|
|
|
ineligible.push({ id: item.id, participantId: item.participantId });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ineligible.length > 0) {
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(inArray(schema.draftQueue.id, ineligible.map((i) => i.id)));
|
|
|
|
|
|
|
|
|
|
const removedParticipantIds = ineligible.map((i) => i.participantId);
|
|
|
|
|
results.push({ teamId, removedParticipantIds });
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
`[QueuePrune] Team ${teamId}: removed ${ineligible.length} ineligible items (sport no longer eligible)`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
type AutodraftSettings = InferSelectModel<typeof schema.autodraftSettings>;
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
export async function executeAutoPick(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
teamId: string;
|
|
|
|
|
pickNumber: number;
|
|
|
|
|
triggeredBy: "commissioner" | "timer";
|
|
|
|
|
commissionerUserId?: string;
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
autodraftSettings?: AutodraftSettings | null;
|
2025-10-25 10:14:36 -07:00
|
|
|
db?: ReturnType<typeof database>;
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
chainEnabled?: boolean; // Set to false when called from within the autodraft chain to prevent recursion
|
2025-10-25 10:14:36 -07:00
|
|
|
}): Promise<{
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
Claude/fix pick timer ghll n (#29)
* Fix force-manual-pick resetting next team's timer to initial time
When a commissioner forced a manual pick, the next team's timer was
being reset to the initial time (2 minutes) instead of carrying
forward their existing time bank balance.
This aligns force-manual-pick with the behavior of regular user picks
and force-autopick: the picking team gets their increment added, and
the next team's timer is left untouched so their bank carries forward.
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add regression tests for draft.force-manual-pick timer behavior
18 tests across 5 describe blocks covering:
- Authorization (401/403)
- Input validation (missing fields, bad participant, ineligible sport)
- Successful pick (response shape, draft-complete detection, socket events)
- Timer behavior (increment added to picking team, new timer creation, additive not reset)
- Two regression tests confirming the next team's timer is never touched:
draftTimers.findFirst called exactly once, no timer-update emitted for
next team, db.update called exactly twice (not three times)
https://claude.ai/code/session_01X7gwWmafUSEvVHcV7Raz5p
* Add TypeScript types and improve draft validation (#28)
* Code review fixes: type safety, security hardening, and dead code removal
- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
() => void but are emitted with { seasonId, paused } data payloads
- Fix draft.force-manual-pick: add missing season.status === "draft" guard
so commissioners cannot force picks outside an active draft; add duplicate
pick-number check so a slot cannot be assigned two picks (the previous
code only checked participant uniqueness, not slot uniqueness)
- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
API routes and league loaders; replace (auth as any).userId casts with
proper const { userId } = await getAuth(args) destructuring
- Remove unused isSnakeDraft = true dead variable from draft.make-pick
- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
properly typed InferSelectModel / DraftSlot types
- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
the two-call flow; add new tests for status-check and slot-uniqueness
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
* Fix RouterContextProvider type errors in action test files
Cast context argument to RouterContextProvider in test helpers so
ActionFunctionArgs strict typing is satisfied without weakening the
production action signatures back to any.
https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 19:29:29 -08:00
|
|
|
pick?: InferSelectModel<typeof schema.draftPicks>;
|
|
|
|
|
participant?: InferSelectModel<typeof schema.participants> & {
|
|
|
|
|
sportsSeason: InferSelectModel<typeof schema.sportsSeasons> & {
|
|
|
|
|
sport: InferSelectModel<typeof schema.sports>;
|
|
|
|
|
};
|
|
|
|
|
};
|
2025-10-25 10:14:36 -07:00
|
|
|
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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] Pick ${pickNumber} already made, skipping`);
|
2025-10-25 10:14:36 -07:00
|
|
|
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),
|
2026-02-23 23:23:24 -08:00
|
|
|
orderBy: asc(schema.draftSlots.draftOrder),
|
2025-10-25 10:14:36 -07:00
|
|
|
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)
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
const queueOnly = autodraftSettings?.queueOnly ?? false;
|
2025-10-25 10:14:36 -07:00
|
|
|
const participantId = await autoPickForTeam(
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
season.draftRounds,
|
2025-10-26 20:35:55 -07:00
|
|
|
allTeamIds,
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
db,
|
|
|
|
|
queueOnly
|
2025-10-25 10:14:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!participantId) {
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
// If queueOnly is set and queue is empty, disable autodraft and return success
|
|
|
|
|
// (timer will fire again and pick highest EV once autodraft is disabled)
|
|
|
|
|
if (triggeredBy === "timer" && queueOnly && autodraftSettings) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] Queue empty with queueOnly constraint — disabling autodraft for team ${teamId}`);
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
await db
|
|
|
|
|
.update(schema.autodraftSettings)
|
|
|
|
|
.set({ isEnabled: false, updatedAt: new Date() })
|
|
|
|
|
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
getSocketIO().to(`draft-${seasonId}`).emit("autodraft-updated", {
|
|
|
|
|
teamId,
|
|
|
|
|
isEnabled: false,
|
|
|
|
|
mode: autodraftSettings.mode,
|
|
|
|
|
queueOnly: autodraftSettings.queueOnly,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Socket.IO autodraft-updated error (queue-empty shutoff):", error);
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
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",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 23:23:24 -08:00
|
|
|
const { round: currentRound, pickInRound } = calculatePickInfo(pickNumber, totalTeams);
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.warn(`[AutoPick] No timer found for team ${teamId} in season ${seasonId}`);
|
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
|
|
|
}
|
|
|
|
|
|
2026-03-24 19:09:38 -07:00
|
|
|
// Create the draft pick — use ON CONFLICT DO NOTHING so that concurrent timer
|
|
|
|
|
// ticks racing to the same pick slot are handled atomically at the DB level
|
|
|
|
|
// rather than relying on the TOCTOU pre-check above.
|
2025-10-25 10:14:36 -07:00
|
|
|
const [draftPick] = await db
|
|
|
|
|
.insert(schema.draftPicks)
|
|
|
|
|
.values({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
participantId: participantToPick.id,
|
|
|
|
|
pickNumber,
|
|
|
|
|
round: currentRound,
|
|
|
|
|
pickInRound,
|
|
|
|
|
pickedByUserId,
|
|
|
|
|
pickedByType: "auto",
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
// Records the team's bank balance at the moment the pick was made (seconds remaining)
|
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
|
|
|
timeUsed: currentTimer ? currentTimer.timeRemaining : undefined,
|
2025-10-25 10:14:36 -07:00
|
|
|
})
|
2026-03-24 19:09:38 -07:00
|
|
|
.onConflictDoNothing()
|
2025-10-25 10:14:36 -07:00
|
|
|
.returning();
|
|
|
|
|
|
2026-03-24 19:09:38 -07:00
|
|
|
if (!draftPick) {
|
|
|
|
|
// Another concurrent path already committed this pick
|
|
|
|
|
logger.log(`[AutoPick] Pick ${pickNumber} already made (conflict on insert), skipping`);
|
|
|
|
|
return { success: false, error: "Pick already made" };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2025-10-25 10:14:36 -07:00
|
|
|
`[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;
|
|
|
|
|
|
2026-03-24 17:00:32 -07:00
|
|
|
// Update the team's timer after the auto-pick.
|
2026-03-20 21:36:39 -07:00
|
|
|
// Standard mode: reset to the per-pick time (atomic, prevents race with timer loop).
|
2026-03-24 17:00:32 -07:00
|
|
|
// Chess clock mode: add the increment so the team starts their next turn with some time
|
|
|
|
|
// (without this, a single timeout would permanently freeze their bank at 0).
|
2026-03-20 21:36:39 -07:00
|
|
|
let emitTimeRemaining: number;
|
|
|
|
|
|
|
|
|
|
if (season.draftTimerMode === "standard") {
|
|
|
|
|
const [updatedTimer] = await db
|
|
|
|
|
.update(schema.draftTimers)
|
|
|
|
|
.set({ timeRemaining: sql`${incrementTime}`, updatedAt: new Date() })
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.draftTimers.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftTimers.teamId, teamId)
|
|
|
|
|
)
|
2026-02-23 23:23:24 -08:00
|
|
|
)
|
2026-03-20 21:36:39 -07:00
|
|
|
.returning();
|
|
|
|
|
emitTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime;
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2026-03-20 21:36:39 -07:00
|
|
|
`[AutoPick] Reset timer for team ${teamId} to ${emitTimeRemaining}s (standard mode)`
|
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
|
|
|
);
|
2026-03-20 21:36:39 -07:00
|
|
|
} else {
|
2026-03-24 17:00:32 -07:00
|
|
|
// Chess clock: add the increment (atomic add, same as a manual pick).
|
|
|
|
|
const [updatedTimer] = await db
|
|
|
|
|
.update(schema.draftTimers)
|
|
|
|
|
.set({
|
|
|
|
|
timeRemaining: sql`${schema.draftTimers.timeRemaining} + ${incrementTime}`,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.draftTimers.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftTimers.teamId, teamId)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.returning();
|
|
|
|
|
emitTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime;
|
|
|
|
|
if (!updatedTimer) {
|
|
|
|
|
await db.insert(schema.draftTimers).values({ seasonId, teamId, timeRemaining: emitTimeRemaining });
|
|
|
|
|
}
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(
|
2026-03-24 17:00:32 -07:00
|
|
|
`[AutoPick] Chess clock auto-pick for team ${teamId}, bank is now ${emitTimeRemaining}s (+${incrementTime}s increment)`
|
2026-03-20 21:36:39 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
getSocketIO().to(`draft-${seasonId}`).emit("timer-update", {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
timeRemaining: emitTimeRemaining,
|
|
|
|
|
currentPickNumber: nextPickNumber,
|
2026-04-26 22:31:52 -07:00
|
|
|
overnightPauseActive: false,
|
2026-03-20 21:36:39 -07:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Socket.IO timer-update error:", error);
|
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
|
|
|
// 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)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
// Proactively prune queue items that are now ineligible due to this pick
|
|
|
|
|
// (e.g. a team queued a snooker player but just filled their last flex slot)
|
|
|
|
|
try {
|
|
|
|
|
const prunedQueues = await pruneIneligibleQueueItems({
|
|
|
|
|
seasonId,
|
|
|
|
|
draftRounds: season.draftRounds,
|
|
|
|
|
allTeamIds,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
const io = getSocketIO();
|
|
|
|
|
for (const { teamId: prunedTeamId, removedParticipantIds } of prunedQueues) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("queue-eligibility-pruned", {
|
|
|
|
|
teamId: prunedTeamId,
|
|
|
|
|
removedParticipantIds,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Error pruning ineligible queue items:", error);
|
feat: proactively prune ineligible queue items after each pick (#59)
After every pick, recalculate draft eligibility for all teams and
remove any queued participants whose sport is no longer eligible
(e.g. a team queued a snooker player but just filled their last flex
slot). Previously this was only caught lazily when autodraft fired,
which could pause the draft or pick an unwanted player.
- Add getAllQueuesForSeason to draft-queue.ts — fetches all queue rows
for a season in one query (Map<teamId, QueueItem[]>) instead of N+1
per-team queries
- Add pruneIneligibleQueueItems to draft-utils.ts — uses Promise.all
for the four required data fetches, collects ineligible items in a
single loop pass, warns on orphaned participant references
- Call from both pick paths: executeAutoPick and draft.make-pick.ts
- Emit queue-eligibility-pruned socket event per affected team so the
client updates the queue UI in real time
- Add 5 tests covering: single ineligible removal, all eligible (no-op),
empty queues (no delete called, getTeamQueue never called), mixed
queue (only ineligible item removed), and unknown participant (warn)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:07:22 -08:00
|
|
|
}
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
// Handle autodraft settings for timer-based picks with "next_pick" mode
|
|
|
|
|
if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`);
|
2025-10-25 10:14:36 -07:00
|
|
|
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,
|
Claude/redesign autodraft queue c4 kp r (#40)
* Redesign autodraft queue system with three-state control and queue-only constraint
Core Logic & Database:
- Add `queue_only` boolean column to `autodraft_settings` (migration 0031)
- Rename autodraft UI states: Off / Next Pick / All Picks (while_on mode maps to All Picks)
- `autoPickForTeam`: respects new `queueOnly` param — skips EV fallback when enabled
- `executeAutoPick`: auto-disables autodraft + emits socket event when queue empties with queueOnly ON (AC3)
- `autodraft-updated` socket event now includes `queueOnly` field
Mobile UI Overhaul:
- Rename "Lobby" tab → "Available" (AC6)
- Add new "Queue" tab to mobile bottom nav with drag-reorder, per-item Draft buttons, and autodraft controls (AC5)
- Controls tab retains commissioner tools, notifications, exit; queue controls moved to Queue tab
- Turn indicator appears on both Available and Queue tabs
Components:
- `AutodraftSettings`: replaces toggle+radio with three-state button group (Off | Next Pick | All Picks) + "Only autodraft from queue" switch (AC1, AC2)
- `QueueSection`: adds `canPick` prop + per-item Draft buttons for instant drafting when on the clock
Desktop (AC4):
- Sidebar QueueSection unchanged in position; gains same three-state controls and Draft buttons
Tests (AC7):
- `autodraft.test.ts`: updated for queueOnly field and socket event shape
- `timer-autodraft.test.ts`: new tests for queue-only constraint, auto-shutoff transitions, and all three autodraft states
https://claude.ai/code/session_01PYhJicAStoJ2u6q6dV1naB
* fix: remove erroneous ?? fallbacks in autodraft socket emissions and add autoPickForTeam tests
- Remove `?? true` default on queueOnly in queue-empty auto-disable socket emit
(line 488) — was dead code since the column is NOT NULL, but semantically wrong
and would have caused client-side UI desync if the type ever relaxed
- Remove `?? false` default on the next_pick auto-disable path for consistency
- Add app/models/__tests__/auto-pick.test.ts with 6 tests covering the queueOnly
constraint: empty queue, all items drafted, partial queue skip, and EV fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing queueOnly prop to AutodraftSettings test fixtures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: rewrite AutodraftSettings tests for three-state button group UI
The component was redesigned from a switch + radio buttons to Off/Next Pick/All Picks
buttons with a separate queue-only Switch toggle. Updated 17 stale tests and added 5
new tests covering the queue-only toggle and the All Picks/Off button interactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 22:16:26 -08:00
|
|
|
queueOnly: autodraftSettings.queueOnly,
|
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
|
|
|
});
|
2025-10-25 10:14:36 -07:00
|
|
|
} catch (error) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Socket.IO autodraft-updated error:", error);
|
2025-10-25 10:14:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Socket.IO events error:", error);
|
2025-10-25 10:14:36 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Check if next team has autodraft enabled and trigger immediately
|
Refactor autodraft chain logic and improve timer handling (#25)
* Fix draft timer missing after rollback causing draft to freeze
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
* Fix rollback to not touch timers at all
Timers are each team's time bank and should not be affected by rolling
back a pick. The previous code (both original and the first fix) was
deleting all timer rows during rollback, which was the actual root cause
of the missing timer bug.
Rollback now only does what it should: delete picks from the rollback
point onwards and reset currentPickNumber. The timer system will
naturally resume for the correct team on its next tick.
Also removes the incorrect timer-update socket emit that was sending
initialTime instead of the team's actual remaining bank.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
* Fix all code review issues across timer, rollback, start, and draft-utils
server/timer.ts:
- Remove noisy per-tick console.log that fired every second per active draft
- Remove slot:any type cast (Drizzle query results are already typed)
- Fix inconsistent autodraftSettings argument in the === 0 trigger path
to match the <= 0 path (pass null when autodraft is disabled)
- Fix infinite auto-pick loop: triggerAutoPick now returns boolean;
on failure the draft is paused and a draft-paused socket event is
emitted so clients and commissioners are notified. "Pick already made"
(race condition) is treated as success, not failure.
draft.start.ts:
- Validate that draft slots exist before updating season status to draft.
Previously a missing-slots error left the season stuck in draft status
with no timer rows.
draft.rollback.ts:
- Guard against empty draftSlots before performing snake draft math,
which would produce Infinity/NaN with zero teams.
draft-utils.ts:
- Convert checkAndTriggerNextAutodraft from recursive to iterative.
The mutual recursion (executeAutoPick → checkAndTriggerNextAutodraft
→ executeAutoPick) is now a while loop, preventing unbounded call
stack growth in all-autodraft leagues. Add chainEnabled param to
executeAutoPick so chain calls skip spawning a second chain.
- Restructure getTopAvailableParticipant so the single-sport query is
only built when actually needed (not thrown away in the multi-sport
branch).
- Update misleading timeUsed comment to accurately describe that the
stored value is the team's bank balance at pick time, not time spent.
https://claude.ai/code/session_01YPxgcQywG57KiXj7m46gLV
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 17:02:35 -08:00
|
|
|
// Only run the chain from the top-level call to prevent recursion
|
|
|
|
|
if (!isDraftComplete && params.chainEnabled !== false) {
|
2025-10-25 22:11:10 -07:00
|
|
|
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) {
|
2026-03-21 13:41:39 -07:00
|
|
|
logger.error("[AutoPick] Error in executeAutoPick:", error);
|
2025-10-25 10:14:36 -07:00
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|