Add end-of-practice summary to the driver's mobile profile
When a driver's personal 5-minute practice timer finishes, a "Pass avslutat" card now appears immediately: how this pass's pace and consistency compared to their recent sessions, any goal or lap-count milestone crossed during the pass with its reward text made prominent, and one suggested next goal (reusing the existing pace recommendation, falling back to the next milestone when there isn't enough data yet). Scoped to the driver's current class, and explicit about insufficient sample sizes instead of showing a misleading number. Adds a per-session consistency calculation to buildPublicTrainingWindowStats server-side; everything else needed (stats, goals, recommendation, milestone) was already sent to the client, so the summary itself is computed there from data already in hand - no raw laps or driver IDs added to what the public endpoint exposes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -212,7 +212,11 @@ function buildPublicTrainingWindowStats(laps) {
|
||||
return {
|
||||
laps: laps.length,
|
||||
bestLapMs: lapTimes.length ? Math.min(...lapTimes) : 0,
|
||||
averageLapMs: lapTimes.length ? Math.round(lapTimes.reduce((sum, lapMs) => sum + lapMs, 0) / lapTimes.length) : 0,
|
||||
averageLapMs: publicTrainingAverage(lapTimes),
|
||||
// A spread needs a handful of laps to mean anything - fewer than that, report it as
|
||||
// unavailable (0 alongside consistencySampleReady:false) rather than a misleading number.
|
||||
consistencySpreadPct: lapTimes.length >= 5 ? publicTrainingSpread(lapTimes) : 0,
|
||||
consistencySampleReady: lapTimes.length >= 5,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -430,22 +434,29 @@ function buildPublicTrainingPaceBuckets(laps, markers, bucketMinutes = 30) {
|
||||
});
|
||||
}
|
||||
|
||||
function publicTrainingAverage(values) {
|
||||
return values.length ? Math.round(values.reduce((sum, value) => sum + value, 0) / values.length) : 0;
|
||||
}
|
||||
|
||||
function publicTrainingPercentile(values, ratio) {
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
return sorted.length
|
||||
? sorted[Math.min(sorted.length - 1, Math.max(0, Math.round((sorted.length - 1) * ratio)))]
|
||||
: 0;
|
||||
}
|
||||
|
||||
function publicTrainingSpread(values) {
|
||||
const mean = publicTrainingAverage(values);
|
||||
return mean ? ((publicTrainingPercentile(values, 0.9) - publicTrainingPercentile(values, 0.1)) / mean) * 100 : 0;
|
||||
}
|
||||
|
||||
function buildPublicTrainingStats(laps) {
|
||||
const paceLaps = laps.filter((lap) => !lap.excludedFromPace);
|
||||
const lapTimes = paceLaps.map((lap) => lap.lapMs);
|
||||
const recent = lapTimes.slice(-30);
|
||||
const previous = lapTimes.slice(Math.max(0, lapTimes.length - 60), Math.max(0, lapTimes.length - 30));
|
||||
const average = (values) => values.length ? Math.round(values.reduce((sum, value) => sum + value, 0) / values.length) : 0;
|
||||
const percentile = (values, ratio) => {
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
return sorted.length
|
||||
? sorted[Math.min(sorted.length - 1, Math.max(0, Math.round((sorted.length - 1) * ratio)))]
|
||||
: 0;
|
||||
};
|
||||
const spread = (values) => {
|
||||
const mean = average(values);
|
||||
return mean ? ((percentile(values, 0.9) - percentile(values, 0.1)) / mean) * 100 : 0;
|
||||
};
|
||||
const average = publicTrainingAverage;
|
||||
const spread = publicTrainingSpread;
|
||||
const recentAverageMs = average(recent);
|
||||
const previousRecentAverageMs = average(previous);
|
||||
const consistencySpreadPct = spread(recent);
|
||||
@@ -488,7 +499,7 @@ function buildPublicTrainingStats(laps) {
|
||||
currentStreak = controlTargetMs && lapMs <= controlTargetMs ? currentStreak + 1 : 0;
|
||||
controlledLapStreak = Math.max(controlledLapStreak, currentStreak);
|
||||
});
|
||||
const medianMs = percentile(values, 0.5);
|
||||
const medianMs = publicTrainingPercentile(values, 0.5);
|
||||
const stintOutliers = values.filter((lapMs) => medianMs && lapMs > medianMs * 1.15).length;
|
||||
outlierLaps += stintOutliers;
|
||||
const comparisonSize = values.length >= 6 ? Math.max(3, Math.floor(values.length / 3)) : 0;
|
||||
|
||||
Reference in New Issue
Block a user