Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
214 commits
Select commit Hold shift + click to select a range
4d18d6d
fix stale-session reprocessing
Korupt-virus Jul 29, 2026
ca7f597
fix: make analysis workspaces responsive
Korupt-virus Jul 30, 2026
ee7d8fa
fix: apply responsive policy app-wide
Korupt-virus Jul 30, 2026
699ad6f
fix: keep analysis track sizing CSS-owned
Korupt-virus Jul 30, 2026
e4e7ad7
docs: specify reusable UI components
Snazzie Jul 30, 2026
773d150
docs: design race result metadata
Snazzie Jul 30, 2026
1bdcea5
docs: plan race result metadata
Snazzie Jul 30, 2026
b11c348
docs: plan reusable UI component migration
Snazzie Jul 30, 2026
20bfdb5
feat: persist race result metadata
Snazzie Jul 30, 2026
17421a1
test: add reusable UI primitive stories
Snazzie Jul 30, 2026
55b23bb
feat: add persisted race result metadata
Snazzie Jul 30, 2026
be1cd64
feat: expose recent race result summaries
Snazzie Jul 30, 2026
766e48c
test: verify rendered button focus ring
Snazzie Jul 30, 2026
5cc4b59
refactor: standardize dialog shells
Snazzie Jul 30, 2026
014c2d6
fix(race-results): reconcile and classify outcomes
Snazzie Jul 30, 2026
9593f1c
fix: preserve dialog overlay default
Snazzie Jul 30, 2026
a4663ae
feat: add shared tabs primitive
Snazzie Jul 31, 2026
6600812
test: add badge primitive story coverage
Snazzie Jul 31, 2026
ff09607
refactor: standardize shared buttons and badges
Snazzie Jul 31, 2026
25b5390
refactor: standardize shared tables
Snazzie Jul 31, 2026
3f5ce8a
refactor: align shared overlay components
Snazzie Jul 31, 2026
a6aee06
refactor: adopt shared card shells
Snazzie Jul 31, 2026
3b90cad
chore: finish reusable UI component audit
Snazzie Jul 31, 2026
3b1d3ed
fix: restore JSX structure after UI migration
Snazzie Jul 31, 2026
1842b4b
chore: add local UI comparison workflow
Korupt-virus Jul 31, 2026
d91521c
fix: restore tune form JSX structure
Snazzie Jul 31, 2026
1692b65
refactor: standardize route controls
Snazzie Jul 31, 2026
90c3647
refactor: standardize analysis controls
Snazzie Jul 31, 2026
5998652
refactor: standardize tune controls
Snazzie Jul 31, 2026
11c741b
Revert "refactor: standardize tune controls"
Snazzie Jul 31, 2026
171ef13
Revert "refactor: standardize analysis controls"
Snazzie Jul 31, 2026
236b923
Revert "refactor: standardize route controls"
Snazzie Jul 31, 2026
3eb29e4
refactor: run local UI diff with Playwright
Korupt-virus Jul 31, 2026
4da2bd8
refactor: finish shared action migration
Snazzie Jul 31, 2026
e2974cc
fix: preserve comparison verdict in dialog
Snazzie Jul 31, 2026
7018b2f
style: format comparison dialog
Snazzie Jul 31, 2026
a02907c
docs(ui): define button component contract
Snazzie Jul 31, 2026
8dadbb6
docs(ui): plan button contract cleanup
Snazzie Jul 31, 2026
8c1585f
refactor(ui): centralize button defaults
Snazzie Jul 31, 2026
79c60dd
Merge branch 'main' into reusable-ui-components-rescue
Snazzie Jul 31, 2026
b057f1d
Add semantic telemetry catalog
Korupt-virus Jul 31, 2026
d24698a
Merge remote-tracking branch 'origin/reusable-ui-components-rescue' i…
Korupt-virus Jul 31, 2026
2e3f0fb
test(ui): expand seeded visual coverage
Korupt-virus Jul 31, 2026
69bd552
test(ui): stabilize Storybook visual approval
Korupt-virus Jul 31, 2026
e7250d1
fix(ui): contain Forza dashboard columns
Korupt-virus Jul 31, 2026
37a36f9
fix(workflow): add 'ready_for_review' type to pull request triggers
Korupt-virus Jul 31, 2026
7c850d3
docs: define component-owned styling
Snazzie Jul 31, 2026
a218c80
docs: plan component-owned styling migration
Snazzie Jul 31, 2026
86aff7b
feat(ui): add semantic component variants
Snazzie Jul 31, 2026
31e216c
fix(ui): complete semantic variant contracts
Snazzie Jul 31, 2026
653c1be
fix(storybook): assert portalled dialog content
Snazzie Jul 31, 2026
e4060bc
refactor(ui): remove control styling overrides
Snazzie Jul 31, 2026
b5c962b
fix(ui): preserve migrated control semantics
Snazzie Jul 31, 2026
14299ab
fix(ui): preserve analysis row spacing
Snazzie Jul 31, 2026
1da2b71
refactor(ui): centralize panel and overlay styling
Snazzie Jul 31, 2026
41a59c6
refactor(ui): remove TrackDetail panel overrides
Snazzie Jul 31, 2026
9bf0ace
refactor(ui): finish consumer styling audit
Snazzie Jul 31, 2026
f09ec40
refactor(ui): finish consumer styling audit
Snazzie Jul 31, 2026
140658b
refactor(ui): finish consumer styling audit
Snazzie Jul 31, 2026
397a437
refactor(ui): finish consumer styling audit
Snazzie Jul 31, 2026
e31ba40
test(ui): refresh dashboard snapshots
Snazzie Jul 31, 2026
6875680
fix(ui): resolve final styling audit findings
Snazzie Jul 31, 2026
6a223e6
.
Snazzie Aug 1, 2026
b37014e
docs: specify shared track laps table
Snazzie Aug 1, 2026
ae50653
fix: use shared table sizing for track laps
Snazzie Aug 1, 2026
564dc11
Responsive gate, map splitter, and UI tweaks
Snazzie Aug 1, 2026
5cf9287
refactor(ui): centralize shared component styling
Korupt-virus Aug 1, 2026
d23a4f2
Merge feat/issue-181-race-results
Korupt-virus Aug 1, 2026
211043d
Implement feature X to enhance user experience and optimize performance
Korupt-virus Aug 1, 2026
2b749f4
feat: add authoritative telemetry intelligence
Korupt-virus Aug 1, 2026
7bac698
feat: add executable telemetry mapping taxonomy
Korupt-virus Aug 1, 2026
639c473
Merge remote-tracking branch 'origin/fix/193-responsive-support-polic…
Korupt-virus Aug 1, 2026
deba712
feat: add migration v52 to repair legacy schema collisions and preven…
Korupt-virus Aug 1, 2026
52d8618
fix: align iRacing analysis car marker
Korupt-virus Aug 1, 2026
ca6b8e2
fix: calculate iRacing segment times from lap distance
Korupt-virus Aug 1, 2026
abd08bf
Merge remote-tracking branch 'origin/feat/issue-181-race-results' int…
Korupt-virus Aug 1, 2026
26972fb
Merge remote-tracking branch 'origin/main' into fix/193-responsive-su…
Korupt-virus Aug 1, 2026
71d6011
Merge remote-tracking branch 'origin/fix/193-responsive-support-polic…
Korupt-virus Aug 2, 2026
cdddf6c
fix: preserve complete iRacing SessionInfo YAML
Korupt-virus Aug 2, 2026
cef6ee5
fix: bound long-running resource usage
Korupt-virus Aug 2, 2026
efffbad
feat: enhance session handling and add new snapshot images
Korupt-virus Aug 2, 2026
f25580e
cleanup folders 1
Korupt-virus Aug 2, 2026
eeab837
docs: reorganize project documentation
Korupt-virus Aug 2, 2026
f29a6b4
refactor: reorganize server architecture
Korupt-virus Aug 2, 2026
acd0b01
refactor: organize server architecture by domain
Korupt-virus Aug 2, 2026
a27eece
refactor: consolidate server domains
Korupt-virus Aug 2, 2026
aa795f3
fix: stabilize Paraglide development output
Korupt-virus Aug 2, 2026
b45a665
fix: stabilize Analyse loading and navigation
Korupt-virus Aug 2, 2026
366886d
refactor: enhance sparkline to maintain lap IDs for repeated lap numbers
Korupt-virus Aug 3, 2026
bfdc2e1
fix: handle unexpected lap transitions and update lap detector ID for…
Korupt-virus Aug 3, 2026
43a02fa
fix: improve lap analysis insights and stabilize navigation in Analyse
Korupt-virus Aug 3, 2026
ee75ad9
fix: update fuel consumption reporting to show litres for iRacing and…
Korupt-virus Aug 3, 2026
f8eabe8
Shared folder refactor complete: domain subfolders, reusable leaves, …
Korupt-virus Aug 3, 2026
dd1edad
test: expand seeded end-to-end coverage
Korupt-virus Aug 3, 2026
e7f0cdd
refactor: reorganize shared modules by domain
Korupt-virus Aug 3, 2026
700f490
Merge conflicts resolved, incoming changes migrated to new shared fol…
Korupt-virus Aug 3, 2026
3cf9c9c
test: complete seeded e2e coverage audit
Korupt-virus Aug 4, 2026
7e5078c
Update lap SVG and telemetry test for ACC session on 2026-04-10
Korupt-virus Aug 4, 2026
c947a59
feat: enable preview and import of iRacing IBT recordings larger than…
Korupt-virus Aug 4, 2026
ef14841
feat: implement fastest lap tracking per sector and update iRacing se…
Korupt-virus Aug 4, 2026
63c92c8
fix: exclude pit transition laps from pace metrics
Korupt-virus Aug 4, 2026
83c7e9a
feat: enhance iRacing Compare maps to display lap position markers wi…
Korupt-virus Aug 4, 2026
d222d5e
feat: correct iRacing steering direction and update telemetry handlin…
Korupt-virus Aug 4, 2026
8a54aea
refactor tests into domain-focused suites
Korupt-virus Aug 4, 2026
f0075ba
fix: update test file references for data directory resolution
Korupt-virus Aug 4, 2026
58aa734
refactor: organize repository scripts
Korupt-virus Aug 4, 2026
bb7a2ae
feat(tests): add Playwright tests for seeded laps and tunes functiona…
Korupt-virus Aug 4, 2026
a16e213
refactor(client): organize feature modules
Korupt-virus Aug 4, 2026
5de41f7
refactor(docs): enhance documentation structure and content for clarity
Korupt-virus Aug 4, 2026
5e9450e
feat(docs): add comprehensive code review for folder cleanup branch
Korupt-virus Aug 4, 2026
98b37f9
fix: model telemetry freshness by clock domain
Korupt-virus Aug 5, 2026
8c67174
chore: migrate Biome config to 2.5.6
Korupt-virus Aug 5, 2026
8603306
ci: require Biome and root TypeScript checks
Korupt-virus Aug 5, 2026
8908ccf
Merge branch 'main' of https://github.com/SpeedHQ/RaceIQ into folder-…
Korupt-virus Aug 5, 2026
1c86252
Merge branch 'fix/173-reprocessing-recovery' into folder-cleanup
Korupt-virus Aug 5, 2026
0bb9c73
Merge remote-tracking branch 'origin/main' into folder-cleanup
Korupt-virus Aug 5, 2026
495d9f0
refactor: update import statements to use node: prefix and clean up code
Korupt-virus Aug 5, 2026
c8d9bf4
refactor: update pre-commit jobs and enhance typecheck script
Korupt-virus Aug 5, 2026
43ae0bf
chore: add internal section for self-contained local and CI verification
Korupt-virus Aug 5, 2026
b044a91
refactor: clean up CHANGELOG by removing outdated sections and organi…
Korupt-virus Aug 5, 2026
6091b8d
refactor: improve telemetry catalog check logic in build-test workflow
Korupt-virus Aug 5, 2026
2a9ba4d
refactor: enhance telemetry catalog validation and improve cleanup lo…
Korupt-virus Aug 5, 2026
33d510d
Fix limited to test/db/db-seed.test.ts: test-only inspection connecti…
Korupt-virus Aug 5, 2026
a275f07
Refactor test imports to use node built-in modules
Korupt-virus Aug 5, 2026
8e0ec81
Update telemetry catalog hash to reflect changes in the generated schema
Korupt-virus Aug 5, 2026
d76879e
Update telemetry catalog hash to reflect changes in version 0.13.0
Korupt-virus Aug 5, 2026
89943b6
fix: update key prop in GearRatioChart to use index for stable identity
Korupt-virus Aug 5, 2026
b9ed3d9
feat: add tsx-no-biome-ignore rule and refactor GearRatioChart and In…
Korupt-virus Aug 5, 2026
9ae049c
feat: add automated AI agent disclosure rule and refactor wireframe u…
Korupt-virus Aug 5, 2026
75b52d7
refactor: replace useRequiredGameId with gameIdForRoutePrefix for imp…
Korupt-virus Aug 5, 2026
970ce8f
feat(tests): add unit tests for setup source catalog and schema opera…
Korupt-virus Aug 5, 2026
c18eccd
fix: reconcile dependency versions
Korupt-virus Aug 5, 2026
39a81ff
fix: type generated localization modules
Korupt-virus Aug 5, 2026
6f341b6
docs: restore project README
Korupt-virus Aug 5, 2026
e7d490a
Merge branch 'main' of https://github.com/SpeedHQ/RaceIQ into folder-…
Korupt-virus Aug 6, 2026
a70e422
Add intelligence docs
Korupt-virus Aug 6, 2026
026c2a6
test: isolate database per run
Snazzie Aug 6, 2026
7fcce96
Merge branch 'folder-cleanup' of https://github.com/SpeedHQ/RaceIQ in…
Korupt-virus Aug 6, 2026
5a19116
fix(db): restore append-only migrations
Snazzie Aug 6, 2026
b470c87
fix(db): remove legacy telemetry migration
Snazzie Aug 6, 2026
6b40ee0
fix(db): renumber migrations after removal
Snazzie Aug 6, 2026
8881fd1
docs: protect historical migrations
Snazzie Aug 6, 2026
94099a2
fix(db): remove redundant convergence migration
Snazzie Aug 6, 2026
55ebafe
fix(db): remove legacy telemetry replay
Snazzie Aug 6, 2026
ad4d7f3
fix(update): show full release notes
Snazzie Aug 6, 2026
c7d96ac
refactor: clean up telemetry handling and remove legacy references
Korupt-virus Aug 6, 2026
847e3fa
docs: specify settings sidebar navigation
Snazzie Aug 6, 2026
ab95564
docs: specify cars race results removal
Snazzie Aug 6, 2026
ef25d74
fix: remove race results card from cars page
Snazzie Aug 6, 2026
57188fd
feat: make settings navigation responsive
Snazzie Aug 6, 2026
43d24c0
feat: widen desktop settings dialog
Snazzie Aug 6, 2026
24ae982
fix: align settings content and tabs
Snazzie Aug 6, 2026
ae4aea6
docs: specify AI panel parity restoration
Snazzie Aug 6, 2026
4f4b6cb
docs: plan AI panel parity restoration
Snazzie Aug 6, 2026
d1e47aa
fix: restore compare AI analysis lifecycle
Snazzie Aug 6, 2026
5d70108
Update TrackDetailRoute.tsx
Snazzie Aug 6, 2026
0e4c66b
fix(ui): remove driver result breakdown
Snazzie Aug 6, 2026
bcef555
fix: update AI setup button text and adjust artifacts directory path
Korupt-virus Aug 6, 2026
93a35b5
test: assert sector counts per game
Snazzie Aug 6, 2026
ce78627
fix(3d): darken translucent wheel cards
Snazzie Aug 6, 2026
0ee2736
fix: restore compare panel borders
Snazzie Aug 6, 2026
2ce3fe4
fix: refine insight panel spacing
Snazzie Aug 6, 2026
1928b8a
fix(analyse): add insight card spacing
Snazzie Aug 6, 2026
9514be6
fix(ui): close searchable dropdowns after selection
Korupt-virus Aug 6, 2026
8f016b0
fix(body-attitude): show vehicle roll in the correct direction on the…
Korupt-virus Aug 6, 2026
7fb9241
refactor(chats): use reusable table
Snazzie Aug 6, 2026
0a33a75
fix(experiments): align modal controls
Snazzie Aug 6, 2026
7d9fe8c
fix(experiments): wrap focus card text
Snazzie Aug 6, 2026
6b31c86
chore: preserve settings dialog work
Snazzie Aug 6, 2026
b78f454
fix(experiments): render v1 in StrictMode
Snazzie Aug 6, 2026
2d5f679
fix(ai): pin chat dependency versions
Snazzie Aug 6, 2026
7f40dde
fix(ui): add experiment panel spacing
Snazzie Aug 6, 2026
ab2c8db
fix(results): restore startup backfill
Snazzie Aug 6, 2026
f12b784
docs: define reconciliation version tests
Snazzie Aug 6, 2026
cec52c3
docs: plan reconciliation version coverage
Snazzie Aug 6, 2026
e82a17a
test: tighten stale session coverage
Snazzie Aug 6, 2026
8c43eb1
fix: label unknown pit service as Pit
Snazzie Aug 6, 2026
5b8f9ae
fix(analyse): match compare chat background
Snazzie Aug 6, 2026
e76dfa2
fix: include resultless sessions in stale queries
Snazzie Aug 6, 2026
4f1c623
test: cover resultless stale reconciliation
Snazzie Aug 6, 2026
387cbaa
docs: specify experiment version deletion UI
Snazzie Aug 6, 2026
d0bec69
docs: plan experiment version deletion UI
Snazzie Aug 6, 2026
71db1cd
test: cover startup stale notifications
Snazzie Aug 6, 2026
c9f67b1
fix: restore detector notification version list
Snazzie Aug 6, 2026
5bebb59
test: cover all active detector IDs
Snazzie Aug 6, 2026
8a79f9a
test: cover all current detector fixtures
Snazzie Aug 6, 2026
5a521ef
test: type stale session result fixtures
Snazzie Aug 6, 2026
f62108e
feat(experiments): restore version deletion UI
Snazzie Aug 6, 2026
8568222
fix(experiments): keep trash available on empty graph
Snazzie Aug 6, 2026
8ae2dac
fix(experiments): announce version action errors
Snazzie Aug 6, 2026
288a526
docs(experiments): record deletion UI accessibility fix
Snazzie Aug 6, 2026
f5f10ee
fix(experiments): identify trash restore actions
Snazzie Aug 6, 2026
fd1c672
docs(experiments): record restore label fix
Snazzie Aug 6, 2026
d9da774
fix(experiments): repair restore button markup
Snazzie Aug 6, 2026
2e198c9
docs(experiments): record restore markup repair
Snazzie Aug 6, 2026
d2ba5ac
fix: suppress seed profile notifications
Snazzie Aug 6, 2026
f3c5711
docs: specify telemetry catalog type safety
Snazzie Aug 6, 2026
c12f284
feat: type telemetry catalog references
Snazzie Aug 6, 2026
a2478a3
fix: refine responsive settings cards
Snazzie Aug 6, 2026
c5059c7
fix(ai): remove Gemini defaults
Snazzie Aug 7, 2026
6977d2e
feat(experiments): add search and sorting
Snazzie Aug 7, 2026
7ee7495
fix(iracing): sort cars by actual name
Snazzie Aug 7, 2026
1213cde
fix(live): restore demo playback
Snazzie Aug 7, 2026
b544bd2
fix(sessions): label imported tab MoTeC
Snazzie Aug 7, 2026
d002d95
docs: specify lap ownership design
Snazzie Aug 7, 2026
96eee23
docs: plan lap ownership implementation
Snazzie Aug 7, 2026
2b5e480
feat(driver): configure profile output budget
Snazzie Aug 7, 2026
792df4f
test: preserve exports in startup mocks
Snazzie Aug 7, 2026
22efeb6
fix(types): accept game-specific tune blobs
Snazzie Aug 7, 2026
6853c64
fix(test): stabilize full suite
Snazzie Aug 7, 2026
17b89af
fix(compare): stretch resize separator
Snazzie Aug 7, 2026
79d920f
feat(analyse): add explicit review routes
Snazzie Aug 8, 2026
b56eced
merge main into session analysis routes
Snazzie Aug 13, 2026
7613450
fix review routes for main APIs
Snazzie Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .omp/rules/tsx-no-biome-ignore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: tsx-no-biome-ignore
description: "Reject noArrayIndexKey suppressions in affected React components"
condition: ["GearRatioChart\\.tsx[\\s\\S]*biome-ignore\\s+lint/suspicious/noArrayIndexKey", "InputOverlay\\.tsx[\\s\\S]*biome-ignore\\s+lint/suspicious/noArrayIndexKey"]
scope: "tool"
---

