Commissioners can now opt in to having a draft start automatically at its scheduled draftDateTime rather than requiring a manual "Start Draft" click. The 1-second timer loop checks for eligible seasons each tick and calls the new shared startDraft() service, which is also used by the existing commissioner HTTP route. - Add autoStartDraft boolean to seasons table (migration 0108) - Extract core start logic into app/services/draft-autostart.ts so both the API route and the timer can call it without AsyncLocalStorage - Add checkAndAutoStartDrafts() to the timer tick; disables autoStartDraft and stops retrying if no draft order is set at fire time - Guard against null draftDateTime in both the timer query (isNotNull) and server actions (autoStartDraft forced false when datetime is null) - Add "Auto-start at scheduled time" checkbox to league creation wizard (step 3) and league settings, gated on date+time being set - Show countdown + "Start Now" button in draft room when auto-start is scheduled; reuses the existing nowForPauseCheck 1-second ticker - Show orange warning banner on league homepage to commissioners when auto-start is within 1 hour and no draft order has been configured Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
import * as schema from "~/database/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { logger } from "~/lib/logger";
|
|
|
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
import type { Server as SocketIOServer } from "socket.io";
|
|
|
|
type Db = PostgresJsDatabase<typeof schema>;
|
|
|
|
export interface StartDraftOptions {
|
|
seasonId: string;
|
|
actorUserId: string;
|
|
actorDisplayName: string;
|
|
db: Db;
|
|
io: SocketIOServer;
|
|
}
|
|
|
|
export interface StartDraftResult {
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Core draft-start logic shared by the commissioner HTTP route and the
|
|
* auto-start timer. Accepts `db` directly (not via AsyncLocalStorage) so it
|
|
* works both inside React Router request context and in the server timer loop.
|
|
*/
|
|
export async function startDraft(options: StartDraftOptions): Promise<StartDraftResult> {
|
|
const { seasonId, actorUserId, actorDisplayName, db, io } = options;
|
|
|
|
const season = await db.query.seasons.findFirst({
|
|
where: eq(schema.seasons.id, seasonId),
|
|
});
|
|
|
|
if (!season) {
|
|
return { success: false, error: "Season not found" };
|
|
}
|
|
|
|
// Idempotency guard — status may have changed between the outer check and here
|
|
if (season.status !== "pre_draft") {
|
|
return { success: false, error: "Draft already started or completed" };
|
|
}
|
|
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
|
});
|
|
|
|
if (draftSlots.length === 0) {
|
|
return { success: false, error: "No draft slots found for this season" };
|
|
}
|
|
|
|
await db
|
|
.update(schema.seasons)
|
|
.set({
|
|
status: "draft",
|
|
currentPickNumber: 1,
|
|
draftStartedAt: new Date(),
|
|
})
|
|
.where(eq(schema.seasons.id, seasonId));
|
|
|
|
// Standard mode: each pick gets exactly the increment time.
|
|
// Chess clock mode: each team starts with the full initial bank.
|
|
const initialTime =
|
|
season.draftTimerMode === "standard"
|
|
? season.draftIncrementTime || 30
|
|
: season.draftInitialTime || 120;
|
|
|
|
await db.delete(schema.draftTimers).where(eq(schema.draftTimers.seasonId, seasonId));
|
|
|
|
await db.insert(schema.draftTimers).values(
|
|
draftSlots.map((slot) => ({
|
|
seasonId,
|
|
teamId: slot.teamId,
|
|
timeRemaining: initialTime,
|
|
}))
|
|
);
|
|
|
|
await db.insert(schema.commissionerAuditLog).values({
|
|
seasonId,
|
|
leagueId: season.leagueId,
|
|
actorUserId,
|
|
actorDisplayName,
|
|
action: "draft_started",
|
|
affectedTeamIds: [],
|
|
details: { pickNumber: 1, autoStarted: actorUserId === "system" },
|
|
});
|
|
|
|
try {
|
|
io.to(`draft-${seasonId}`).emit("draft-started", {
|
|
seasonId,
|
|
currentPickNumber: 1,
|
|
});
|
|
} catch (err) {
|
|
// Socket emit failure is non-fatal; DB state is already committed
|
|
logger.error("[startDraft] Socket emit failed:", err);
|
|
}
|
|
|
|
return { success: true };
|
|
}
|