Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 38 additions & 10 deletions src/frontend/components/Settings/sections/StandingsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ const sortableSettings: SortableSetting[] = [
},
{ id: 'driverTag', label: 'Driver Tag', configKey: 'driverTag' },
{ id: 'badge', label: 'Driver Badge', configKey: 'badge' },
{ id: 'iratingChange', label: 'iRating Change', configKey: 'iratingChange' },
{
id: 'iratingChange',
label: 'iRating Change',
configKey: 'iratingChange',
hasSubSetting: true,
},
{
id: 'positionChange',
label: 'Position Change',
Expand Down Expand Up @@ -144,6 +149,28 @@ const DisplaySettingsList = ({
}}
sortableProps={sortableProps}
>
{setting.hasSubSetting &&
setting.configKey === 'iratingChange' &&
settings.config.iratingChange.enabled && (
<div className="flex items-center justify-between gap-3 pl-8 mt-2 indent-8">
<span className="text-sm text-slate-300">
Estimate During Practice
</span>
<ToggleSwitch
enabled={
settings.config.iratingChange.estimateInPractice ?? false
}
onToggle={(enabled) =>
handleConfigChange({
iratingChange: {
...settings.config.iratingChange,
estimateInPractice: enabled,
},
})
}
/>
Comment on lines +155 to +171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Give the switch an accessible name.

ToggleSwitch renders an unnamed role="switch" button. The sibling span does not label that button. Associate the text with the control through aria-labelledby, or extend ToggleSwitch with an accessible-label prop and pass Estimate During Practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/Settings/sections/StandingsSettings.tsx` around lines
155 - 171, Give the ToggleSwitch in the “Estimate During Practice” setting an
accessible name by associating it with the visible label text via
aria-labelledby, or by passing an accessible-label prop if supported. Update
ToggleSwitch as needed so its rendered switch button receives that name while
preserving the existing toggle behavior.

</div>
)}
{setting.hasSubSetting &&
setting.configKey === 'lapTimeDeltas' &&
settings.config.lapTimeDeltas.enabled && (
Expand Down Expand Up @@ -287,8 +314,7 @@ const DisplaySettingsList = ({
[setting.configKey]: {
...cv,
pitLapDisplayMode: e.target.value as
| 'lastPitLap'
| 'lapsSinceLastPit',
'lastPitLap' | 'lapsSinceLastPit',
},
});
}}
Expand Down Expand Up @@ -1092,24 +1118,26 @@ export const StandingsSettings = () => {
manufacturerStats: {
enabled: newValue,
cap:
settings.config.classHeaderStyle?.manufacturerStats
?.cap ?? 5,
settings.config.classHeaderStyle
?.manufacturerStats?.cap ?? 5,
showPlayerManufacturer:
settings.config.classHeaderStyle?.manufacturerStats
?.showPlayerManufacturer ?? false,
settings.config.classHeaderStyle
?.manufacturerStats?.showPlayerManufacturer ??
false,
},
},
})
}
/>
{(settings.config.classHeaderStyle?.manufacturerStats
?.enabled ?? false) && (
?.enabled ??
false) && (
<>
<SettingSelectRow
title="Max manufacturers to show"
value={
settings.config.classHeaderStyle?.manufacturerStats
?.cap?.toString() ?? 'all'
settings.config.classHeaderStyle?.manufacturerStats?.cap?.toString() ??
'all'
}
options={[
...Array.from({ length: 10 }, (_, i) => ({
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/components/Settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface SessionVisibilitySettings {

export interface StandingsWidgetSettings extends BaseWidgetSettings {
config: {
iratingChange: { enabled: boolean };
iratingChange: { enabled: boolean; estimateInPractice?: boolean };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Version the changed standings settings schema

This adds a persisted field to the Standings settings shape, but StandingsWidgetSettings still has no version field. Architecture rule R8.1 explicitly requires every changed settings shape to carry a version; without one, this schema cannot participate in the required versioned migration path when later changes become breaking.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

positionChange: { enabled: boolean };
badge: {
enabled: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
import { describe, it, expect } from 'vitest';
import { calculateLapDeltas } from './useDriverStandings';
import {
calculateLapDeltas,
shouldCalculateIRatingChange,
} from './useDriverStandings';

describe('shouldCalculateIRatingChange', () => {
it('calculates changes for official race weekends', () => {
expect(shouldCalculateIRatingChange('Race', true, 'Race', false)).toBe(
true
);
});

it('calculates hypothetical changes in practice when enabled', () => {
expect(
shouldCalculateIRatingChange('Practice', false, 'Practice', true)
).toBe(true);
});

it('does not calculate hypothetical changes in practice by default', () => {
expect(
shouldCalculateIRatingChange('Practice', false, 'Practice', false)
).toBe(false);
});

it('does not calculate hypothetical changes during offline testing', () => {
expect(shouldCalculateIRatingChange('Test', false, 'Practice', true)).toBe(
false
);
});

it('does not extend estimates to qualifying sessions', () => {
expect(
shouldCalculateIRatingChange('Practice', false, 'Open Qualify', true)
).toBe(false);
});
});

describe('calculateLapDeltas', () => {
it('should return undefined when disabled', () => {
Expand Down
27 changes: 22 additions & 5 deletions src/frontend/components/Standings/hooks/useDriverStandings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ const EMPTY_NUMBERS: number[] = [];
const EMPTY_BOOLEANS: boolean[] = [];
const EMPTY_TRACK_LOCATIONS: TrackLocation[] = [];

export const shouldCalculateIRatingChange = (
eventType: string | undefined,
isOfficial: boolean,
sessionType: string | undefined,
estimateInPractice: boolean
) =>
(eventType === 'Race' && isOfficial) ||
(eventType === 'Practice' &&
sessionType === 'Practice' &&
estimateInPractice);
Comment on lines +36 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude qualifying sessions during official race weekends.

Line 42 returns true for ('Race', true, 'Open Qualify', false). This enables hypothetical iRating changes during qualifying. Require sessionType === 'Race' for the official-race branch.

  • src/frontend/components/Standings/hooks/useDriverStandings.tsx#L36-L45: require a race session in the official-race eligibility branch.
  • src/frontend/components/Standings/hooks/useDriverStandings.spec.ts#L7-L37: add an assertion that ('Race', true, 'Open Qualify', true) returns false.
📍 Affects 2 files
  • src/frontend/components/Standings/hooks/useDriverStandings.tsx#L36-L45 (this comment)
  • src/frontend/components/Standings/hooks/useDriverStandings.spec.ts#L7-L37
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/components/Standings/hooks/useDriverStandings.tsx` around lines
36 - 45, The official-race branch of shouldCalculateIRatingChange must require
sessionType === 'Race' so qualifying sessions return false; update
src/frontend/components/Standings/hooks/useDriverStandings.tsx lines 36-45
accordingly. Add an assertion in
src/frontend/components/Standings/hooks/useDriverStandings.spec.ts lines 7-37
verifying ('Race', true, 'Open Qualify', true) returns false.


export const useDriverStandings = (
settings?: StandingsWidgetSettings['config']
) => {
Expand Down Expand Up @@ -173,11 +184,16 @@ export const useDriverStandings = (
? augmentStandingsWithPositionChange(groupedByClass, qualifyingResults)
: groupedByClass;

// Calculate iRating changes for official race weekends
const iratingAugmentedGroupedByClass =
eventType === 'Race' && isOfficial
? augmentStandingsWithIRating(positionChangeAugmentedGroupedByClass)
: positionChangeAugmentedGroupedByClass;
// Official race weekends retain the existing behavior. Practice estimates
// are opt-in because they are hypothetical and do not affect iRating.
const iratingAugmentedGroupedByClass = shouldCalculateIRatingChange(
eventType,
isOfficial,
sessionType,
settings?.iratingChange?.estimateInPractice ?? false
Comment on lines +189 to +193

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Instrument the new practice iRating hot path

When practice estimation is enabled, telemetry-driven standings recomputations now invoke augmentStandingsWithIRating, whose calculation allocates an N-by-N chance matrix, but this newly enabled high-frequency path has no perfMetrics.measure wrapper. Architecture rule R13.1 requires that instrumentation so regressions from large practice fields appear in the performance overlay.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

)
? augmentStandingsWithIRating(positionChangeAugmentedGroupedByClass)
: positionChangeAugmentedGroupedByClass;

// Calculate gap to class leader when enabled OR when interval is enabled (interval needs gap data)
const gapAugmentedGroupedByClass =
Expand Down Expand Up @@ -226,6 +242,7 @@ export const useDriverStandings = (
useLivePositionStandings,
isOfficial,
eventType,
settings?.iratingChange?.estimateInPractice,
gapEnabled,
intervalEnabled,
carIdxLap,
Expand Down
1 change: 1 addition & 0 deletions src/types/defaultDashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ describe('getWidgetDefaultConfig', () => {
expect(config).toBeDefined();
expect(config.background).toBeDefined();
expect(config.displayOrder).toBeDefined();
expect(config.iratingChange.estimateInPractice).toBe(false);
});

it('returns the fuel config', () => {
Expand Down
1 change: 1 addition & 0 deletions src/types/defaultDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const defaultDashboard: {
useLivePosition: false,
iratingChange: {
enabled: true,
estimateInPractice: false,
},
positionChange: {
enabled: false,
Expand Down
2 changes: 1 addition & 1 deletion src/types/widgetConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export type RelativeBadgeFormat =
// ===========================

export interface StandingsConfig {
iratingChange: { enabled: boolean };
iratingChange: { enabled: boolean; estimateInPractice?: boolean };
positionChange: { enabled: boolean };
badge: { enabled: boolean; badgeFormat: StandingsBadgeFormat };
delta: { enabled: boolean };
Expand Down