From 73f0256ff5a999e22bea58b6e8b847a88899c641 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 6 Jun 2026 09:38:47 -0700 Subject: [PATCH 1/2] Fix QP standings: filter to drafted/points-earning participants with correct global ranks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filter the QP table to show only participants with QP > 0 or drafted by any team in the league (undrafted 0-QP participants hidden) - Compute global ranks with tie handling across the full field before filtering so displayed rank numbers and the top-8 Points Line remain correct after rows are removed - Fix flaky CI timeouts: world-cup simulator test (100 → 50 iterations), SportsSection userEvent test (explicit 15s timeout) Co-Authored-By: Claude Sonnet 4.6 --- .../QualifyingPointsStandings.test.tsx | 3 +++ .../settings/__tests__/SportsSection.test.tsx | 2 +- .../scoring/QualifyingPointsStandings.tsx | 26 +++++-------------- app/components/scoring/SportSeasonDisplay.tsx | 1 + .../admin.sports-seasons.$id.events.server.ts | 18 +++++++++++-- ...d.sports-seasons.$sportsSeasonId.server.ts | 25 +++++++++++++++--- .../__tests__/world-cup-simulator.test.ts | 2 +- 7 files changed, 50 insertions(+), 27 deletions(-) diff --git a/app/components/__tests__/QualifyingPointsStandings.test.tsx b/app/components/__tests__/QualifyingPointsStandings.test.tsx index 133ff8e..d66718b 100644 --- a/app/components/__tests__/QualifyingPointsStandings.test.tsx +++ b/app/components/__tests__/QualifyingPointsStandings.test.tsx @@ -23,6 +23,7 @@ describe("QualifyingPointsStandings", () => { totalQualifyingPoints: "14.67", eventsScored: 1, finalRanking: null, + globalRank: 1, participant: { id: "participant-1", name: "Player One" }, }, { @@ -30,6 +31,7 @@ describe("QualifyingPointsStandings", () => { totalQualifyingPoints: "0.50", eventsScored: 1, finalRanking: null, + globalRank: 2, participant: { id: "participant-2", name: "Player Two" }, }, { @@ -37,6 +39,7 @@ describe("QualifyingPointsStandings", () => { totalQualifyingPoints: "0.43", eventsScored: 1, finalRanking: null, + globalRank: 3, participant: { id: "participant-3", name: "Player Three" }, }, ]} diff --git a/app/components/league/settings/__tests__/SportsSection.test.tsx b/app/components/league/settings/__tests__/SportsSection.test.tsx index 763c74f..5b1abc3 100644 --- a/app/components/league/settings/__tests__/SportsSection.test.tsx +++ b/app/components/league/settings/__tests__/SportsSection.test.tsx @@ -82,7 +82,7 @@ describe("SportsSection", () => { await user.click(screen.getByText("2025 MLB Season")); expect(mlbCheckbox).toBeChecked(); - }); + }, 15000); it("submits the currently selected sports seasons after toggling", async () => { const user = userEvent.setup(); diff --git a/app/components/scoring/QualifyingPointsStandings.tsx b/app/components/scoring/QualifyingPointsStandings.tsx index 7f00b9d..21ecfc0 100644 --- a/app/components/scoring/QualifyingPointsStandings.tsx +++ b/app/components/scoring/QualifyingPointsStandings.tsx @@ -23,6 +23,7 @@ interface QPStanding { totalQualifyingPoints: string; eventsScored: number; finalRanking: number | null; + globalRank: number; participant: { id: string; name: string; @@ -70,26 +71,11 @@ export function QualifyingPointsStandings({ const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o])); const userParticipantSet = new Set(userParticipantIds); - // Assign current ranks with tie handling - const rankedStandings: Array = []; - let previousQP = -1; - let previousRankStart = -1; - - for (let index = 0; index < standings.length; index++) { - const standing = standings[index]; - const currentQP = parseFloat(standing.totalQualifyingPoints); - - let currentRank: number; - if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) { - currentRank = previousRankStart; - } else { - currentRank = index + 1; - } - - rankedStandings.push({ ...standing, currentRank }); - previousRankStart = currentRank; - previousQP = currentQP; - } + // Use the pre-computed global rank from the loader so displayed ranks reflect the full + // participant field even when the array has been filtered down to drafted/points-earning rows. + const rankedStandings: Array = standings.map( + (standing) => ({ ...standing, currentRank: standing.globalRank }) + ); const tiedCountByRank = new Map(); for (const s of rankedStandings) { diff --git a/app/components/scoring/SportSeasonDisplay.tsx b/app/components/scoring/SportSeasonDisplay.tsx index f8d1591..eec53f5 100644 --- a/app/components/scoring/SportSeasonDisplay.tsx +++ b/app/components/scoring/SportSeasonDisplay.tsx @@ -64,6 +64,7 @@ interface QPStanding { totalQualifyingPoints: string; eventsScored: number; finalRanking: number | null; + globalRank: number; participant: { id: string; name: string; diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index d0bb526..1a59cdf 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -25,11 +25,25 @@ export async function loader({ params }: Route.LoaderArgs) { const events = await getScoringEventsForSportsSeason(params.id); - // For qualifying sports seasons, get QP standings + // For qualifying sports seasons, get QP standings with global ranks attached let qpStandings = null; const scoringRules = null; if (sportsSeason.scoringPattern === "qualifying_points") { - qpStandings = await getQPStandings(params.id); + const standings = await getQPStandings(params.id); + let prevQP = -1; + let prevRankStart = 1; + qpStandings = standings.map((s, index) => { + const qp = parseFloat(s.totalQualifyingPoints); + let rank: number; + if (index > 0 && Math.abs(qp - prevQP) < 0.001) { + rank = prevRankStart; + } else { + rank = index + 1; + prevRankStart = rank; + } + prevQP = qp; + return { ...s, globalRank: rank }; + }); } // Tournaments for this sport that don't already have a scoring event in this season. diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts index 4af7fbc..2b33e66 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts @@ -155,7 +155,7 @@ export async function loader(args: Route.LoaderArgs) { let participantPoints: { participantId: string; points: number }[] = []; let partialScoreParticipantIds: string[] = []; let seasonStandings: SeasonStanding[] = []; - type QPStanding = Awaited>[number]; + type QPStanding = Awaited>[number] & { globalRank: number }; let qpStandings: QPStanding[] = []; // Group standings for group-stage events (e.g. FIFA World Cup) @@ -293,9 +293,28 @@ export async function loader(args: Route.LoaderArgs) { }, })); } else if (scoringPattern === "qualifying_points") { - // Fetch qualifying points standings const standings = await getQPStandings(sportsSeasonId); - qpStandings = standings; + // Compute global ranks with tie handling across the full field before filtering, + // so the displayed rank numbers remain correct after undrafted/zero-QP rows are removed. + let prevQP = -1; + let prevRankStart = 1; + const withGlobalRank = standings.map((s, index) => { + const qp = parseFloat(s.totalQualifyingPoints); + let rank: number; + if (index > 0 && Math.abs(qp - prevQP) < 0.001) { + rank = prevRankStart; + } else { + rank = index + 1; + prevRankStart = rank; + } + prevQP = qp; + return { ...s, globalRank: rank }; + }); + qpStandings = withGlobalRank.filter( + (s) => + parseFloat(s.totalQualifyingPoints) > 0 || + ownershipMap.has(s.participant.id) + ); } // Fetch event schedule for all patterns (upcoming + recent) diff --git a/app/services/simulations/__tests__/world-cup-simulator.test.ts b/app/services/simulations/__tests__/world-cup-simulator.test.ts index 0ea67b6..c8394cb 100644 --- a/app/services/simulations/__tests__/world-cup-simulator.test.ts +++ b/app/services/simulations/__tests__/world-cup-simulator.test.ts @@ -162,7 +162,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(50); const results = await sim.simulate("season-1"); // probFirst + probSecond + probThird + probFourth should cover all probability mass -- 2.45.3 From a4ef8c3aa1342870bb63b1398cbacd3c62923e05 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 6 Jun 2026 09:47:24 -0700 Subject: [PATCH 2/2] =?UTF-8?q?Fix=20remaining=20world-cup=20simulator=20t?= =?UTF-8?q?est=20timeouts=20(100=20=E2=86=92=2020=20iterations)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Column-sum and structural assertions are mathematically exact regardless of iteration count; 20 iterations runs the suite in ~270ms vs the 5s CI limit. Co-Authored-By: Claude Sonnet 4.6 --- .../simulations/__tests__/world-cup-simulator.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/simulations/__tests__/world-cup-simulator.test.ts b/app/services/simulations/__tests__/world-cup-simulator.test.ts index c8394cb..599127c 100644 --- a/app/services/simulations/__tests__/world-cup-simulator.test.ts +++ b/app/services/simulations/__tests__/world-cup-simulator.test.ts @@ -114,7 +114,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(20); await expect(sim.simulate("season-1")).rejects.toThrow("No participants found"); }); @@ -125,7 +125,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(20); const results = await sim.simulate("season-1"); expect(results).toHaveLength(48); const ids = new Set(results.map((r) => r.participantId)); @@ -141,7 +141,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(20); const results = await sim.simulate("season-1"); const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0); @@ -218,7 +218,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(20); const results = await sim.simulate("season-1"); const p3Result = results.find((r) => r.participantId === "p3"); @@ -238,7 +238,7 @@ describe("WorldCupSimulator", () => { mockDb.query.tournamentGroups.findMany.mockResolvedValue([]); mockDb.query.playoffMatches.findMany.mockResolvedValue([]); - const sim = new WorldCupSimulator(100); + const sim = new WorldCupSimulator(20); const results = await sim.simulate("season-1"); for (const r of results) { -- 2.45.3