Scope driver training-profile stats by track layout too
The user pointed out that track layout changes matter most for Open Practice/training, not races - so the admin Training page and the driver's own mobile profile (server/public_training_profile.js, the actual thing most viewed after practice) needed the same layout scoping already applied to the live "track record" audio cue. Every stat now splits into two groups: lifetime/commitment numbers (total laps, sessions, training days, milestone badges) still read a driver's whole history, since a track rebuild doesn't erase past practice. Pace/record/consistency numbers (personal best, recent-pace trend, consistency spread, the "stable pace" control zone, stint achievements, "best stint") now read only laps tagged with whichever layout is currently active. Each ledger entry is tagged with trackLayoutId at the moment it's synced from a session, so it survives the source session later being deleted. Verified with new unit tests covering both implementations and a real headless-browser run confirming both the admin page and the actual public mobile profile show the current-layout best lap (never a faster but incomparable old-layout one) while keeping the correct full lifetime lap count in both places. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getDriverProfile, getDriverProfileReferences } from "./driver_profile_logic.js";
|
||||
import { getTrackLayoutIdAt } from "./track_layouts_logic.js";
|
||||
|
||||
const PUBLIC_TRAINING_SECTIONS = ["progress", "achievements", "performance", "goals", "history"];
|
||||
|
||||
@@ -81,6 +82,7 @@ export function normalizeTrainingState(training, { uid }) {
|
||||
timestamp,
|
||||
lapMs: Math.max(0, Math.round(Number(entry?.lapMs || 0) || 0)),
|
||||
recordedAt: String(entry?.recordedAt || (timestamp ? new Date(timestamp).toISOString() : new Date().toISOString())),
|
||||
trackLayoutId: String(entry?.trackLayoutId || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter((entry) => {
|
||||
@@ -234,6 +236,7 @@ export function syncTrainingLedger(training, state, { uid }) {
|
||||
timestamp,
|
||||
lapMs,
|
||||
recordedAt: timestamp ? new Date(timestamp).toISOString() : new Date().toISOString(),
|
||||
trackLayoutId: String(session?.trackLayoutId || "").trim(),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -262,10 +265,24 @@ export function getDriverTrainingLaps(state, driverId, classId = "") {
|
||||
stintBoundary: Boolean(entry.stintBoundary),
|
||||
timestamp: Number(entry.timestamp || 0) || 0,
|
||||
lapMs: Number(entry.lapMs || 0),
|
||||
trackLayoutId: String(entry.trackLayoutId || ""),
|
||||
}))
|
||||
.sort((left, right) => left.timestamp - right.timestamp);
|
||||
}
|
||||
|
||||
// Every lifetime/commitment stat (total laps ever driven, training days, achievement
|
||||
// badges) intentionally keeps reading the full history via getDriverTrainingLaps above -
|
||||
// a track rebuild doesn't erase how much a driver has practiced. Only pace-comparison
|
||||
// stats (personal best, consistency, the "stable pace" cue's target) should stop blending
|
||||
// laps from a track that no longer exists into today's numbers, so those read this
|
||||
// layout-scoped subset instead. With no track layouts configured, every lap's
|
||||
// trackLayoutId is "" and getTrackLayoutIdAt() also returns "" - so this returns exactly
|
||||
// the same laps as getDriverTrainingLaps, and the feature stays fully inert until used.
|
||||
export function getDriverTrainingLapsForCurrentLayout(state, driverId, classId = "") {
|
||||
const currentLayoutId = getTrackLayoutIdAt(state?.trackLayouts);
|
||||
return getDriverTrainingLaps(state, driverId, classId).filter((lap) => lap.trackLayoutId === currentLayoutId);
|
||||
}
|
||||
|
||||
function percentile(values, ratio) {
|
||||
if (!values.length) return 0;
|
||||
const sorted = [...values].sort((left, right) => left - right);
|
||||
@@ -417,7 +434,9 @@ function getTrainingStintMetrics(stint) {
|
||||
}
|
||||
|
||||
export function buildTrainingStintAnalysis(state, driverId, classId = "") {
|
||||
const stints = splitTrainingStints(getDriverTrainingLaps(state, driverId, classId));
|
||||
// Scoped to the currently active track layout, same as buildDriverTrainingStats - a
|
||||
// "best stint" pick spanning a track that no longer exists would not be comparable.
|
||||
const stints = splitTrainingStints(getDriverTrainingLapsForCurrentLayout(state, driverId, classId));
|
||||
const rows = stints.map((stint, index) => {
|
||||
const { laps, values, outlierLaps, startPaceMs, endPaceMs, endChangeMs, strongFinish } = getTrainingStintMetrics(stint);
|
||||
return {
|
||||
@@ -637,8 +656,15 @@ export function getBestTrainingDurationResult(stints, targetMs = 5 * 60 * 1000)
|
||||
}
|
||||
|
||||
export function buildDriverTrainingStats(state, driverId, classId = "") {
|
||||
// Lifetime commitment stats (how much a driver has trained, ever) read the full lap
|
||||
// history - a track rebuild doesn't erase past practice. Every pace/record/consistency
|
||||
// stat below instead reads layoutLaps, scoped to whichever track layout is active right
|
||||
// now, so a lap from a track that no longer exists can't be reported as today's "record"
|
||||
// or blend into today's consistency numbers. See getDriverTrainingLapsForCurrentLayout.
|
||||
const laps = getDriverTrainingLaps(state, driverId, classId);
|
||||
const paceLaps = laps.filter((lap) => !lap.excludedFromPace);
|
||||
const layoutLaps = getDriverTrainingLapsForCurrentLayout(state, driverId, classId);
|
||||
const historyPaceLaps = laps.filter((lap) => !lap.excludedFromPace);
|
||||
const paceLaps = layoutLaps.filter((lap) => !lap.excludedFromPace);
|
||||
const lapTimes = paceLaps.map((lap) => lap.lapMs);
|
||||
const recentLapTimes = lapTimes.slice(-30);
|
||||
const previousWindowLapTimes = lapTimes.slice(Math.max(0, lapTimes.length - 60), Math.max(0, lapTimes.length - 30));
|
||||
@@ -656,8 +682,8 @@ export function buildDriverTrainingStats(state, driverId, classId = "") {
|
||||
const personalBestGainMs = previousBestMs && recentBestMs ? Math.max(0, previousBestMs - recentBestMs) : 0;
|
||||
const controlZoneMinMs = bestLapMs;
|
||||
const controlTargetMs = bestLapMs ? Math.round(bestLapMs * 1.08) : 0;
|
||||
const dailySummaries = buildDailySummaries(paceLaps);
|
||||
const stints = splitTrainingStints(laps);
|
||||
const dailySummaries = buildDailySummaries(historyPaceLaps);
|
||||
const stints = splitTrainingStints(layoutLaps);
|
||||
const stintMetrics = stints.map((stint) => ({ stint, ...getTrainingStintMetrics(stint) }));
|
||||
const controlledLapStreak = getLongestControlledLapStreak(stints, controlTargetMs);
|
||||
const completedFiveMinuteStints = stints.filter((stint) => getBestTrainingDurationResult([stint], 5 * 60 * 1000)).length;
|
||||
@@ -667,7 +693,7 @@ export function buildDriverTrainingStats(state, driverId, classId = "") {
|
||||
const bestThreeConsecutive = getBestConsecutiveTrainingLaps(stints, 3);
|
||||
const bestFiveMinuteResult = getBestTrainingDurationResult(stints, 5 * 60 * 1000);
|
||||
const todayKey = getLocalDayKey(Date.now());
|
||||
const todayLaps = laps.filter((lap) => getLocalDayKey(lap.timestamp) === todayKey);
|
||||
const todayLaps = layoutLaps.filter((lap) => getLocalDayKey(lap.timestamp) === todayKey);
|
||||
const todayStints = splitTrainingStints(todayLaps);
|
||||
const todayStintMetrics = todayStints.map((stint) => ({ stint, ...getTrainingStintMetrics(stint) }));
|
||||
const todayLapTimes = todayLaps.filter((lap) => !lap.excludedFromPace).map((lap) => lap.lapMs);
|
||||
|
||||
Reference in New Issue
Block a user