Improve training statistics, achievements and stable pace audio

This commit is contained in:
larssand
2026-09-03 06:36:17 +02:00
parent 82d52ed6cd
commit 4f847610dc
16 changed files with 315 additions and 50 deletions

View File

@@ -107,12 +107,12 @@ function buildPublicTrainingProfile(state, shareToken, updatedAt, sinceTimestamp
}
function buildPublicTimingChannelStats(state, classDriverIds) {
const splitTimes = [];
const splits = [];
const pitTimes = [];
Object.values(state?.resultsBySession || {}).forEach((result) => {
(Array.isArray(result?.splits) ? result.splits : []).forEach((split) => {
const splitMs = Math.round(Number(split?.splitMs || 0) || 0);
if (classDriverIds.has(String(split?.driverId || "")) && splitMs > 0) splitTimes.push(splitMs);
if (classDriverIds.has(String(split?.driverId || "")) && splitMs > 0) splits.push({ splitMs, timestamp: Number(split?.timestamp || 0) });
});
(Array.isArray(result?.pitStops) ? result.pitStops : []).forEach((stop) => {
const pitStopMs = Math.round(Number(stop?.pitStopMs || 0) || 0);
@@ -122,10 +122,19 @@ function buildPublicTimingChannelStats(state, classDriverIds) {
const average = (values) => values.length
? Math.round(values.reduce((sum, value) => sum + value, 0) / values.length)
: 0;
splits.sort((left, right) => left.timestamp - right.timestamp);
const splitTimes = splits.map((split) => split.splitMs);
let runningBestSplitMs = 0;
let latestSplitGainMs = 0;
splits.forEach((split) => {
if (runningBestSplitMs && split.splitMs < runningBestSplitMs) latestSplitGainMs = runningBestSplitMs - split.splitMs;
if (!runningBestSplitMs || split.splitMs < runningBestSplitMs) runningBestSplitMs = split.splitMs;
});
return {
splitCount: splitTimes.length,
bestSplitMs: splitTimes.length ? Math.min(...splitTimes) : 0,
averageSplitMs: average(splitTimes),
latestSplitGainMs,
pitStopCount: pitTimes.length,
totalPitMs: pitTimes.reduce((sum, value) => sum + value, 0),
bestPitStopMs: pitTimes.length ? Math.min(...pitTimes) : 0,
@@ -217,12 +226,41 @@ function buildPublicTrainingStats(laps) {
.slice(0, 30);
const bestLapMs = lapTimes.length ? Math.min(...lapTimes) : 0;
const controlTargetMs = bestLapMs ? Math.round(bestLapMs * 1.08) : 0;
let controlledLapStreak = 0;
for (let index = lapTimes.length - 1; index >= 0; index -= 1) {
if (!controlTargetMs || lapTimes[index] > controlTargetMs) break;
controlledLapStreak += 1;
}
const stints = splitPublicTrainingStints(laps);
const summarizeStints = (items) => {
let controlledLapStreak = 0;
let outlierLaps = 0;
let strongFinishStints = 0;
let cleanFiveMinuteStints = 0;
let completedFiveMinuteStints = 0;
items.forEach((stint) => {
let currentStreak = 0;
const values = stint.laps.filter((lap) => !lap.excludedFromPace).map((lap) => lap.lapMs);
values.forEach((lapMs) => {
currentStreak = controlTargetMs && lapMs <= controlTargetMs ? currentStreak + 1 : 0;
controlledLapStreak = Math.max(controlledLapStreak, currentStreak);
});
const medianMs = percentile(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;
const startPaceMs = comparisonSize ? average(values.slice(0, comparisonSize)) : 0;
const endPaceMs = comparisonSize ? average(values.slice(-comparisonSize)) : 0;
if (comparisonSize && endPaceMs <= startPaceMs * 1.01) strongFinishStints += 1;
const fiveMinuteResult = getPublicBestDurationResult([stint], 5 * 60 * 1000);
if (fiveMinuteResult) completedFiveMinuteStints += 1;
if (!stintOutliers && fiveMinuteResult) cleanFiveMinuteStints += 1;
});
return { controlledLapStreak, outlierLaps, strongFinishStints, cleanFiveMinuteStints, completedFiveMinuteStints };
};
const stintSummary = summarizeStints(stints);
const today = new Date();
const todayKey = [today.getFullYear(), String(today.getMonth() + 1).padStart(2, "0"), String(today.getDate()).padStart(2, "0")].join("-");
const todayStintSummary = summarizeStints(splitPublicTrainingStints(laps.filter((lap) => {
const date = new Date(lap.timestamp);
const dateKey = [date.getFullYear(), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0")].join("-");
return !Number.isNaN(date.getTime()) && dateKey === todayKey;
})));
const bestThreeConsecutive = getPublicBestConsecutiveLaps(stints, 3);
const bestFiveMinuteResult = getPublicBestDurationResult(stints, 5 * 60 * 1000);
@@ -240,7 +278,16 @@ function buildPublicTrainingStats(laps) {
previousConsistencySpreadPct,
consistencyTrendPct: trendSampleReady ? previousConsistencySpreadPct - consistencySpreadPct : 0,
trendSampleReady,
controlledLapStreak,
controlZoneMinMs: bestLapMs,
controlZoneMaxMs: controlTargetMs,
controlledLapStreak: stintSummary.controlledLapStreak,
outlierLaps: stintSummary.outlierLaps,
strongFinishStints: stintSummary.strongFinishStints,
cleanFiveMinuteStints: stintSummary.cleanFiveMinuteStints,
todayControlledLapStreak: todayStintSummary.controlledLapStreak,
todayStrongFinishStints: todayStintSummary.strongFinishStints,
todayCleanFiveMinuteStints: todayStintSummary.cleanFiveMinuteStints,
todayCompletedFiveMinuteStints: todayStintSummary.completedFiveMinuteStints,
dailySummaries,
stintCount: stints.length,
bestThreeConsecutive,
@@ -259,6 +306,8 @@ function buildPublicTrainingAchievements(stats) {
["days_3", Number(stats.trainingDays || 0), 3],
["days_10", Number(stats.trainingDays || 0), 10],
["controlled_5", Number(stats.controlledLapStreak || 0), 5],
["strong_finish", Number(stats.strongFinishStints || 0), 1],
["clean_five_minute", Number(stats.cleanFiveMinuteStints || 0), 1],
["five_minute", stats.bestFiveMinuteResult ? 1 : 0, 1],
];
return definitions.map(([id, current, target]) => ({