diff --git a/app/components/AutodraftSettings.tsx b/app/components/AutodraftSettings.tsx index 49c7182..d033216 100644 --- a/app/components/AutodraftSettings.tsx +++ b/app/components/AutodraftSettings.tsx @@ -94,7 +94,6 @@ interface CompactAutodraftBadgeProps { isEnabled: boolean; mode: AutodraftMode; queueOnly: boolean; - isMyTurn: boolean; showChevron?: boolean; } @@ -102,7 +101,6 @@ export function CompactAutodraftBadge({ isEnabled, mode, queueOnly, - isMyTurn, showChevron = false, }: CompactAutodraftBadgeProps) { const state = toAutodraftState(isEnabled, mode, queueOnly); @@ -122,9 +120,6 @@ export function CompactAutodraftBadge({ {label} {showChevron && } - {isMyTurn && ( - Your turn! - )} ); } @@ -137,6 +132,7 @@ interface AutodraftBadgeWithPopoverProps { queueOnly: boolean; isMyTurn: boolean; onUpdate: (isEnabled: boolean, mode: AutodraftMode, queueOnly: boolean) => void; + showOvernightNote?: boolean; } function AutodraftOptions({ @@ -243,6 +239,7 @@ export function AutodraftBadgeWithPopover({ queueOnly, isMyTurn, onUpdate, + showOvernightNote = false, }: AutodraftBadgeWithPopoverProps) { const [open, setOpen] = useState(false); @@ -258,7 +255,6 @@ export function AutodraftBadgeWithPopover({ isEnabled={isEnabled} mode={mode} queueOnly={queueOnly} - isMyTurn={isMyTurn} showChevron /> @@ -299,6 +295,11 @@ export function AutodraftBadgeWithPopover({ ); })} + {showOvernightNote && ( +

+ Autodraft picks still fire during the overnight pause β€” the pause only freezes the timer if autodraft is off. +

+ )} @@ -313,6 +314,7 @@ interface AutodraftSettingsProps { queueOnly: boolean; isMyTurn: boolean; onUpdate: (isEnabled: boolean, mode: AutodraftMode, queueOnly: boolean) => void; + showOvernightNote?: boolean; } export function AutodraftSettings({ @@ -323,6 +325,7 @@ export function AutodraftSettings({ queueOnly, isMyTurn, onUpdate, + showOvernightNote, }: AutodraftSettingsProps) { const [localState, setLocalState] = useState( toAutodraftState(isEnabled, mode, queueOnly) @@ -440,6 +443,11 @@ export function AutodraftSettings({ {isMyTurn && (

You're on the clock!

)} + {showOvernightNote && ( +

+ If overnight pause is enabled, autodraft picks still fire during your overnight window β€” the pause only protects your timer if autodraft is off. +

+ )} ); } diff --git a/app/components/draft/DraftGridSection.tsx b/app/components/draft/DraftGridSection.tsx index 1122409..9fa5a5e 100644 --- a/app/components/draft/DraftGridSection.tsx +++ b/app/components/draft/DraftGridSection.tsx @@ -56,6 +56,7 @@ interface DraftGridSectionProps { isCommissioner: boolean; seasonStatus?: SeasonStatus; draftPaused?: boolean; + overnightPauseInfo?: { isActive: boolean; resumesAt: Date | null; pausedTeamIds?: Set }; onAdjustTimeBankOpen?: (teamId: string) => void; onSetAutodraftOpen?: (teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void; @@ -75,6 +76,7 @@ export const DraftGridSection = memo(function DraftGridSection({ isCommissioner, seasonStatus, draftPaused, + overnightPauseInfo, onAdjustTimeBankOpen, onSetAutodraftOpen, onForceAutopick, @@ -90,6 +92,8 @@ export const DraftGridSection = memo(function DraftGridSection({ [draftGrid, currentPick] ); + const isOvernightActive = overnightPauseInfo?.isActive ?? false; + return (

Draft Grid

@@ -101,6 +105,9 @@ export const DraftGridSection = memo(function DraftGridSection({ const teamTime = teamTimers[slot.team.id]; const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled || false; const isConnected = connectedTeams.has(slot.team.id); + const isCurrentTeamOvernight = slot.team.id === currentTeamId + ? isOvernightActive + : (overnightPauseInfo?.pausedTeamIds?.has(slot.team.id) ?? false); const headerInner = (
@@ -127,9 +134,9 @@ export const DraftGridSection = memo(function DraftGridSection({ {ownerMap[slot.team.id] || slot.team.name}
- {formatClockTime(teamTime)} + {isCurrentTeamOvernight ? `πŸŒ™ ${formatClockTime(teamTime)}` : formatClockTime(teamTime)}
{isCommissioner && (onAdjustTimeBankOpen || onSetAutodraftOpen) && (
) : isCurrent ? ( -
{currentLabel}
+ <> +
{currentLabel}
+ {resumesAt && ( +
+ Resumes {resumesAt.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })} +
+ )} + ) : null} {children &&
{children}
} diff --git a/app/components/draft/MiniDraftGrid.tsx b/app/components/draft/MiniDraftGrid.tsx index 687c5bb..94cf0cf 100644 --- a/app/components/draft/MiniDraftGrid.tsx +++ b/app/components/draft/MiniDraftGrid.tsx @@ -51,6 +51,7 @@ export interface MiniDraftGridProps { connectedTeams?: Set; seasonStatus?: SeasonStatus; draftPaused?: boolean; + overnightPauseInfo?: { isActive: boolean; resumesAt: Date | null; pausedTeamIds?: Set }; onAdjustTimeBankOpen?: (teamId: string) => void; onSetAutodraftOpen?: (teamId: string) => void; onForceAutopick?: (pickNumber: number, teamId: string) => void; @@ -70,6 +71,7 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ connectedTeams, seasonStatus, draftPaused, + overnightPauseInfo, onAdjustTimeBankOpen, onSetAutodraftOpen, onForceAutopick, @@ -151,6 +153,9 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ ? [displayedIndices[0], displayedIndices[1], extraIndex] : [displayedIndices[0], displayedIndices[1]]; + const currentTeamId = draftGrid.flat().find((c) => c.pickNumber === currentPick)?.teamId; + const isOvernightActive = overnightPauseInfo?.isActive ?? false; + return (
@@ -160,6 +165,9 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ const teamTime = teamTimers[slot.team.id]; const isAutodraft = autodraftStatus[slot.team.id]?.isEnabled ?? false; const isConnected = connectedTeams ? connectedTeams.has(slot.team.id) : true; + const isCurrentTeamOvernight = slot.team.id === currentTeamId + ? isOvernightActive + : (overnightPauseInfo?.pausedTeamIds?.has(slot.team.id) ?? false); const headerContent = ( <> @@ -171,8 +179,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ {ownerMap[slot.team.id] || slot.team.name}
-
- {formatClockTime(teamTime)} +
+ {isCurrentTeamOvernight ? `πŸŒ™ ${formatClockTime(teamTime)}` : formatClockTime(teamTime)}
); @@ -253,6 +261,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ pick={pickData} seasonStatus={seasonStatus} draftPaused={draftPaused} + isOvernightPause={isCurrent && isOvernightActive} + resumesAt={isCurrent && isOvernightActive && overnightPauseInfo?.resumesAt ? overnightPauseInfo.resumesAt : undefined} className="cursor-context-menu" /> @@ -317,6 +327,8 @@ export const MiniDraftGrid = memo(function MiniDraftGrid({ pick={pickData} seasonStatus={seasonStatus} draftPaused={draftPaused} + isOvernightPause={isCurrent && isOvernightActive} + resumesAt={isCurrent && isOvernightActive && overnightPauseInfo?.resumesAt ? overnightPauseInfo.resumesAt : undefined} /> ); })} diff --git a/app/components/league/DraftInfoCard.tsx b/app/components/league/DraftInfoCard.tsx index cbbffdb..16905bf 100644 --- a/app/components/league/DraftInfoCard.tsx +++ b/app/components/league/DraftInfoCard.tsx @@ -1,5 +1,5 @@ import { format } from "date-fns"; -import { ChevronRight, ChessPawn, Hourglass, LayoutGrid } from "lucide-react"; +import { ChevronRight, ChessPawn, Hourglass, LayoutGrid, Moon } from "lucide-react"; import { Link } from "react-router"; import { formatClockTime } from "~/lib/draft-timer"; import { Button } from "~/components/ui/button"; @@ -24,6 +24,10 @@ export interface DraftInfoCardProps { draftRoomHref: string; /** 1-based draft position for the current user; omit or undefined to hide the panel */ userDraftPosition?: number; + overnightPauseMode?: "none" | "league" | "per_user"; + overnightPauseStart?: string | null; + overnightPauseEnd?: string | null; + overnightPauseTimezone?: string | null; } // ─── Sub-components ─────────────────────────────────────────────────────────── @@ -52,6 +56,10 @@ function DraftInfoCardVariant({ draftDateTime, draftRoomHref, userDraftPosition, + overnightPauseMode, + overnightPauseStart, + overnightPauseEnd, + overnightPauseTimezone, }: DraftInfoCardProps) { const flexSpots = Math.max(0, draftRounds - sportsCount); @@ -66,6 +74,31 @@ function DraftInfoCardVariant({ flexSpots > 0 ? `${flexSpots} flex` : null, ].filter(Boolean).join(" + "); + const hasPosition = userDraftPosition !== null && userDraftPosition !== undefined; + const hasOvernight = !!(overnightPauseMode && overnightPauseMode !== "none" && overnightPauseStart && overnightPauseEnd); + + // Mechanics row: Rounds + (Your Pick?) + Timer + // When overnight is shown, Draft Date moves to the top row so mechanics has 2–3 items. + // When no overnight, Draft Date stays in this row giving 3–4 items. + const mechanicsColsClass = hasOvernight + ? (hasPosition ? "grid-cols-2 sm:grid-cols-3" : "grid-cols-2") + : (hasPosition ? "grid-cols-2 sm:grid-cols-4" : "grid-cols-2 sm:grid-cols-3"); + + const timerPanel = ( + + {draftTimerMode === "chess_clock" + ? + : } + {timerValue} + + } + sub={timerSub} + /> + ); + return ( @@ -82,36 +115,46 @@ function DraftInfoCardVariant({
- -
- {/* Draft Date */} - + + {/* Scheduling row: Draft Date + Overnight Pause side-by-side when overnight is configured */} + {hasOvernight && ( +
+ + + + {formatHHMM(overnightPauseStart ?? "")} – {formatHHMM(overnightPauseEnd ?? "")} + + } + sub={overnightPauseMode === "league" && overnightPauseTimezone + ? overnightPauseTimezone + : "per-user timezone"} + /> +
+ )} - {/* Rounds */} + {/* Mechanics row: Rounds + Your Pick? + Timer (+ Draft Date when no overnight) */} +
+ {!hasOvernight && ( + + )} - - {/* User's Draft Position (only when set) */} - {userDraftPosition !== null && userDraftPosition !== undefined && ( + {hasPosition && ( )} - - {/* Timer */} - - {draftTimerMode === "chess_clock" - ? - : } - {timerValue} - - } - sub={timerSub} - /> + {timerPanel} + {/* placeholder to keep grid balanced on mobile when only 2 items */} + {hasOvernight && !hasPosition &&
}
@@ -120,6 +163,16 @@ function DraftInfoCardVariant({ // ─── Helpers ────────────────────────────────────────────────────────────────── +/** Formats "23:00" β†’ "11 PM", "07:00" β†’ "7 AM". */ +function formatHHMM(hhmm: string): string { + const [hStr, mStr] = hhmm.split(":"); + const h = parseInt(hStr, 10); + const m = parseInt(mStr, 10); + const period = h < 12 ? "AM" : "PM"; + const h12 = h % 12 === 0 ? 12 : h % 12; + return m === 0 ? `${h12} ${period}` : `${h12}:${String(m).padStart(2, "0")} ${period}`; +} + /** Converts a seconds value to a short human-readable string, e.g. "1 minute", "10 seconds", "2 hours". */ function formatHumanTime(seconds: number): string { const h = Math.floor(seconds / 3600); diff --git a/app/components/league/TimezonePromptBanner.tsx b/app/components/league/TimezonePromptBanner.tsx new file mode 100644 index 0000000..405a62c --- /dev/null +++ b/app/components/league/TimezonePromptBanner.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import { TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect"; + +const VALID_TIMEZONES = new Set( + TIMEZONE_OPTIONS.flatMap((g) => g.zones.map((z) => z.value)) +); + +interface TimezonePromptBannerProps { + /** Called with the saved timezone value after a successful POST. */ + onSaved?: (timezone: string) => void; +} + +export function TimezonePromptBanner({ onSaved }: TimezonePromptBannerProps) { + const [dismissed, setDismissed] = useState(false); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + if (dismissed || saved) return null; + + const detectedTz = + typeof window !== "undefined" + ? (() => { + try { + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return VALID_TIMEZONES.has(tz) ? tz : null; + } catch { + return null; + } + })() + : null; + + if (!detectedTz) return null; + + const handleSave = async () => { + setSaving(true); + try { + const body = new FormData(); + body.set("timezone", detectedTz); + const res = await fetch("/api/user/timezone", { method: "POST", body }); + if (res.ok) { + setSaved(true); + onSaved?.(detectedTz); + } + } finally { + setSaving(false); + } + }; + + return ( +
+ + Set your timezone to {detectedTz} for overnight pick + protection? + +
+ + +
+
+ ); +} diff --git a/app/components/league/TimezoneSelect.tsx b/app/components/league/TimezoneSelect.tsx new file mode 100644 index 0000000..abcf16e --- /dev/null +++ b/app/components/league/TimezoneSelect.tsx @@ -0,0 +1,141 @@ +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; + +export const TIMEZONE_OPTIONS: { region: string; zones: { value: string; label: string }[] }[] = [ + { + region: "North America", + zones: [ + { value: "America/New_York", label: "Eastern Time (ET)" }, + { value: "America/Chicago", label: "Central Time (CT)" }, + { value: "America/Denver", label: "Mountain Time (MT)" }, + { value: "America/Phoenix", label: "Mountain Time – Arizona (no DST)" }, + { value: "America/Los_Angeles", label: "Pacific Time (PT)" }, + { value: "America/Anchorage", label: "Alaska Time (AKT)" }, + { value: "Pacific/Honolulu", label: "Hawaii Time (HT)" }, + { value: "America/Toronto", label: "Eastern Time – Toronto" }, + { value: "America/Vancouver", label: "Pacific Time – Vancouver" }, + { value: "America/Edmonton", label: "Mountain Time – Edmonton" }, + { value: "America/Winnipeg", label: "Central Time – Winnipeg" }, + { value: "America/Halifax", label: "Atlantic Time – Halifax" }, + { value: "America/St_Johns", label: "Newfoundland Time" }, + { value: "America/Mexico_City", label: "Central Time – Mexico City" }, + ], + }, + { + region: "South America", + zones: [ + { value: "America/Sao_Paulo", label: "BrasΓ­lia Time (BRT)" }, + { value: "America/Argentina/Buenos_Aires", label: "Argentina Time (ART)" }, + { value: "America/Bogota", label: "Colombia Time (COT)" }, + { value: "America/Lima", label: "Peru Time (PET)" }, + { value: "America/Santiago", label: "Chile Time (CLT)" }, + ], + }, + { + region: "Europe", + zones: [ + { value: "Europe/London", label: "GMT / British Time (GMT/BST)" }, + { value: "Europe/Dublin", label: "Irish Time (GMT/IST)" }, + { value: "Europe/Paris", label: "Central European Time (CET/CEST)" }, + { value: "Europe/Berlin", label: "Central European Time – Berlin" }, + { value: "Europe/Amsterdam", label: "Central European Time – Amsterdam" }, + { value: "Europe/Madrid", label: "Central European Time – Madrid" }, + { value: "Europe/Rome", label: "Central European Time – Rome" }, + { value: "Europe/Warsaw", label: "Central European Time – Warsaw" }, + { value: "Europe/Helsinki", label: "Eastern European Time (EET/EEST)" }, + { value: "Europe/Athens", label: "Eastern European Time – Athens" }, + { value: "Europe/Bucharest", label: "Eastern European Time – Bucharest" }, + { value: "Europe/Kyiv", label: "Eastern European Time – Kyiv" }, + { value: "Europe/Moscow", label: "Moscow Time (MSK)" }, + ], + }, + { + region: "Africa", + zones: [ + { value: "Africa/Johannesburg", label: "South Africa Time (SAST)" }, + { value: "Africa/Cairo", label: "Eastern European Time – Cairo" }, + { value: "Africa/Lagos", label: "West Africa Time (WAT)" }, + { value: "Africa/Nairobi", label: "East Africa Time (EAT)" }, + ], + }, + { + region: "Asia / Middle East", + zones: [ + { value: "Asia/Dubai", label: "Gulf Standard Time (GST)" }, + { value: "Asia/Riyadh", label: "Arabia Standard Time (AST)" }, + { value: "Asia/Kolkata", label: "India Standard Time (IST)" }, + { value: "Asia/Dhaka", label: "Bangladesh Standard Time (BST)" }, + { value: "Asia/Bangkok", label: "Indochina Time (ICT)" }, + { value: "Asia/Singapore", label: "Singapore Time (SGT)" }, + { value: "Asia/Shanghai", label: "China Standard Time (CST)" }, + { value: "Asia/Hong_Kong", label: "Hong Kong Time (HKT)" }, + { value: "Asia/Tokyo", label: "Japan Standard Time (JST)" }, + { value: "Asia/Seoul", label: "Korea Standard Time (KST)" }, + { value: "Asia/Karachi", label: "Pakistan Standard Time (PKT)" }, + { value: "Asia/Colombo", label: "Sri Lanka Time (SLST)" }, + { value: "Asia/Tehran", label: "Iran Standard Time (IRST)" }, + { value: "Asia/Jerusalem", label: "Israel Standard Time (IST/IDT)" }, + ], + }, + { + region: "Oceania", + zones: [ + { value: "Australia/Sydney", label: "Australian Eastern Time (AEST/AEDT)" }, + { value: "Australia/Melbourne", label: "Australian Eastern Time – Melbourne" }, + { value: "Australia/Brisbane", label: "Australian Eastern Time – Queensland" }, + { value: "Australia/Adelaide", label: "Australian Central Time (ACST/ACDT)" }, + { value: "Australia/Perth", label: "Australian Western Time (AWST)" }, + { value: "Pacific/Auckland", label: "New Zealand Time (NZST/NZDT)" }, + { value: "Pacific/Fiji", label: "Fiji Time (FJT)" }, + ], + }, + { + region: "UTC / Other", + zones: [ + { value: "UTC", label: "UTC (Coordinated Universal Time)" }, + ], + }, +]; + +interface TimezoneSelectProps { + value: string; + onChange: (value: string) => void; + disabled?: boolean; + placeholder?: string; + name?: string; +} + +export function TimezoneSelect({ + value, + onChange, + disabled, + placeholder = "Select timezone…", + name, +}: TimezoneSelectProps) { + return ( + + ); +} diff --git a/app/hooks/useDraftRoomState.ts b/app/hooks/useDraftRoomState.ts index 1511bab..0bf1a8e 100644 --- a/app/hooks/useDraftRoomState.ts +++ b/app/hooks/useDraftRoomState.ts @@ -125,6 +125,9 @@ export function useDraftRoomState({ const [timeBankDirection, setTimeBankDirection] = useState<"add" | "remove">("add"); const [isAdjustingTimeBank, setIsAdjustingTimeBank] = useState(false); + const [isOvernightPause, setIsOvernightPause] = useState(false); + const [overnightResumesAt, setOvernightResumesAt] = useState(null); + return { picks, setPicks, currentPick, setCurrentPick, @@ -160,5 +163,7 @@ export function useDraftRoomState({ timeBankUnit, setTimeBankUnit, timeBankDirection, setTimeBankDirection, isAdjustingTimeBank, setIsAdjustingTimeBank, + isOvernightPause, setIsOvernightPause, + overnightResumesAt, setOvernightResumesAt, }; } diff --git a/app/hooks/useDraftSocketEvents.ts b/app/hooks/useDraftSocketEvents.ts index 66b1249..cbc903f 100644 --- a/app/hooks/useDraftSocketEvents.ts +++ b/app/hooks/useDraftSocketEvents.ts @@ -36,6 +36,8 @@ interface UseDraftSocketEventsParams { setConnectedTeams: (fn: (prev: Set) => Set) => void; setQueue: (fn: (prev: QueueItem[]) => QueueItem[]) => void; setTeamTimers: (fn: (prev: Record) => Record) => void; + setIsOvernightPause: (value: boolean) => void; + setOvernightResumesAt: (value: Date | null) => void; } export function useDraftSocketEvents({ @@ -60,6 +62,8 @@ export function useDraftSocketEvents({ setConnectedTeams, setQueue, setTeamTimers, + setIsOvernightPause, + setOvernightResumesAt, }: UseDraftSocketEventsParams) { useEffect(() => { type PickMadePayload = { @@ -93,7 +97,13 @@ export function useDraftSocketEvents({ } }; - const handleTimerUpdate = (data: { teamId: string; timeRemaining: number; currentPickNumber: number | null }) => { + const handleTimerUpdate = (data: { + teamId: string; + timeRemaining: number; + currentPickNumber: number | null; + overnightPauseActive?: boolean; + resumesAtUTC?: number; + }) => { setTeamTimers((prev) => { if (prev[data.teamId] === data.timeRemaining) return prev; return { ...prev, [data.teamId]: data.timeRemaining }; @@ -101,6 +111,9 @@ export function useDraftSocketEvents({ if (data.currentPickNumber !== null) { setCurrentPick(data.currentPickNumber); } + const pauseActive = data.overnightPauseActive ?? false; + setIsOvernightPause(pauseActive); + setOvernightResumesAt(pauseActive && data.resumesAtUTC ? new Date(data.resumesAtUTC) : null); }; const handleDraftPaused = () => setIsPaused(true); diff --git a/app/lib/__tests__/overnight-pause.test.ts b/app/lib/__tests__/overnight-pause.test.ts new file mode 100644 index 0000000..04fde29 --- /dev/null +++ b/app/lib/__tests__/overnight-pause.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect } from "vitest"; +import { isInOvernightWindow, getOvernightResumeUTC } from "../overnight-pause"; + +// Helper to build a Date at a specific local time in a timezone. +// We use a known UTC offset approach: format a UTC date, check what local time +// it produces, then shift until we hit the desired time. +function dateAtLocalTime( + timezone: string, + year: number, + month: number, // 1-indexed + day: number, + hour: number, + minute: number +): Date { + // Use a binary-search-free approach: construct approximate UTC, then correct + const approxUtc = Date.UTC(year, month - 1, day, hour, minute); + const probe = new Date(approxUtc); + const fmt = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + const parts = fmt.formatToParts(probe); + const localHour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); + const localMin = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); + const h = localHour === 24 ? 0 : localHour; + const diffMs = ((hour * 60 + minute) - (h * 60 + localMin)) * 60 * 1000; + return new Date(approxUtc + diffMs); +} + +// ─── isInOvernightWindow ────────────────────────────────────────────────────── + +describe("isInOvernightWindow β€” midnight-spanning window (23:00–07:00)", () => { + const tz = "America/New_York"; + const start = "23:00"; + const end = "07:00"; + + it("returns true just after window starts (23:30)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 23, 30))).toBe(true); + }); + + it("returns true at exact window start (23:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 23, 0))).toBe(true); + }); + + it("returns true past midnight (01:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 1, 0))).toBe(true); + }); + + it("returns true just before window ends (06:59)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 6, 59))).toBe(true); + }); + + it("returns false exactly at window end (07:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 16, 7, 0))).toBe(false); + }); + + it("returns false in the middle of the day (14:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 14, 0))).toBe(false); + }); + + it("returns false just before window starts (22:59)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 22, 59))).toBe(false); + }); +}); + +describe("isInOvernightWindow β€” same-day window (10:00–18:00)", () => { + const tz = "America/Chicago"; + const start = "10:00"; + const end = "18:00"; + + it("returns true at 14:00", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 14, 0))).toBe(true); + }); + + it("returns true at exact start (10:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 10, 0))).toBe(true); + }); + + it("returns false at exact end (18:00)", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 18, 0))).toBe(false); + }); + + it("returns false at 09:59", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 9, 59))).toBe(false); + }); + + it("returns false at 20:00", () => { + expect(isInOvernightWindow(tz, start, end, dateAtLocalTime(tz, 2025, 6, 15, 20, 0))).toBe(false); + }); +}); + +describe("isInOvernightWindow β€” different IANA timezones", () => { + it("works for Europe/London at 00:30 in a midnight-spanning window", () => { + const d = dateAtLocalTime("Europe/London", 2025, 6, 15, 0, 30); + expect(isInOvernightWindow("Europe/London", "23:00", "07:00", d)).toBe(true); + }); + + it("works for Asia/Tokyo β€” 23:30 is in window", () => { + const d = dateAtLocalTime("Asia/Tokyo", 2025, 6, 15, 23, 30); + expect(isInOvernightWindow("Asia/Tokyo", "23:00", "07:00", d)).toBe(true); + }); + + it("works for Asia/Tokyo β€” 14:00 is not in window", () => { + const d = dateAtLocalTime("Asia/Tokyo", 2025, 6, 15, 14, 0); + expect(isInOvernightWindow("Asia/Tokyo", "23:00", "07:00", d)).toBe(false); + }); +}); + +// ─── getOvernightResumeUTC ──────────────────────────────────────────────────── + +describe("getOvernightResumeUTC", () => { + it("returns a future date when called during the overnight window", () => { + const tz = "America/New_York"; + // 2:00 AM ET β€” inside the 23:00–07:00 window + const now = dateAtLocalTime(tz, 2025, 6, 16, 2, 0); + const resume = getOvernightResumeUTC(tz, "07:00", now); + expect(resume.getTime()).toBeGreaterThan(now.getTime()); + }); + + it("returns a date that represents 07:00 in target timezone", () => { + const tz = "America/Chicago"; + const now = dateAtLocalTime(tz, 2025, 6, 16, 1, 0); + const resume = getOvernightResumeUTC(tz, "07:00", now); + // Verify it shows as 07:00 in Chicago + const fmt = new Intl.DateTimeFormat("en-US", { + timeZone: tz, + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + const parts = fmt.formatToParts(resume); + const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); + const m = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); + expect(h).toBe(7); + expect(m).toBe(0); + }); + + it("returns tomorrow's end time when current time is after window end (daytime)", () => { + const tz = "America/New_York"; + // 14:00 ET β€” outside the window, should return tomorrow 07:00 + const now = dateAtLocalTime(tz, 2025, 6, 16, 14, 0); + const resume = getOvernightResumeUTC(tz, "07:00", now); + expect(resume.getTime()).toBeGreaterThan(now.getTime()); + }); +}); diff --git a/app/lib/overnight-pause.ts b/app/lib/overnight-pause.ts new file mode 100644 index 0000000..2b62bf4 --- /dev/null +++ b/app/lib/overnight-pause.ts @@ -0,0 +1,96 @@ +/** + * Utilities for overnight pause windows. + * Works in both Node.js and browser β€” uses only Intl, no extra dependencies. + */ + +function getLocalMinutes(timezone: string, now: Date): number { + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: timezone, + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + const parts = formatter.formatToParts(now); + const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); + const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); + // Intl hour12:false can return "24" for midnight β€” normalize to 0 + return (hour === 24 ? 0 : hour) * 60 + minute; +} + +/** + * Build a UTC Date representing "the given date at HH:MM in the given timezone." + * Corrects for the UTC offset by comparing what local time the naive UTC produces. + */ +function utcForLocalTime( + timezone: string, + year: number, + month: number, // 1-indexed + day: number, + hour: number, + minute: number +): Date { + // Start with a naive UTC (treating target H:M as UTC) + const approxUTC = Date.UTC(year, month - 1, day, hour, minute, 0); + const probe = new Date(approxUTC); + const localMins = getLocalMinutes(timezone, probe); + const targetMins = hour * 60 + minute; + // Shift: if local is behind target, add the gap; if ahead, subtract it + return new Date(approxUTC + (targetMins - localMins) * 60 * 1000); +} + +/** + * Returns true if `now` falls inside the overnight pause window defined by + * startHHMM–endHHMM in the given IANA timezone. + * + * The window almost always spans midnight (e.g. "23:00"–"07:00"), but a + * same-day window (e.g. "10:00"–"18:00") is also handled correctly. + */ +export function isInOvernightWindow( + timezone: string, + startHHMM: string, + endHHMM: string, + now = new Date() +): boolean { + const currentMinutes = getLocalMinutes(timezone, now); + const [sh, sm] = startHHMM.split(":").map(Number); + const [eh, em] = endHHMM.split(":").map(Number); + const startMinutes = sh * 60 + sm; + const endMinutes = eh * 60 + em; + + if (startMinutes > endMinutes) { + // Spans midnight: in window if current >= start OR current < end + return currentMinutes >= startMinutes || currentMinutes < endMinutes; + } + // Same-day window: in window if start <= current < end + return currentMinutes >= startMinutes && currentMinutes < endMinutes; +} + +/** + * Returns the next UTC Date when endHHMM will occur in the given timezone. + * Used to tell clients when the overnight window will end. + */ +export function getOvernightResumeUTC( + timezone: string, + endHHMM: string, + now = new Date() +): Date { + const [eh, em] = endHHMM.split(":").map(Number); + + // Get current calendar date in the target timezone + const dateFmt = new Intl.DateTimeFormat("en-CA", { + timeZone: timezone, + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + const [year, month, day] = dateFmt.format(now).split("-").map(Number); + + // Candidate: today at endHHMM in the target timezone + const candidate = utcForLocalTime(timezone, year, month, day, eh, em); + + // If this moment has already passed, advance one day + if (candidate.getTime() <= now.getTime()) { + return new Date(candidate.getTime() + 24 * 60 * 60 * 1000); + } + return candidate; +} diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index 1c300e4..1e4d39a 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -705,6 +705,7 @@ export async function executeAutoPick(params: { teamId, timeRemaining: emitTimeRemaining, currentPickNumber: nextPickNumber, + overnightPauseActive: false, }); } catch (error) { logger.error("[AutoPick] Socket.IO timer-update error:", error); diff --git a/app/models/user.ts b/app/models/user.ts index d1761d6..238c593 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,4 +1,4 @@ -import { eq, inArray } from "drizzle-orm"; +import { eq, inArray, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; @@ -74,3 +74,23 @@ export async function findAllUsers(): Promise { orderBy: (users, { asc }) => [asc(users.displayName)], }); } + +/** + * Returns true if the user currently owns a team in a draft-status season. + * Used to prevent timezone changes mid-draft. + */ +export async function isUserInActiveDraft(userId: string): Promise { + const db = database(); + const result = await db + .select({ id: schema.teams.id }) + .from(schema.teams) + .innerJoin(schema.seasons, eq(schema.teams.seasonId, schema.seasons.id)) + .where( + and( + eq(schema.teams.ownerId, userId), + eq(schema.seasons.status, "draft") + ) + ) + .limit(1); + return result.length > 0; +} diff --git a/app/routes.ts b/app/routes.ts index 732b367..5d20319 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -47,6 +47,7 @@ export default [ route("api/draft/rollback", "routes/api/draft.rollback.ts"), route("api/draft/adjust-time-bank", "routes/api/draft.adjust-time-bank.ts"), route("api/autodraft/update", "routes/api/autodraft.update.ts"), + route("api/user/timezone", "routes/api/user.timezone.ts"), route("api/seasons/:seasonId/draft", "routes/api/seasons.$seasonId.draft.ts"), route("login", "routes/login.tsx"), route("register", "routes/register.tsx"), diff --git a/app/routes/api/user.timezone.ts b/app/routes/api/user.timezone.ts new file mode 100644 index 0000000..f89b6c6 --- /dev/null +++ b/app/routes/api/user.timezone.ts @@ -0,0 +1,32 @@ +import type { ActionFunctionArgs } from "react-router"; +import { auth } from "~/lib/auth.server"; +import { updateUser, isUserInActiveDraft } from "~/models/user"; +import { TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect"; + +const VALID_TIMEZONES = new Set( + TIMEZONE_OPTIONS.flatMap((g) => g.zones.map((z) => z.value)) +); + +export async function action(args: ActionFunctionArgs) { + const session = await auth.api.getSession({ headers: args.request.headers }); + if (!session) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const formData = await args.request.formData(); + const timezone = formData.get("timezone") as string | null; + + if (!timezone || !VALID_TIMEZONES.has(timezone)) { + return Response.json({ error: "Invalid timezone" }, { status: 400 }); + } + + if (await isUserInActiveDraft(session.user.id)) { + return Response.json( + { error: "Cannot change timezone during an active draft" }, + { status: 403 } + ); + } + + await updateUser(session.user.id, { timezone }); + return Response.json({ success: true }); +} diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index b748530..931e3d0 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -1,7 +1,7 @@ import { useLoaderData, Link, useRevalidator } from "react-router"; import { useDraftSocket } from "~/hooks/useDraftSocket"; import { logger } from "~/lib/logger"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "~/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { auth } from "~/lib/auth.server"; @@ -20,8 +20,9 @@ import { getDraftParticipants } from "~/models/participant"; import { getSeasonTimers } from "~/models/draft-timer"; import { getSeasonAutodraftSettings } from "~/models/autodraft-settings"; import { hasCommissionerRecord } from "~/models/commissioner"; +import { findUsersByIds, getUserDisplayName } from "~/models/user"; +import { isInOvernightWindow } from "~/lib/overnight-pause"; import logomarkUrl from "../../../public/logomark.svg?url"; -import { buildOwnerMap } from "~/lib/owner-map"; import { DraftSidebar } from "~/components/DraftSidebar"; import { TeamRosterView } from "~/components/draft/TeamRosterView"; import { DraftSummaryView } from "~/components/draft/DraftSummaryView"; @@ -113,7 +114,32 @@ export async function loader(args: Route.LoaderArgs) { ? (autodraftSettings.find((s) => s.teamId === userTeam.id) ?? null) : null; - const ownerMap = await buildOwnerMap(draftSlots); + // Single users query builds both the owner display-name map and the timezone + // map for overnight pause moon icons β€” avoids two separate findUsersByIds calls. + const uniqueOwnerIds = [...new Set( + draftSlots.map((s) => s.team.ownerId).filter((id): id is string => id !== null) + )]; + const userRows = uniqueOwnerIds.length ? await findUsersByIds(uniqueOwnerIds) : []; + const userById = new Map(userRows.map((u) => [u.id, u])); + + const ownerMap: Record = {}; + for (const slot of draftSlots) { + if (slot.team.ownerId) { + const user = userById.get(slot.team.ownerId); + const name = user ? getUserDisplayName(user) : null; + if (name) ownerMap[slot.team.id] = name; + } + } + + const teamTimezoneMap: Record = + season.overnightPauseMode === "per_user" + ? Object.fromEntries( + draftSlots.map((slot) => [ + slot.team.id, + slot.team.ownerId ? (userById.get(slot.team.ownerId)?.timezone || null) : null, + ]) + ) + : {}; return { season, @@ -128,6 +154,7 @@ export async function loader(args: Route.LoaderArgs) { isCommissioner: !!isCommissioner, currentUserId: userId, ownerMap, + teamTimezoneMap, }; } @@ -145,6 +172,7 @@ export default function DraftRoom() { isCommissioner, currentUserId, ownerMap, + teamTimezoneMap, } = useLoaderData(); const { revalidate, state: revalidatorState } = useRevalidator(); const { isConnected, connectionError, isReconnecting, reconnectCount, socketVersion, on, off } = useDraftSocket(season.id, userTeam?.id); @@ -203,6 +231,8 @@ export default function DraftRoom() { timeBankUnit, setTimeBankUnit, timeBankDirection, setTimeBankDirection, isAdjustingTimeBank, setIsAdjustingTimeBank, + isOvernightPause, setIsOvernightPause, + overnightResumesAt, setOvernightResumesAt, } = useDraftRoomState({ draftPicks, season, @@ -214,6 +244,27 @@ export default function DraftRoom() { userQueue, }); + // Advances every second so isInOvernightWindow sees the real current time. + const [nowForPauseCheck, setNowForPauseCheck] = useState(() => new Date()); + useEffect(() => { + const id = setInterval(() => setNowForPauseCheck(new Date()), 1_000); + return () => clearInterval(id); + }, []); + + // Compute which teams are currently in their overnight pause window. + const pausedTeamIds = useMemo(() => { + const { overnightPauseMode: mode, overnightPauseStart: start, overnightPauseEnd: end, overnightPauseTimezone: fallbackTz } = season; + if (mode === "none" || !start || !end) return new Set(); + const result = new Set(); + for (const slot of draftSlots) { + const tz = mode === "league" + ? (fallbackTz || null) + : (teamTimezoneMap[slot.team.id] || fallbackTz || null); + if (tz && isInOvernightWindow(tz, start, end, nowForPauseCheck)) result.add(slot.team.id); + } + return result; + }, [season, draftSlots, teamTimezoneMap, nowForPauseCheck]); + const { authDegraded, authFetch, @@ -365,6 +416,8 @@ export default function DraftRoom() { setConnectedTeams, setQueue, setTeamTimers, + setIsOvernightPause, + setOvernightResumesAt, }); // Persist sidebar collapsed state @@ -956,13 +1009,14 @@ export default function DraftRoom() { connectedTeams, seasonStatus: season.status, draftPaused: isPaused, + overnightPauseInfo: { isActive: isOvernightPause, resumesAt: overnightResumesAt, pausedTeamIds }, onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined, onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined, onForceAutopick: isCommissioner ? handleForceAutopick : undefined, onForceManualPickOpen: isCommissioner ? handleForceManualPickOpen : undefined, onReplacePick: isCommissioner ? handleReplacePickOpen : undefined, onRollbackToPick: isCommissioner && !isDraftComplete ? handleRollbackToPick : undefined, - }), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, connectedTeams, season.status, isPaused, isCommissioner, handleAdjustTimeBankOpen, handleSetAutodraftOpen, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]); + }), [draftSlots, draftGrid, currentPick, currentRound, ownerMap, teamTimers, autodraftStatus, connectedTeams, season.status, isPaused, isOvernightPause, overnightResumesAt, pausedTeamIds, isCommissioner, handleAdjustTimeBankOpen, handleSetAutodraftOpen, handleForceAutopick, handleForceManualPickOpen, handleReplacePickOpen, handleRollbackToPick, isDraftComplete]); const availableParticipantsSectionProps = useMemo(() => ({ participants: filteredParticipants, @@ -1002,6 +1056,7 @@ export default function DraftRoom() { ownerMap, seasonStatus: season.status, draftPaused: isPaused, + overnightPauseInfo: { isActive: isOvernightPause, resumesAt: overnightResumesAt, pausedTeamIds }, onAdjustTimeBankOpen: isCommissioner ? handleAdjustTimeBankOpen : undefined, onSetAutodraftOpen: isCommissioner ? handleSetAutodraftOpen : undefined, onForceAutopick: isCommissioner ? handleForceAutopick : undefined, @@ -1145,12 +1200,20 @@ export default function DraftRoom() { {season.status === "draft" && !isDraftComplete && currentDraftSlot && (
-
- {isMyTurn ? "Your Turn!" : "On the Clock"} +
+ {isMyTurn + ? isOvernightPause ? "Overnight Pause" : "Your Turn!" + : "On the Clock"}
{currentDraftSlotOwnerName ?? currentDraftSlot.team.name} @@ -1161,6 +1224,10 @@ export default function DraftRoom() {
{isPaused ? ( Paused + ) : isOvernightPause ? ( + + πŸŒ™ {formatClockTime(currentClockTime)} + ) : ( {formatClockTime(currentClockTime)} @@ -1170,9 +1237,11 @@ export default function DraftRoom() { )} {/* Desktop Tab Navigation Row */} -
Summary - {userTeam && ( - - )} +
+ {isMyTurn && season.status === "draft" && !isDraftComplete && ( + isOvernightPause ? ( + + πŸŒ™ Overnight pause active β€” timer frozen + {overnightResumesAt && ( + <> until {overnightResumesAt.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })} + )}. You can still pick. + + ) : ( + + It's your turn! + + ) + )} +
+
+ {userTeam && ( + + )} +
{/* Main Content β€” single layout tree based on isMobile to avoid duplicate component instances */} diff --git a/app/routes/leagues/$leagueId.server.ts b/app/routes/leagues/$leagueId.server.ts index 986bc44..c07a387 100644 --- a/app/routes/leagues/$leagueId.server.ts +++ b/app/routes/leagues/$leagueId.server.ts @@ -17,6 +17,7 @@ import { getUpcomingEventsForDraftedParticipants } from "~/models/scoring-event" import { getUpcomingGroupStageMatchesForParticipants } from "~/models/group-stage-match"; import { getDraftedParticipantsBySportsSeason, getDraftedParticipantsWithPoints, type DraftedParticipantWithPoints } from "~/models/draft-pick"; import { getAuditLogForSeason } from "~/models/audit-log"; +import { findUserById } from "~/models/user"; import type { Route } from "./+types/$leagueId"; export async function loader(args: Route.LoaderArgs) { @@ -205,9 +206,18 @@ export async function loader(args: Route.LoaderArgs) { ? await getAuditLogForSeason(season.id, { limit: 5 }) : { entries: [], total: 0, hasMore: false }; + // Show timezone banner in per_user overnight pause mode when user hasn't set their timezone + const showTimezoneBanner = + userId && + season?.status === "pre_draft" && + season?.overnightPauseMode === "per_user" + ? !(await findUserById(userId))?.timezone + : false; + return { league, season, + showTimezoneBanner: !!showTimezoneBanner, teams, commissioners, currentUserId: userId, diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index 642f27c..d473ee3 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -68,6 +68,10 @@ import { import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor"; import { toast } from "sonner"; +function isValidHHMM(s: string): boolean { + return /^\d{2}:\d{2}$/.test(s); +} + export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { return [{ title: `Settings β€” ${data?.league?.name ?? "League"} - Brackt` }]; } @@ -156,6 +160,8 @@ export async function loader(args: Route.LoaderArgs) { name: o.name, })); + const commishTimezone = (await findUserById(userId))?.timezone ?? null; + return { league, season, @@ -170,6 +176,7 @@ export async function loader(args: Route.LoaderArgs) { ownerMap: Object.fromEntries(ownerMap), commissioners: commissionerUserData, currentUserId: userId, + commishTimezone, }; } @@ -587,6 +594,28 @@ export async function action(args: Route.ActionArgs) { seasonUpdates.draftTimerMode = draftTimerMode; } + // Overnight pause settings + const overnightPauseMode = formData.get("overnightPauseMode") as "none" | "league" | "per_user" | null; + if (overnightPauseMode && ["none", "league", "per_user"].includes(overnightPauseMode)) { + if (overnightPauseMode !== season.overnightPauseMode) { + seasonUpdates.overnightPauseMode = overnightPauseMode; + } + if (overnightPauseMode !== "none") { + const start = formData.get("overnightPauseStart") as string | null; + const end = formData.get("overnightPauseEnd") as string | null; + const tz = formData.get("overnightPauseTimezone") as string | null; + if (start && isValidHHMM(start) && start !== season.overnightPauseStart) { + seasonUpdates.overnightPauseStart = start; + } + if (end && isValidHHMM(end) && end !== season.overnightPauseEnd) { + seasonUpdates.overnightPauseEnd = end; + } + if (tz && tz.length > 0 && tz !== season.overnightPauseTimezone) { + seasonUpdates.overnightPauseTimezone = tz; + } + } + } + // Handle scoring rules (only if in pre_draft status and values changed) if (season.status === "pre_draft") { const scoringFields = [ @@ -823,10 +852,11 @@ export async function action(args: Route.ActionArgs) { } export default function LeagueSettings({ loaderData, actionData }: Route.ComponentProps) { - const { league, season, teams, teamCount, teamsWithOwners, allSportsSeasons, draftSlots, isAdmin, allUsers, leagueMembers, ownerMap, commissioners, currentUserId } = loaderData; + const { league, season, teams, teamCount, teamsWithOwners, allSportsSeasons, draftSlots, isAdmin, allUsers, leagueMembers, ownerMap, commissioners, currentUserId, commishTimezone } = loaderData; const navigation = useNavigation(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">(season?.draftTimerMode ?? "chess_clock"); + const [overnightMode, setOvernightMode] = useState<"none" | "league" | "per_user">(season?.overnightPauseMode ?? "none"); const [selectedSports, setSelectedSports] = useState>( new Set(season?.seasonSports?.map((s) => s.sportsSeason.id) || []) ); @@ -840,11 +870,10 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone season?.draftDateTime ? new Date(season.draftDateTime) : undefined ); const [draftTime, setDraftTime] = useState( - season?.draftDateTime + season?.draftDateTime ? format(new Date(season.draftDateTime), "HH:mm") : "" ); - // Update draft order when loader data changes (after randomization) useEffect(() => { const newOrder = draftSlots.length > 0 @@ -1239,6 +1268,137 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone + {/* Overnight Pause */} + + + Overnight Pause + + Protect players from having their pick timer expire while they're asleep. + Autodraft will still fire for teams that have it enabled. + + + +
+ + +

+ Per-user mode: each player sets their timezone in their profile. League-wide mode: one timezone applies to all teams. +

+ {overnightMode === "per_user" && ( +
+

Each player's profile timezone is used to determine their overnight window. Players without one set fall back to the timezone below.

+ {!commishTimezone && ( +

+ You haven't set your timezone yet β€”{" "} + + set it in your profile + + . +

+ )} +
+ )} +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ {/* Scoring Rules */} t.ownerId === currentUserId); @@ -131,6 +133,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { return (
+ {showTimezoneBanner && }
@@ -200,6 +203,10 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) { draftBoardHref={`/leagues/${league.id}/draft-board/${season.id}`} draftRoomHref={`/leagues/${league.id}/draft/${season.id}`} userDraftPosition={userDraftPosition} + overnightPauseMode={season.overnightPauseMode} + overnightPauseStart={season.overnightPauseStart} + overnightPauseEnd={season.overnightPauseEnd} + overnightPauseTimezone={season.overnightPauseTimezone} /> )} diff --git a/app/routes/leagues/__tests__/draft-reset.test.ts b/app/routes/leagues/__tests__/draft-reset.test.ts index e4a731f..6bf810f 100644 --- a/app/routes/leagues/__tests__/draft-reset.test.ts +++ b/app/routes/leagues/__tests__/draft-reset.test.ts @@ -26,6 +26,10 @@ const createMockSeason = (overrides: { currentPickNumber: null, draftStartedAt: null, draftPaused: false, + overnightPauseMode: 'none' as const, + overnightPauseStart: null, + overnightPauseEnd: null, + overnightPauseTimezone: null, inviteCode: 'ABC123', pointsFor1st: 100, pointsFor2nd: 70, diff --git a/app/routes/leagues/__tests__/settings-update.test.ts b/app/routes/leagues/__tests__/settings-update.test.ts index 86ed53b..151696b 100644 --- a/app/routes/leagues/__tests__/settings-update.test.ts +++ b/app/routes/leagues/__tests__/settings-update.test.ts @@ -56,6 +56,10 @@ const mockSeason = { currentPickNumber: null, draftStartedAt: null, draftPaused: false, + overnightPauseMode: 'none' as const, + overnightPauseStart: null, + overnightPauseEnd: null, + overnightPauseTimezone: null, inviteCode: 'TEST123', pointsFor1st: 100, pointsFor2nd: 70, diff --git a/app/routes/leagues/__tests__/team-management.test.ts b/app/routes/leagues/__tests__/team-management.test.ts index adbb154..20b60b7 100644 --- a/app/routes/leagues/__tests__/team-management.test.ts +++ b/app/routes/leagues/__tests__/team-management.test.ts @@ -191,6 +191,7 @@ describe('Team Management - Admin User List', () => { lastName: 'One', imageUrl: null, isAdmin: false, + timezone: null, createdAt: new Date(), updatedAt: new Date(), }, @@ -205,6 +206,7 @@ describe('Team Management - Admin User List', () => { lastName: 'Two', imageUrl: null, isAdmin: false, + timezone: null, createdAt: new Date(), updatedAt: new Date(), }, @@ -240,6 +242,7 @@ describe('Team Management - Admin User List', () => { lastName: 'One', imageUrl: null, isAdmin: false, + timezone: null, createdAt: new Date(), updatedAt: new Date(), }, diff --git a/app/routes/leagues/new.tsx b/app/routes/leagues/new.tsx index 51bfcbc..eb79e39 100644 --- a/app/routes/leagues/new.tsx +++ b/app/routes/leagues/new.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Form, redirect, Link } from "react-router"; import { auth } from "~/lib/auth.server"; import type { Route } from "./+types/new"; @@ -11,6 +11,7 @@ import { linkMultipleSportsToSeason } from "~/models/season-sport"; import { createManyTeams } from "~/models/team"; import { findActiveSeasonTemplates, findSeasonTemplateWithSportsSeasons } from "~/models/season-template"; import { findDraftableSportsSeasons } from "~/models/sports-season"; +import { findUserById } from "~/models/user"; import { generateUniqueTeamNames } from "~/utils/team-names"; import { format } from "date-fns"; import { CalendarIcon, Check } from "lucide-react"; @@ -42,16 +43,19 @@ import { import { cn } from "~/lib/utils"; import { parseDraftSpeed } from "~/lib/draft-timer"; import { ScoringRulesEditor } from "~/components/scoring/ScoringRulesEditor"; +import { TimezoneSelect } from "~/components/league/TimezoneSelect"; export function meta(): Route.MetaDescriptors { return [{ title: "New League - Brackt" }]; } -export async function loader() { +export async function loader(args: Route.LoaderArgs) { + const session = await auth.api.getSession({ headers: args.request.headers }); + const userId = session?.user.id ?? null; + const templates = await findActiveSeasonTemplates(); const allSportsSeasons = await findDraftableSportsSeasons(); - - // Fetch templates with their sports seasons for mapping + const templatesWithSports = await Promise.all( templates.map(async (template) => { const templateWithSports = await findSeasonTemplateWithSportsSeasons(template.id); @@ -63,10 +67,15 @@ export async function loader() { }; }) ); - + + const commishTimezone = userId + ? ((await findUserById(userId))?.timezone ?? null) + : null; + return { templates: templatesWithSports, - allSportsSeasons: allSportsSeasons as Array + allSportsSeasons: allSportsSeasons as Array, + commishTimezone, }; } @@ -74,7 +83,7 @@ export async function action(args: Route.ActionArgs) { const session = await auth.api.getSession({ headers: args.request.headers }); const userId = session?.user.id ?? null; const { request } = args; - + if (!userId) { throw new Response("Unauthorized", { status: 401 }); } @@ -88,11 +97,9 @@ export async function action(args: Route.ActionArgs) { const draftSpeed = formData.get("draftSpeed"); const draftTimerMode = (formData.get("draftTimerMode") as "chess_clock" | "standard") || "chess_clock"; - // Map draft speed to time values const { draftInitialTime: draftInitialTimeNum, draftIncrementTime: draftIncrementTimeNum } = parseDraftSpeed(draftSpeed as string | null, draftTimerMode); - // Validation if (typeof name !== "string" || !name.trim()) { return { error: "League name is required" }; } @@ -111,7 +118,6 @@ export async function action(args: Route.ActionArgs) { return { error: "Draft rounds must be between 1 and 50" }; } - // Extract and validate scoring rules const scoringFields = [ "pointsFor1st", "pointsFor2nd", "pointsFor3rd", "pointsFor4th", "pointsFor5th", "pointsFor6th", "pointsFor7th", "pointsFor8th" @@ -131,20 +137,30 @@ export async function action(args: Route.ActionArgs) { } } + const rawPauseMode = formData.get("overnightPauseMode") as string | null; + const overnightPauseMode: "none" | "league" | "per_user" = + rawPauseMode === "league" || rawPauseMode === "per_user" ? rawPauseMode : "none"; + const overnightPauseStart = overnightPauseMode !== "none" + ? (formData.get("overnightPauseStart") as string | null) ?? "23:00" + : null; + const overnightPauseEnd = overnightPauseMode !== "none" + ? (formData.get("overnightPauseEnd") as string | null) ?? "07:00" + : null; + const overnightPauseTimezone = overnightPauseMode !== "none" + ? (formData.get("overnightPauseTimezone") as string | null) ?? null + : null; + try { - // Create league const league = await createLeague({ name: name.trim(), createdBy: userId, }); - // Make creator a commissioner await createCommissioner({ leagueId: league.id, userId: userId, }); - // Create first season const currentYear = new Date().getFullYear(); const season = await createSeason({ leagueId: league.id, @@ -156,22 +172,23 @@ export async function action(args: Route.ActionArgs) { draftInitialTime: draftInitialTimeNum, draftIncrementTime: draftIncrementTimeNum, draftTimerMode, + overnightPauseMode, + overnightPauseStart, + overnightPauseEnd, + overnightPauseTimezone, ...scoringRules, }); - // Set this as the current season for the league await setCurrentSeason(league.id, season.id); - // Add selected sports to the season const selectedSports = formData.getAll("sportsSeasons"); - - // Validate that draft rounds is at least the number of sports + if (selectedSports.length > draftRoundsNum) { - return { - error: `You need at least ${selectedSports.length} draft rounds for the ${selectedSports.length} sports selected. Currently set to ${draftRoundsNum} rounds.` + return { + error: `You need at least ${selectedSports.length} draft rounds for the ${selectedSports.length} sports selected. Currently set to ${draftRoundsNum} rounds.` }; } - + if (selectedSports.length > 0) { await linkMultipleSportsToSeason( selectedSports.map((id) => ({ @@ -181,7 +198,6 @@ export async function action(args: Route.ActionArgs) { ); } - // Create teams with fun random names const joinAsPlayer = formData.get("joinAsPlayer") === "on"; const teamNames = generateUniqueTeamNames(teamCountNum); const teams = teamNames.map((teamName, i) => ({ @@ -192,7 +208,6 @@ export async function action(args: Route.ActionArgs) { await createManyTeams(teams); - // Redirect to league homepage return redirect(`/leagues/${league.id}`); } catch (error) { logger.error("Error creating league:", error); @@ -201,13 +216,22 @@ export async function action(args: Route.ActionArgs) { } export default function NewLeague({ loaderData, actionData }: Route.ComponentProps) { - const { templates, allSportsSeasons } = loaderData; + const { templates, allSportsSeasons, commishTimezone } = loaderData; const [selectedTemplate, setSelectedTemplate] = useState(""); const [selectedSports, setSelectedSports] = useState>(new Set()); const [draftRounds, setDraftRounds] = useState(20); const [draftDate, setDraftDate] = useState(); const [draftTime, setDraftTime] = useState(""); const [timerMode, setTimerMode] = useState<"chess_clock" | "standard">("chess_clock"); + const [overnightMode, setOvernightMode] = useState<"none" | "league" | "per_user">("none"); + const [overnightTimezone, setOvernightTimezone] = useState(commishTimezone ?? ""); + + useEffect(() => { + if (!overnightTimezone) { + const detected = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (detected) setOvernightTimezone(detected); + } + }, [overnightTimezone]); const handleSportToggle = (sportId: string) => { const newSelected = new Set(selectedSports); @@ -224,7 +248,6 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro setSelectedSports(new Set(sportsSeasonIds)); }; - // Calculate flex spots and minimum rounds const sportsCount = selectedSports.size; const minRounds = sportsCount; const recommendedRounds = Math.ceil(sportsCount * 1.25); @@ -281,9 +304,9 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
- )}

@@ -425,6 +448,89 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro

+
+
+ + +

+ Protect players from having their pick timer expire while they're asleep. Autodraft still fires normally. +

+
+ + {overnightMode === "per_user" && ( +
+

Each player's profile timezone is used to determine their overnight window. Players without one set fall back to the timezone below.

+ {!commishTimezone && ( +

+ You haven't set your timezone yet β€”{" "} + + set it in your profile + + . +

+ )} +
+ )} + + {overnightMode !== "none" && ( + <> +
+
+ + +
+
+ + +
+
+ +
+ + + {overnightMode === "per_user" && ( +

+ Each player sets their timezone in their profile. This is used as a fallback if they haven't set one. +

+ )} +
+ + )} +
+
@@ -432,10 +538,9 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro

Choose a template to auto-select sports, or manually select your own

- + - - {/* Template Cards */} + {templates.length > 0 && (
{templates.map((template) => ( @@ -471,7 +576,6 @@ export default function NewLeague({ loaderData, actionData }: Route.ComponentPro
)} - {/* Sports Checkboxes */}

diff --git a/app/routes/user-profile.tsx b/app/routes/user-profile.tsx index 1c2b596..dc1945a 100644 --- a/app/routes/user-profile.tsx +++ b/app/routes/user-profile.tsx @@ -1,11 +1,17 @@ import { redirect, Form } from "react-router"; +import { useEffect, useState } from "react"; import { auth } from "~/lib/auth.server"; -import { findUserById, updateUser } from "~/models/user"; +import { findUserById, updateUser, isUserInActiveDraft } from "~/models/user"; import type { Route } from "./+types/user-profile"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; +import { TimezoneSelect, TIMEZONE_OPTIONS } from "~/components/league/TimezoneSelect"; + +const VALID_TIMEZONES = new Set( + TIMEZONE_OPTIONS.flatMap((g) => g.zones.map((z) => z.value)) +); export function meta(): Route.MetaDescriptors { return [{ title: "Profile - Brackt" }]; @@ -20,7 +26,8 @@ export async function loader(args: Route.LoaderArgs) { if (!user) { return redirect("/"); } - return { user }; + const isInActiveDraft = await isUserInActiveDraft(session.user.id); + return { user, isInActiveDraft }; } export async function action(args: Route.ActionArgs) { @@ -34,19 +41,39 @@ export async function action(args: Route.ActionArgs) { const username = formData.get("username") as string | null; const firstName = formData.get("firstName") as string | null; const lastName = formData.get("lastName") as string | null; + const timezone = formData.get("timezone") as string | null; + + const timezoneValue = + timezone && VALID_TIMEZONES.has(timezone) ? timezone : undefined; await updateUser(session.user.id, { displayName: displayName || undefined, username: username || undefined, firstName: firstName || undefined, lastName: lastName || undefined, + timezone: timezoneValue, }); return { success: true }; } export default function UserProfilePage({ loaderData, actionData }: Route.ComponentProps) { - const { user } = loaderData; + const { user, isInActiveDraft } = loaderData; + const [timezone, setTimezone] = useState(user.timezone ?? ""); + + // Auto-detect browser timezone if not already set + useEffect(() => { + if (!user.timezone) { + try { + const detected = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (detected && VALID_TIMEZONES.has(detected)) { + setTimezone(detected); + } + } catch { + // Intl not available β€” leave blank + } + } + }, [user.timezone]); return (

@@ -97,6 +124,27 @@ export default function UserProfilePage({ loaderData, actionData }: Route.Compon

{user.email}

+ +
+ + {/* Hidden input carries the controlled value into the form submission */} + + + {isInActiveDraft ? ( +

+ Timezone cannot be changed while you are in an active draft. +

+ ) : ( +

+ Used for overnight pick protection. We pre-filled your browser's detected timezone. +

+ )} +
+ diff --git a/database/schema.ts b/database/schema.ts index 4cfbf14..123921e 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -12,6 +12,7 @@ export const users = pgTable("users", { lastName: varchar("last_name", { length: 255 }), imageUrl: varchar("image_url", { length: 512 }), // mapped as BetterAuth's "image" field isAdmin: boolean("is_admin").notNull().default(false), + timezone: varchar("timezone", { length: 50 }), // IANA timezone string, e.g. "America/Chicago" createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); @@ -107,6 +108,12 @@ export const draftTimerModeEnum = pgEnum("draft_timer_mode", [ "standard", ]); +export const overnightPauseModeEnum = pgEnum("overnight_pause_mode", [ + "none", + "league", + "per_user", +]); + export const probabilitySourceEnum = pgEnum("probability_source", [ "manual", "futures_odds", @@ -188,6 +195,10 @@ export const seasons = pgTable("seasons", { currentPickNumber: integer("current_pick_number").default(1), draftStartedAt: timestamp("draft_started_at"), draftPaused: boolean("draft_paused").notNull().default(false), + overnightPauseMode: overnightPauseModeEnum("overnight_pause_mode").notNull().default("none"), + overnightPauseStart: varchar("overnight_pause_start", { length: 5 }), // "23:00" + overnightPauseEnd: varchar("overnight_pause_end", { length: 5 }), // "07:00" + overnightPauseTimezone: varchar("overnight_pause_timezone", { length: 50 }), // IANA β€” league TZ (league mode) or per-user fallback inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(), // Scoring configuration (8 values for 1st through 8th) pointsFor1st: integer("points_for_1st").notNull().default(100), diff --git a/drizzle/0083_fearless_leader.sql b/drizzle/0083_fearless_leader.sql new file mode 100644 index 0000000..cd2c182 --- /dev/null +++ b/drizzle/0083_fearless_leader.sql @@ -0,0 +1,6 @@ +CREATE TYPE "public"."overnight_pause_mode" AS ENUM('none', 'league', 'per_user');--> statement-breakpoint +ALTER TABLE "seasons" ADD COLUMN "overnight_pause_mode" "overnight_pause_mode" DEFAULT 'none' NOT NULL;--> statement-breakpoint +ALTER TABLE "seasons" ADD COLUMN "overnight_pause_start" varchar(5);--> statement-breakpoint +ALTER TABLE "seasons" ADD COLUMN "overnight_pause_end" varchar(5);--> statement-breakpoint +ALTER TABLE "seasons" ADD COLUMN "overnight_pause_timezone" varchar(50);--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "timezone" varchar(50); \ No newline at end of file diff --git a/drizzle/meta/0083_snapshot.json b/drizzle/meta/0083_snapshot.json new file mode 100644 index 0000000..9635f2a --- /dev/null +++ b/drizzle/meta/0083_snapshot.json @@ -0,0 +1,5102 @@ +{ + "id": "5862dc64-8b4f-4b9d-949c-45ecc05b8890", + "prevId": "7fc7f723-b4c3-418d-a5dc-b282c1a6def4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "queue_only": { + "name": "queue_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioner_audit_log": { + "name": "commissioner_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_clerk_id": { + "name": "actor_clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "actor_display_name": { + "name": "actor_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "affected_team_ids": { + "name": "affected_team_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_season_id_idx": { + "name": "audit_log_season_id_idx", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "commissioner_audit_log_season_id_seasons_id_fk": { + "name": "commissioner_audit_log_season_id_seasons_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "commissioner_audit_log_league_id_leagues_id_fk": { + "name": "commissioner_audit_log_league_id_leagues_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cs2_major_stage_results": { + "name": "cs2_major_stage_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_entry": { + "name": "stage_entry", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_eliminated": { + "name": "stage_eliminated", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stage_eliminated_wins": { + "name": "stage_eliminated_wins", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_placement": { + "name": "final_placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cs2_major_stage_results_unique": { + "name": "cs2_major_stage_results_unique", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk": { + "name": "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cs2_major_stage_results_participant_id_participants_id_fk": { + "name": "cs2_major_stage_results_participant_id_participants_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "draft_picks_season_pick_unique": { + "name": "draft_picks_season_pick_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pick_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_participants_id_fk": { + "name": "draft_picks_participant_id_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_participants_id_fk": { + "name": "draft_queue_participant_id_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_participant_id_participants_id_fk": { + "name": "event_results_participant_id_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_stage_matches": { + "name": "group_stage_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_score": { + "name": "participant1_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "matchday": { + "name": "matchday", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_stage_matches_group_pair_unique": { + "name": "group_stage_matches_group_pair_unique", + "columns": [ + { + "expression": "tournament_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant2_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_stage_matches_tournament_group_id_tournament_groups_id_fk": { + "name": "group_stage_matches_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_stage_matches_participant1_id_participants_id_fk": { + "name": "group_stage_matches_participant1_id_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "group_stage_matches_participant2_id_participants_id_fk": { + "name": "group_stage_matches_participant2_id_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discord_webhook_url": { + "name": "discord_webhook_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_ev_snapshots": { + "name": "participant_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_ev": { + "name": "calculated_ev", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_snapshots_unique": { + "name": "participant_ev_snapshots_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_ev_snapshots_participant_id_participants_id_fk": { + "name": "participant_ev_snapshots_participant_id_participants_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk": { + "name": "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_expected_values": { + "name": "participant_expected_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "probability_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'manual'" + }, + "source_odds": { + "name": "source_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_elo": { + "name": "source_elo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_participant_season_unique": { + "name": "participant_ev_participant_season_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_expected_values_participant_id_participants_id_fk": { + "name": "participant_expected_values_participant_id_participants_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_expected_values_sports_season_id_sports_seasons_id_fk": { + "name": "participant_expected_values_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_golf_skills": { + "name": "participant_golf_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sg_total": { + "name": "sg_total", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "datagolf_rank": { + "name": "datagolf_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "masters_odds": { + "name": "masters_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "us_open_odds": { + "name": "us_open_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "open_championship_odds": { + "name": "open_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pga_championship_odds": { + "name": "pga_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_golf_skills_unique": { + "name": "participant_golf_skills_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_golf_skills_participant_id_participants_id_fk": { + "name": "participant_golf_skills_participant_id_participants_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_golf_skills_sports_season_id_sports_seasons_id_fk": { + "name": "participant_golf_skills_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_qualifying_totals": { + "name": "participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_qualifying_totals_participant_id_participants_id_fk": { + "name": "participant_qualifying_totals_participant_id_participants_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_results": { + "name": "participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_partial_score": { + "name": "is_partial_score", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_results_participant_id_participants_id_fk": { + "name": "participant_results_participant_id_participants_id_fk", + "tableFrom": "participant_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_season_results_participant_id_participants_id_fk": { + "name": "participant_season_results_participant_id_participants_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_season_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_season_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_surface_elos": { + "name": "participant_surface_elos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_hard": { + "name": "elo_hard", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_clay": { + "name": "elo_clay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_grass": { + "name": "elo_grass", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_surface_elos_unique": { + "name": "participant_surface_elos_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_surface_elos_participant_id_participants_id_fk": { + "name": "participant_surface_elos_participant_id_participants_id_fk", + "tableFrom": "participant_surface_elos", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_surface_elos_sports_season_id_sports_seasons_id_fk": { + "name": "participant_surface_elos_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_surface_elos", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participants": { + "name": "participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "vorp_value": { + "name": "vorp_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participants_sports_season_name_unique": { + "name": "participants_sports_season_name_unique", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participants_sports_season_id_sports_seasons_id_fk": { + "name": "participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_standings_mappings": { + "name": "pending_standings_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_team_id": { + "name": "external_team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "standing_data": { + "name": "standing_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "psm_season_external_id_idx": { + "name": "psm_season_external_id_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_standings_mappings_sports_season_id_sports_seasons_id_fk": { + "name": "pending_standings_mappings_sports_season_id_sports_seasons_id_fk", + "tableFrom": "pending_standings_mappings", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_match_games": { + "name": "playoff_match_games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "playoff_match_game_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_match_games_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_games_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_games_winner_id_participants_id_fk": { + "name": "playoff_match_games_winner_id_participants_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_match_odds": { + "name": "playoff_match_odds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "moneyline_odds": { + "name": "moneyline_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "implied_probability": { + "name": "implied_probability", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "odds_source": { + "name": "odds_source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_match_odds_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_odds_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_odds_participant_id_participants_id_fk": { + "name": "playoff_match_odds_participant_id_participants_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_matches": { + "name": "playoff_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "match_number": { + "name": "match_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "loser_id": { + "name": "loser_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_scoring": { + "name": "is_scoring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "template_round": { + "name": "template_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "seed_info": { + "name": "seed_info", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_matches_scoring_event_id_scoring_events_id_fk": { + "name": "playoff_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_matches_participant1_id_participants_id_fk": { + "name": "playoff_matches_participant1_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_participant2_id_participants_id_fk": { + "name": "playoff_matches_participant2_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_winner_id_participants_id_fk": { + "name": "playoff_matches_winner_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_loser_id_participants_id_fk": { + "name": "playoff_matches_loser_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "loser_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifying_point_config": { + "name": "qualifying_point_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "qualifying_point_config_sports_season_id_sports_seasons_id_fk": { + "name": "qualifying_point_config_sports_season_id_sports_seasons_id_fk", + "tableFrom": "qualifying_point_config", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.regular_season_standings": { + "name": "regular_season_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ot_losses": { + "name": "ot_losses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ties": { + "name": "ties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "win_pct": { + "name": "win_pct", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "games_back": { + "name": "games_back", + "type": "numeric(5, 1)", + "primaryKey": false, + "notNull": false + }, + "conference": { + "name": "conference", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "division": { + "name": "division", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "conference_rank": { + "name": "conference_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "division_rank": { + "name": "division_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "league_rank": { + "name": "league_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "streak": { + "name": "streak", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "last_ten": { + "name": "last_ten", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "home_record": { + "name": "home_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "away_record": { + "name": "away_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "external_team_id": { + "name": "external_team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "srs": { + "name": "srs", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rss_participant_season_idx": { + "name": "rss_participant_season_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "regular_season_standings_participant_id_participants_id_fk": { + "name": "regular_season_standings_participant_id_participants_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "regular_season_standings_sports_season_id_sports_seasons_id_fk": { + "name": "regular_season_standings_sports_season_id_sports_seasons_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_starts_at": { + "name": "event_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bracket_template_id": { + "name": "bracket_template_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "scoring_starts_at_round": { + "name": "scoring_starts_at_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "bracket_region_config": { + "name": "bracket_region_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scoring_events_sports_season_id_sports_seasons_id_fk": { + "name": "scoring_events_sports_season_id_sports_seasons_id_fk", + "tableFrom": "scoring_events", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_template_sports_template_id_season_templates_id_fk": { + "name": "season_template_sports_template_id_season_templates_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_template_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_template_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_templates": { + "name": "season_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "draft_rounds": { + "name": "draft_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "flex_spots": { + "name": "flex_spots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draft_date_time": { + "name": "draft_date_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "draft_timer_mode": { + "name": "draft_timer_mode", + "type": "draft_timer_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'chess_clock'" + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "overnight_pause_mode": { + "name": "overnight_pause_mode", + "type": "overnight_pause_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "overnight_pause_start": { + "name": "overnight_pause_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_end": { + "name": "overnight_pause_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_timezone": { + "name": "overnight_pause_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "scoring_type": { + "name": "scoring_type", + "type": "scoring_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "scoring_pattern": { + "name": "scoring_pattern", + "type": "scoring_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_majors": { + "name": "total_majors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "majors_completed": { + "name": "majors_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points_finalized": { + "name": "qualifying_points_finalized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elo_calibration_exponent": { + "name": "elo_calibration_exponent", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "elo_min_rating": { + "name": "elo_min_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1250 + }, + "elo_max_rating": { + "name": "elo_max_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1750 + }, + "simulation_status": { + "name": "simulation_status", + "type": "simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "draft_on": { + "name": "draft_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "draft_off": { + "name": "draft_off", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sports_seasons_sport_id_sports_id_fk": { + "name": "sports_seasons_sport_id_sports_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_ev_snapshots": { + "name": "team_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_ev_snapshots_unique": { + "name": "team_ev_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_ev_snapshots_team_id_teams_id_fk": { + "name": "team_ev_snapshots_team_id_teams_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_ev_snapshots_season_id_seasons_id_fk": { + "name": "team_ev_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_score_events": { + "name": "team_score_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scoring_event_name": { + "name": "scoring_event_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sport_name": { + "name": "sport_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant_ids": { + "name": "participant_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "points_delta": { + "name": "points_delta", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_score_events_match_unique": { + "name": "team_score_events_match_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_score_events_event_unique": { + "name": "team_score_events_event_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_score_events_team_id_teams_id_fk": { + "name": "team_score_events_team_id_teams_id_fk", + "tableFrom": "team_score_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_season_id_seasons_id_fk": { + "name": "team_score_events_season_id_seasons_id_fk", + "tableFrom": "team_score_events", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_scoring_event_id_scoring_events_id_fk": { + "name": "team_score_events_scoring_event_id_scoring_events_id_fk", + "tableFrom": "team_score_events", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "team_score_events_match_id_playoff_matches_id_fk": { + "name": "team_score_events_match_id_playoff_matches_id_fk", + "tableFrom": "team_score_events", + "tableTo": "playoff_matches", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_standings_snapshots_unique": { + "name": "team_standings_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_group_members": { + "name": "tournament_group_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_group_members_tournament_group_id_tournament_groups_id_fk": { + "name": "tournament_group_members_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_group_members_participant_id_participants_id_fk": { + "name": "tournament_group_members_participant_id_participants_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_groups": { + "name": "tournament_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_groups_scoring_event_id_scoring_events_id_fk": { + "name": "tournament_groups_scoring_event_id_scoring_events_id_fk", + "tableFrom": "tournament_groups", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "league_settings_changed", + "draft_settings_changed", + "scoring_rules_changed", + "sports_changed", + "draft_order_set", + "draft_order_randomized", + "draft_started", + "draft_paused", + "draft_resumed", + "draft_reset", + "draft_rollback", + "force_autopick", + "force_manual_pick", + "draft_pick_changed", + "time_bank_edited" + ] + }, + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.draft_timer_mode": { + "name": "draft_timer_mode", + "schema": "public", + "values": [ + "chess_clock", + "standard" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings", + "schedule_event" + ] + }, + "public.overnight_pause_mode": { + "name": "overnight_pause_mode", + "schema": "public", + "values": [ + "none", + "league", + "per_user" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "admin", + "auto" + ] + }, + "public.playoff_match_game_status": { + "name": "playoff_match_game_status", + "schema": "public", + "values": [ + "scheduled", + "complete", + "postponed" + ] + }, + "public.probability_source": { + "name": "probability_source", + "schema": "public", + "values": [ + "manual", + "futures_odds", + "elo_simulation", + "performance_model" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "playoff_bracket", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.simulation_status": { + "name": "simulation_status", + "schema": "public", + "values": [ + "idle", + "running", + "failed" + ] + }, + "public.simulator_type": { + "name": "simulator_type", + "schema": "public", + "values": [ + "f1_standings", + "indycar_standings", + "golf_qualifying_points", + "playoff_bracket", + "ucl_bracket", + "ncaam_bracket", + "ncaaw_bracket", + "nba_bracket", + "nhl_bracket", + "nfl_bracket", + "afl_bracket", + "snooker_bracket", + "tennis_qualifying_points", + "mlb_bracket", + "wnba_bracket", + "world_cup", + "darts_bracket", + "cs2_major_qualifying_points", + "ncaa_football_bracket", + "llws_bracket" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 0fd262c..1334537 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -582,6 +582,13 @@ "when": 1777090754037, "tag": "0082_stale_pride", "breakpoints": true + }, + { + "idx": 83, + "version": "7", + "when": 1777180952144, + "tag": "0083_fearless_leader", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/timer.ts b/server/timer.ts index 7f19994..50b27ee 100644 --- a/server/timer.ts +++ b/server/timer.ts @@ -3,6 +3,7 @@ import { eq, and, asc, sql } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm"; import { getSocketIO } from "./socket"; import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils"; +import { isInOvernightWindow, getOvernightResumeUTC } from "~/lib/overnight-pause"; import { logger } from "./logger"; import { db } from "./db"; @@ -13,6 +14,63 @@ let timerTickRunning = false; // a redundant query on every tick. const draftSlotsCache = new Map(); +// Team timezone cache for per_user overnight pause mode (seasonId β†’ teamId β†’ timezone). +// Populated lazily; evicted when the season leaves active drafting. +const teamTimezoneCache = new Map>(); + +async function getTeamTimezone(seasonId: string, teamId: string): Promise { + let seasonMap = teamTimezoneCache.get(seasonId); + if (!seasonMap) { + try { + const rows = await db + .select({ teamId: schema.teams.id, timezone: schema.users.timezone }) + .from(schema.teams) + .leftJoin(schema.users, sql`${schema.teams.ownerId}::uuid = ${schema.users.id}`) + .where(eq(schema.teams.seasonId, seasonId)); + seasonMap = new Map(rows.map((r) => [r.teamId, r.timezone || null])); + } catch (err) { + // ownerId column is varchar; if any row still holds a non-UUID value + // (pre-migration remnant) the ::uuid cast throws. Degrade gracefully. + logger.error("[Timer] getTeamTimezone cast failed, skipping overnight check:", err); + seasonMap = new Map(); + } + teamTimezoneCache.set(seasonId, seasonMap); + } + return seasonMap.get(teamId) ?? null; +} + +/** + * Determine whether the current team's pick is in an overnight pause window. + * Returns { active: false } or { active: true, resumesAtUTC: number }. + */ +async function checkOvernightPause( + season: InferSelectModel, + currentTeamId: string +): Promise<{ active: boolean; resumesAtUTC?: number }> { + const mode = season.overnightPauseMode; + const start = season.overnightPauseStart; + const end = season.overnightPauseEnd; + + if (mode === "none" || !start || !end) return { active: false }; + + let tz: string | null = null; + if (mode === "league") { + tz = season.overnightPauseTimezone || null; + } else { + // per_user: use the team owner's timezone, fall back to league timezone + tz = await getTeamTimezone(season.id, currentTeamId); + if (!tz) tz = season.overnightPauseTimezone || null; + } + + if (!tz) return { active: false }; + + if (isInOvernightWindow(tz, start, end)) { + const resumesAt = getOvernightResumeUTC(tz, end); + return { active: true, resumesAtUTC: resumesAt.getTime() }; + } + return { active: false }; +} + /** * Start the draft timer system * Runs every second to update all active draft timers @@ -73,6 +131,9 @@ async function updateDraftTimers(): Promise { for (const cachedId of draftSlotsCache.keys()) { if (!activeIds.has(cachedId)) draftSlotsCache.delete(cachedId); } + for (const cachedId of teamTimezoneCache.keys()) { + if (!activeIds.has(cachedId)) teamTimezoneCache.delete(cachedId); + } for (const season of activeDrafts) { // Skip if draft is paused @@ -153,6 +214,10 @@ async function updateDraftTimers(): Promise { // while_on means "pick immediately when it's my turn" β€” bypass the countdown. const isWhileOn = shouldAutodraft && autodraftSettings?.mode === "while_on"; + // Overnight pause: freeze timer for non-autodraft teams in their overnight window. + const overnightPause = await checkOvernightPause(season, currentTeamId); + const isOvernightFreeze = overnightPause.active && !shouldAutodraft; + // Trigger pick when: timer expired OR team is in while_on autodraft mode. // The chain picks consecutive while_on teams after each pick, but it is capped at // totalTeams iterations. The while_on bypass here fills the gap: the timer picks @@ -174,6 +239,7 @@ async function updateDraftTimers(): Promise { teamId: currentTeamId, timeRemaining: 0, currentPickNumber, + overnightPauseActive: false, }); } @@ -188,6 +254,19 @@ async function updateDraftTimers(): Promise { continue; } + // Overnight freeze: skip decrement, notify clients to display the pause indicator. + if (isOvernightFreeze) { + io.to(`draft-${season.id}`).emit("timer-update", { + seasonId: season.id, + teamId: currentTeamId, + timeRemaining: timer.timeRemaining, + currentPickNumber, + overnightPauseActive: true, + resumesAtUTC: overnightPause.resumesAtUTC, + }); + continue; + } + // Atomically decrement timer (race-condition safe: uses DB-level update // so concurrent increments from pick handlers are never overwritten) const [updatedTimer] = await db @@ -207,6 +286,7 @@ async function updateDraftTimers(): Promise { teamId: currentTeamId, timeRemaining: newTimeRemaining, currentPickNumber, + overnightPauseActive: false, }); // If timer just hit 0, trigger auto-pick (next_pick mode or no autodraft)