Do not suppress `noArrayIndexKey`. Fix identity at source with stable domain IDs or stable source-data keys. If no compliant identity exists, stop before editing: cite Biome’s `noArrayIndexKey` reference, explain concrete tradeoffs, and ask which model change is acceptable.
5 changes: 4 additions & 1 deletion client/src/components/ChatsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface LapSummary {
isValid: boolean;
carName: string;
trackName: string;
trackOrdinal: number | null;
carOrdinal: number | null;
gameId: string;
}

Expand Down Expand Up @@ -102,9 +104,10 @@ export function ChatsPage() {
const prefix = `/${game.routePrefix}`;
if (row.type === "analyse" && row.laps[0]) {
const lap = row.laps[0];
if (lap.trackOrdinal == null || lap.carOrdinal == null) return;
navigate({
to: `${prefix}/analyse` as never,
search: { lap: lap.id, ai: 1 } as never,
search: { track: lap.trackOrdinal, car: lap.carOrdinal, lap: lap.id, ai: 1 } as never,
});
} else if (row.type === "compare" && row.laps.length === 2) {
const [a, b] = row.laps;
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/RecordedLaps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ export function RecordedLaps({ laps, trackOrdinal, maxLaps = 15 }: RecordedLapsP
<span className={`text-base font-mono font-bold tabular-nums text-right ${timeColor}`}>{formatLapTime(l.lapTime)}</span>
<span className="text-xs text-app-text-dim font-mono tabular-nums text-right w-14">{isBest ? "PB" : `+${delta.toFixed(3)}`}</span>
<div className="flex items-center gap-1 w-16 justify-end">
<Button // eslint-disable-next-line @typescript-eslint/no-explicit-any
onClick={() => navigate({ to: `${gameRoute}/analyse` as any, search: { track: l.trackOrdinal, car: l.carOrdinal, lap: l.id } as any })}
<Button
disabled={l.trackOrdinal == null || l.carOrdinal == null}
title={l.trackOrdinal == null || l.carOrdinal == null ? "Analyse unavailable: track/car identity unresolved" : undefined}
onClick={() => void navigate({ to: `${gameRoute}/analyse` as never, search: { track: l.trackOrdinal, car: l.carOrdinal, lap: l.id } as never })}
variant="app-primary"
size="app-sm"
className="!px-1.5 !py-0.5"
Expand Down
12 changes: 9 additions & 3 deletions client/src/components/dev/ImportDumpPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function ImportDumpPanel() {
const navigate = useNavigate();

const openInAnalyse = (lap: ImportedLap) => {
if (!result) return;
if (!result || lap.trackOrdinal <= 0 || lap.carOrdinal <= 0) return;
// Route is /{routePrefix}/analyse with ?track&car&lap search params
navigate({
to: `/${result.routePrefix}/analyse`,
Expand Down Expand Up @@ -185,8 +185,14 @@ export function ImportDumpPanel() {
<span className="text-app-text-muted">#{lap.lapNumber}</span> <span>{formatLapTime(lap.lapTime)}</span>
{!lap.isValid && <span className="ml-2 px-1.5 py-0.5 rounded bg-status-danger/15 text-status-danger text-app-caption">invalid</span>}
</div>
<Button type="button" onClick={() => openInAnalyse(lap)} className="px-2.5 py-1 text-xs rounded bg-app-accent text-app-on-filled hover:opacity-90 transition-opacity">
{m.dev_open_analyse()}
<Button
type="button"
disabled={lap.trackOrdinal <= 0 || lap.carOrdinal <= 0}
title={lap.trackOrdinal <= 0 || lap.carOrdinal <= 0 ? "Analyse unavailable: track/car identity unresolved" : undefined}
onClick={() => openInAnalyse(lap)}
className="px-2.5 py-1 text-xs rounded bg-app-accent text-app-on-filled hover:opacity-90 transition-opacity"
>
{lap.trackOrdinal <= 0 || lap.carOrdinal <= 0 ? "Identity unavailable" : m.dev_open_analyse()}
</Button>
</div>
))}
Expand Down
59 changes: 59 additions & 0 deletions client/src/components/tunes/review/AnalysePickerPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { GameId } from "@shared/games/ids";
import { useNavigate } from "@tanstack/react-router";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { useResolveNames } from "@/hooks/catalog-queries";
import { useLaps } from "@/hooks/laps";

export function AnalysePickerPage({ gameId }: { gameId: GameId }) {
const navigate = useNavigate();
const { data: laps = [], isLoading } = useLaps();
const trackOrdinals = useMemo(() => [...new Set(laps.map((lap) => lap.trackOrdinal).filter((ordinal): ordinal is number => ordinal != null && ordinal > 0))].sort((a, b) => a - b), [laps]);
const carOrdinals = useMemo(() => [...new Set(laps.map((lap) => lap.carOrdinal).filter((ordinal): ordinal is number => ordinal != null && ordinal > 0))].sort((a, b) => a - b), [laps]);
const { data: names, isLoading: namesLoading } = useResolveNames(trackOrdinals, carOrdinals);
const trackName = (ordinal: number) => names?.trackNames[String(ordinal)] ?? `Track ${ordinal}`;
const carName = (ordinal: number) => names?.carNames[String(ordinal)] ?? `Car ${ordinal}`;

if (isLoading || namesLoading) {
return <div role="status" className="p-8 text-sm text-app-text-muted">Loading Analyse selections…</div>;
}

return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6 p-6">
<div>
<h1 className="text-xl font-semibold text-app-text">Analyse</h1>
<p className="mt-1 text-sm text-app-text-muted">Choose track and car to review recorded laps for {gameId}.</p>
</div>
{trackOrdinals.length === 0 ? (
<div className="rounded-lg border border-app-border p-6 text-sm text-app-text-muted">No recorded laps available for this game.</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{trackOrdinals.map((track) => {
const cars = carOrdinals.filter((car) => laps.some((lap) => lap.trackOrdinal === track && lap.carOrdinal === car));
return (
<div key={track} className="rounded-lg border border-app-border p-4">
<div className="font-semibold text-app-text">{trackName(track)}</div>
<div className="mt-3 flex flex-wrap gap-2">
{cars.map((car) => {
const count = laps.filter((lap) => lap.trackOrdinal === track && lap.carOrdinal === car && lap.isValid && lap.lapTime > 0).length;
return (
<Button
key={car}
variant="app-outline"
size="app-sm"
aria-label={`Review ${carName(car)} on ${trackName(track)} (${count} valid laps)`}
onClick={() => void navigate({ search: (previous: Record<string, unknown>) => ({ ...previous, track, car, lap: undefined, laps: undefined }) } as never)}
>
{carName(car)} ({count})
</Button>
);
})}
</div>
</div>
);
})}
</div>
)}
</div>
);
}
53 changes: 53 additions & 0 deletions client/src/components/tunes/review/AnalyseRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { GameId } from "@shared/games/ids";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { parseAnalyseLapIds, type AnalyseSearch } from "@/lib/game-routes";
import { AnalysePickerPage } from "./AnalysePickerPage";
import { TrackCarAnalyseReviewPage } from "./TrackCarAnalyseReviewPage";
import { LapAnalyse } from "../../analyse/LapAnalyse";

function InvalidAnalyseSelection({ message }: { message: string }) {
const navigate = useNavigate();
return (
<div className="flex min-h-[18rem] items-center justify-center p-8 text-center">
<div>
<h1 className="text-lg font-semibold text-app-text">Invalid Analyse selection</h1>
<p role="alert" className="mt-2 text-sm text-app-text-muted">{message}</p>
<button
type="button"
className="mt-4 rounded border border-app-border px-3 py-1.5 text-sm text-app-text hover:bg-app-surface-alt"
onClick={() => void navigate({ search: (previous: Record<string, unknown>) => ({ ...previous, track: undefined, car: undefined, lap: undefined, laps: undefined }) } as never)}
>
Back to Analyse picker
</button>
</div>
</div>
);
}

export function AnalyseRoute({ gameId }: { gameId: GameId }) {
const search = useSearch({ strict: false }) as AnalyseSearch;
const hasTrack = search.track != null;
const hasCar = search.car != null;
const hasLap = search.lap != null;
const hasComparison = search.laps != null;
const comparisonLapIds = parseAnalyseLapIds(search.laps);
const validTrack = !hasTrack || (Number.isInteger(search.track!) && search.track! > 0);
const validCar = !hasCar || (Number.isInteger(search.car!) && search.car! > 0);
const validLap = !hasLap || (Number.isInteger(search.lap!) && search.lap! > 0);

if (!validTrack || !validCar || !validLap) {
return <InvalidAnalyseSelection message="Track, car, and lap must be positive numeric selections." />;
}
if (hasComparison && (comparisonLapIds == null || comparisonLapIds.length === 0)) {
return <InvalidAnalyseSelection message="Comparison laps must be a comma-separated list of positive, unique lap IDs." />;
}
if (hasTrack !== hasCar) {
return <InvalidAnalyseSelection message="Choose both track and car before selecting laps." />;
}
if ((hasLap || hasComparison) && (!hasTrack || !hasCar)) {
return <InvalidAnalyseSelection message="A lap selection must include its track and car." />;
}
if (!hasTrack && !hasCar) return <AnalysePickerPage gameId={gameId} />;
if (hasLap) return <LapAnalyse />;
return <TrackCarAnalyseReviewPage gameId={gameId} trackOrdinal={search.track!} carOrdinal={search.car!} />;
}
9 changes: 5 additions & 4 deletions client/src/components/tunes/review/TestReviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export function TestReviewPage({ gameId, experimentId, lapIds, versionId }: { ga
);
}

if (gameId !== "acc" && gameId !== "ac-evo") {
return <div role="alert" className="p-8 text-sm text-app-text-muted">Review is unavailable for this game.</div>;
}

return (
// Single page scroll: the app shell's outlet wrapper is the only scroll
// container. This page just flows — the dashboard grows to its content and
Expand All @@ -81,11 +85,8 @@ export function TestReviewPage({ gameId, experimentId, lapIds, versionId }: { ga
`items-start` lets the sticky chat column pin instead of stretching. */}
<div className="grid grid-cols-1 items-start gap-3 @5xl/workspace:grid-cols-[1fr_360px]">
<div className="border border-app-border rounded-lg">
{/* TuneReviewDashboard's gameId union is ACC/AC-Evo (setup-engineer
panels); F1 rides the ACC path — it never reaches ACC-specific
setup data, and the sector/tyre analysis is game-agnostic. */}
<TuneReviewDashboard
gameId={gameId === "f1-2025" ? "acc" : gameId}
gameId={gameId}
laps={laps}
trackName={session?.trackName ?? undefined}
onBack={backToWorkspace}
Expand Down
54 changes: 54 additions & 0 deletions client/src/components/tunes/review/TrackCarAnalyseReviewPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { GameId } from "@shared/games/ids";
import { selectEvaluationLaps } from "@shared/racing/laps/review-selection";
import { parseAnalyseLapIds } from "@/lib/game-routes";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { useEffect, useMemo } from "react";
import { useCarName, useResolveNames } from "@/hooks/catalog-queries";
import { useLaps } from "@/hooks/laps";
import { useTrackName } from "@/hooks/track-queries";
import { TuneReviewDashboard } from "./TuneReviewDashboard";

export function TrackCarAnalyseReviewPage({ gameId, trackOrdinal, carOrdinal }: { gameId: GameId; trackOrdinal: number; carOrdinal: number }) {
const navigate = useNavigate();
const search = useSearch({ strict: false }) as { laps?: string };
const { data: laps = [], isLoading: lapsLoading } = useLaps();
const { data: trackName, isLoading: trackLoading } = useTrackName(trackOrdinal);
const { data: resolvedNames, isLoading: namesLoading } = useResolveNames([trackOrdinal], [carOrdinal]);
const { data: carName, isLoading: carLoading } = useCarName(carOrdinal);
const groupLaps = useMemo(() => laps.filter((lap) => lap.trackOrdinal === trackOrdinal && lap.carOrdinal === carOrdinal), [carOrdinal, laps, trackOrdinal]);
const requestedLapIds = parseAnalyseLapIds(search.laps);
const comparisonCandidates = useMemo(() => (requestedLapIds ? groupLaps.filter((lap) => requestedLapIds.includes(lap.id)) : groupLaps), [groupLaps, requestedLapIds]);
const evaluationIds = useMemo(() => selectEvaluationLaps(comparisonCandidates).chosen.map((lap) => lap.id), [comparisonCandidates]);
const canonicalLaps = evaluationIds.join(",");
const resolvedTrackName = trackName ?? resolvedNames?.trackNames[String(trackOrdinal)] ?? `Track ${trackOrdinal}`;
const resolvedCarName = carName ?? resolvedNames?.carNames[String(carOrdinal)] ?? `Car ${carOrdinal}`;
const backToPicker = () => void navigate({ search: (previous: Record<string, unknown>) => ({ ...previous, track: undefined, car: undefined, lap: undefined, laps: undefined }) } as never);

useEffect(() => {
if (lapsLoading || trackLoading || carLoading || namesLoading || groupLaps.length === 0) return;
if (search.laps === canonicalLaps) return;
void navigate({ replace: true, search: (previous: Record<string, unknown>) => ({ ...previous, track: trackOrdinal, car: carOrdinal, lap: undefined, laps: canonicalLaps }) } as never);
}, [carLoading, carOrdinal, canonicalLaps, groupLaps.length, lapsLoading, namesLoading, navigate, search.laps, trackLoading, trackOrdinal]);

if (lapsLoading || trackLoading || carLoading || namesLoading) {
return <div role="status" aria-live="polite" className="p-8 text-sm text-app-text-muted">Loading Analyse review…</div>;
}
if (gameId !== "acc" && gameId !== "ac-evo") {
return <div role="alert" className="p-8 text-sm text-app-text-muted">Analyse review is unavailable for this game.</div>;
}


return (
<div className="flex flex-col gap-3 p-3">
<div className="px-1 text-sm text-app-text-muted">{resolvedTrackName} · {resolvedCarName}</div>
<div className="rounded-lg border border-app-border">
<TuneReviewDashboard
gameId={gameId}
laps={groupLaps}
trackName={resolvedTrackName}
onBack={backToPicker}
/>
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions client/src/lib/game-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ export type AnalyseSearch = {
track?: number;
car?: number;
lap?: number;
laps?: string;
cursor?: number;
viz?: string;
ai?: number;
view?: string;
};
export type CompareSearch = {
track?: number;
Expand Down Expand Up @@ -78,14 +80,27 @@ export function parseOptionalNumber(value: unknown): number | undefined {
return Number.isFinite(parsed) ? parsed : undefined;
}

/** Parse the canonical comparison lap list without silently accepting malformed IDs. */
export function parseAnalyseLapIds(value: string | undefined): number[] | null | undefined {
if (value == null) return undefined;
const parts = value.split(",");
if (parts.length === 0 || parts.some((part) => part.trim() === "")) return null;
const ids = parts.map((part) => Number(part));
if (ids.some((id) => !Number.isInteger(id) || id <= 0) || new Set(ids).size !== ids.length) return null;
return ids;
}


export function validateAnalyseSearch(search: Record<string, unknown>): AnalyseSearch {
return {
track: parseOptionalNumber(search.track),
car: parseOptionalNumber(search.car),
lap: parseOptionalNumber(search.lap),
laps: typeof search.laps === "string" ? search.laps : undefined,
cursor: parseOptionalNumber(search.cursor),
viz: typeof search.viz === "string" ? search.viz : undefined,
ai: parseOptionalNumber(search.ai),
view: typeof search.view === "string" ? search.view : undefined,
};
}

Expand Down
Loading
Loading