From 58917b87cef5999929dad6c5faf0b39f287130f2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:15:32 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Replace=20O(N)=20array?= =?UTF-8?q?=20reduction=20with=20O(1)=20turn=20sum=20in=20DeckShowcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/DeckShowcase.tsx | 6 +++-- frontend/src/hooks/useWinData.ts | 29 +++++++++++++++++------- frontend/src/pages/JobStatus.test.tsx | 6 +++-- frontend/src/pages/JobStatus.tsx | 3 ++- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/DeckShowcase.tsx b/frontend/src/components/DeckShowcase.tsx index 6db54b38..e741ea87 100644 --- a/frontend/src/components/DeckShowcase.tsx +++ b/frontend/src/components/DeckShowcase.tsx @@ -9,6 +9,7 @@ interface DeckShowcaseProps { colorIdentityByDeckName: Record; winTally: Record | null; winTurns: Record | null; + winTurnSums: Record | null; gamesPlayed: number; totalSimulations: number; deckLinks?: Record; @@ -48,6 +49,7 @@ export const DeckShowcase = memo(function DeckShowcase({ colorIdentityByDeckName, winTally, winTurns, + winTurnSums, gamesPlayed, totalSimulations, deckLinks, @@ -77,12 +79,12 @@ export const DeckShowcase = memo(function DeckShowcase({ const turns = winTurns?.[name] ?? []; stats[name] = { winPct: gamesPlayed > 0 ? ((wins / gamesPlayed) * 100).toFixed(0) : null, - avgTurn: turns.length > 0 ? (turns.reduce((acc, t) => acc + t, 0) / turns.length).toFixed(1) : null, + avgTurn: turns.length > 0 && winTurnSums ? (winTurnSums[name] / turns.length).toFixed(1) : null, }; } return { sorted: s, maxWins: m, deckStats: stats }; - }, [deckNames, winTally, winTurns, gamesPlayed]); + }, [deckNames, winTally, winTurns, winTurnSums, gamesPlayed]); return (
diff --git a/frontend/src/hooks/useWinData.ts b/frontend/src/hooks/useWinData.ts index f5e43529..f6de6fb6 100644 --- a/frontend/src/hooks/useWinData.ts +++ b/frontend/src/hooks/useWinData.ts @@ -7,6 +7,7 @@ import type { StructuredGame } from '@shared/types/log'; export interface WinData { winTally: Record | null; winTurns: Record | null; + winTurnSums: Record | null; gamesPlayed: number; simGamesCompleted: number; } @@ -18,6 +19,7 @@ export interface WinData { export interface SimWinResult { simWinTally: Record | null; simWinTurns: Record | null; + simWinTurnSums: Record | null; simGamesCompleted: number; } @@ -30,7 +32,7 @@ export function computeSimWins( deckNames: string[], ): SimWinResult { if (simulations.length === 0) { - return { simWinTally: null, simWinTurns: null, simGamesCompleted: 0 }; + return { simWinTally: null, simWinTurns: null, simWinTurnSums: null, simGamesCompleted: 0 }; } const completedSims = simulations.filter((s) => s.state === 'COMPLETED'); @@ -42,15 +44,17 @@ export function computeSimWins( const gamesCompleted = completedSims.reduce( (sum, s) => sum + (s.winners && s.winners.length > 0 ? s.winners.length : 1), 0, ) || completedSims.length; - return { simWinTally: null, simWinTurns: null, simGamesCompleted: gamesCompleted }; + return { simWinTally: null, simWinTurns: null, simWinTurnSums: null, simGamesCompleted: gamesCompleted }; } const tally: Record = {}; const turns: Record = {}; + const turnSums: Record = {}; for (const name of deckNames) { tally[name] = 0; turns[name] = []; + turnSums[name] = 0; } for (const sim of completedSims) { @@ -76,6 +80,7 @@ export function computeSimWins( if (i < simTurns.length && simTurns[i] !== undefined) { if (!turns[matchedDeck]) turns[matchedDeck] = []; turns[matchedDeck].push(simTurns[i]); + turnSums[matchedDeck] = (turnSums[matchedDeck] || 0) + simTurns[i]; } } } @@ -90,12 +95,13 @@ export function computeSimWins( (sum, s) => sum + (s.winners && s.winners.length > 0 ? s.winners.length : 1), 0, ) || completedSims.length; - return { simWinTally: tally, simWinTurns: turns, simGamesCompleted: gamesCompleted }; + return { simWinTally: tally, simWinTurns: turns, simWinTurnSums: turnSums, simGamesCompleted: gamesCompleted }; } export interface StructuredWinResult { structuredWinTally: Record | null; structuredWinTurns: Record | null; + structuredWinTurnSums: Record | null; } /** @@ -106,16 +112,18 @@ export function computeStructuredWins( logDeckNames: string[] | null, ): StructuredWinResult { if (!structuredGames || structuredGames.length === 0) { - return { structuredWinTally: null, structuredWinTurns: null }; + return { structuredWinTally: null, structuredWinTurns: null, structuredWinTurnSums: null }; } const tally: Record = {}; const turns: Record = {}; + const turnSums: Record = {}; if (logDeckNames) { for (const name of logDeckNames) { tally[name] = 0; turns[name] = []; + turnSums[name] = 0; } } @@ -131,6 +139,7 @@ export function computeStructuredWins( if (game.winningTurn !== undefined) { if (!turns[matchedDeck]) turns[matchedDeck] = []; turns[matchedDeck].push(game.winningTurn); + turnSums[matchedDeck] = (turnSums[matchedDeck] || 0) + game.winningTurn; } } } @@ -139,7 +148,7 @@ export function computeStructuredWins( turns[deck].sort((a, b) => a - b); } - return { structuredWinTally: tally, structuredWinTurns: turns }; + return { structuredWinTally: tally, structuredWinTurns: turns, structuredWinTurnSums: turnSums }; } /** @@ -151,8 +160,8 @@ export function resolveEffectiveWins( structuredResult: StructuredWinResult, structuredGames: StructuredGame[] | null, ): WinData { - const { structuredWinTally, structuredWinTurns } = structuredResult; - const { simWinTally, simWinTurns, simGamesCompleted } = simResult; + const { structuredWinTally, structuredWinTurns, structuredWinTurnSums } = structuredResult; + const { simWinTally, simWinTurns, simWinTurnSums, simGamesCompleted } = simResult; const winTally = jobResults?.wins ?? @@ -166,13 +175,17 @@ export function resolveEffectiveWins( structuredWinTally && Object.keys(structuredWinTally).length > 0 ? structuredWinTurns : simWinTurns; + const winTurnSums = + structuredWinTally && Object.keys(structuredWinTally).length > 0 + ? structuredWinTurnSums + : simWinTurnSums; const gamesPlayed = jobResults?.gamesPlayed ?? (structuredGames && structuredGames.length > 0 ? structuredGames.length : simGamesCompleted); - return { winTally, winTurns, gamesPlayed, simGamesCompleted }; + return { winTally, winTurns, winTurnSums, gamesPlayed, simGamesCompleted }; } // --------------------------------------------------------------------------- diff --git a/frontend/src/pages/JobStatus.test.tsx b/frontend/src/pages/JobStatus.test.tsx index 4c02eb4d..3d8e1595 100644 --- a/frontend/src/pages/JobStatus.test.tsx +++ b/frontend/src/pages/JobStatus.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { screen, waitFor, fireEvent } from '@testing-library/react'; +import { screen, waitFor, fireEvent, act } from '@testing-library/react'; import { renderWithRouter } from '../test/render'; import JobStatusPage from './JobStatus'; import { useJobStream } from '../hooks/useJobStream'; @@ -408,7 +408,9 @@ describe('JobStatus — Run Again', () => { }); expect(screen.getByText('Submitting...').closest('button')).toBeDisabled(); - resolveResponse({ ok: true, json: () => Promise.resolve({ id: 'job-new' }) } as unknown as Response); + await act(async () => { + resolveResponse({ ok: true, json: () => Promise.resolve({ id: 'job-new' }) } as unknown as Response); + }); }); it('shows error when resubmit fails', async () => { diff --git a/frontend/src/pages/JobStatus.tsx b/frontend/src/pages/JobStatus.tsx index 6e6d2891..f731de2e 100644 --- a/frontend/src/pages/JobStatus.tsx +++ b/frontend/src/pages/JobStatus.tsx @@ -61,7 +61,7 @@ export default function JobStatusPage() { // Data hooks const { job, simulations, error: streamError } = useJobStream(id); const logs = useJobLogs(id, job, { showLogPanel, loadStructured: loadStructuredLogs }); - const { winTally, winTurns, gamesPlayed, simGamesCompleted } = useWinData( + const { winTally, winTurns, winTurnSums, gamesPlayed, simGamesCompleted } = useWinData( job, simulations, logs.structuredGames, logs.deckNames, ); @@ -201,6 +201,7 @@ export default function JobStatusPage() { colorIdentityByDeckName={logs.colorIdentityByDeckName} winTally={winTally} winTurns={winTurns} + winTurnSums={winTurnSums} gamesPlayed={gamesPlayed} totalSimulations={job.simulations} deckLinks={job.deckLinks} From 7b2e686fcc19af885eacbb9105e33daaeeb19dac Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:40:10 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Replace=20O(N)=20array?= =?UTF-8?q?=20reduction=20with=20O(1)=20turn=20sum=20in=20DeckShowcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/DeckShowcase.test.tsx | 1 + frontend/src/hooks/useWinData.test.ts | 2 ++ frontend/src/pages/JobStatus.test.tsx | 1 + 3 files changed, 4 insertions(+) diff --git a/frontend/src/components/DeckShowcase.test.tsx b/frontend/src/components/DeckShowcase.test.tsx index c4a94e81..e1bafd96 100644 --- a/frontend/src/components/DeckShowcase.test.tsx +++ b/frontend/src/components/DeckShowcase.test.tsx @@ -11,6 +11,7 @@ function renderShowcase(overrides: Partial[0]> = colorIdentityByDeckName: {}, winTally: null, winTurns: null, + winTurnSums: null, gamesPlayed: 0, totalSimulations: 20, jobStatus: 'RUNNING', diff --git a/frontend/src/hooks/useWinData.test.ts b/frontend/src/hooks/useWinData.test.ts index 2d1b7f52..880b6763 100644 --- a/frontend/src/hooks/useWinData.test.ts +++ b/frontend/src/hooks/useWinData.test.ts @@ -184,11 +184,13 @@ describe('resolveEffectiveWins', () => { const noSims: ReturnType = { simWinTally: null, simWinTurns: null, + simWinTurnSums: null, simGamesCompleted: 0, }; const noStructured: ReturnType = { structuredWinTally: null, structuredWinTurns: null, + structuredWinTurnSums: null, }; it('prefers server results when present', () => { diff --git a/frontend/src/pages/JobStatus.test.tsx b/frontend/src/pages/JobStatus.test.tsx index 3d8e1595..15c33364 100644 --- a/frontend/src/pages/JobStatus.test.tsx +++ b/frontend/src/pages/JobStatus.test.tsx @@ -53,6 +53,7 @@ vi.mock('@tanstack/react-query', async () => { const defaultWinData = { winTally: null, winTurns: null, + winTurnSums: null, gamesPlayed: 0, simGamesCompleted: 0, };