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:
@@ -20,6 +20,7 @@ import {
|
||||
getBestConsecutiveTrainingLaps,
|
||||
getBestTrainingDurationResult,
|
||||
getDriverTrainingLaps,
|
||||
getDriverTrainingLapsForCurrentLayout,
|
||||
getNextTrainingMilestone,
|
||||
getClubTrainingGoalProgress,
|
||||
getTrainingGoalProgress,
|
||||
@@ -176,6 +177,67 @@ test("driver statistics remain isolated and aggregate days and sessions", () =>
|
||||
assert.equal(stats.dailySummaries[1].laps, 2);
|
||||
});
|
||||
|
||||
test("driver pace/PB/consistency stats only compare laps from the current track layout, but lifetime totals still count every lap", () => {
|
||||
const oldLayoutDay = new Date(2026, 0, 1, 12, 0, 0).getTime();
|
||||
const newLayoutDay = new Date(2026, 2, 1, 12, 0, 0).getTime();
|
||||
const state = {
|
||||
trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: new Date(2026, 0, 1).getTime() }, { id: "layout_2", label: "Layout 2", startedAt: new Date(2026, 1, 1).getTime() }],
|
||||
training: {
|
||||
ledger: [
|
||||
// Old layout: much faster (e.g. a shorter loop) - must not count as today's record.
|
||||
{ driverId: "driver_1", sessionId: "old_session", timestamp: oldLayoutDay, lapMs: 10_000, trackLayoutId: "layout_1" },
|
||||
{ driverId: "driver_1", sessionId: "old_session", timestamp: oldLayoutDay + 11_000, lapMs: 10_500, trackLayoutId: "layout_1" },
|
||||
// Current layout: slower, genuinely the best time achievable on today's track.
|
||||
{ driverId: "driver_1", sessionId: "new_session", timestamp: newLayoutDay, lapMs: 18_000, trackLayoutId: "layout_2" },
|
||||
{ driverId: "driver_1", sessionId: "new_session", timestamp: newLayoutDay + 19_000, lapMs: 17_500, trackLayoutId: "layout_2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const layoutLaps = getDriverTrainingLapsForCurrentLayout(state, "driver_1");
|
||||
assert.equal(layoutLaps.length, 2, "only the current layout's laps");
|
||||
assert.ok(layoutLaps.every((lap) => lap.trackLayoutId === "layout_2"));
|
||||
|
||||
const stats = buildDriverTrainingStats(state, "driver_1");
|
||||
assert.equal(stats.totalLaps, 4, "lifetime total still counts every lap ever driven");
|
||||
assert.equal(stats.totalSessions, 2, "lifetime session count is unaffected by layout scoping");
|
||||
assert.equal(stats.trainingDays, 2, "training-day history is unaffected by layout scoping");
|
||||
assert.equal(stats.bestLapMs, 17_500, "best lap only compares laps from today's layout, not the faster old-layout lap");
|
||||
});
|
||||
|
||||
test("with no track layouts configured, layout scoping is a no-op (every lap has trackLayoutId \"\")", () => {
|
||||
const state = {
|
||||
training: {
|
||||
ledger: [
|
||||
{ driverId: "driver_1", sessionId: "s1", timestamp: 1000, lapMs: 15_000 },
|
||||
{ driverId: "driver_1", sessionId: "s1", timestamp: 16_000, lapMs: 14_000 },
|
||||
],
|
||||
},
|
||||
};
|
||||
assert.deepEqual(getDriverTrainingLapsForCurrentLayout(state, "driver_1"), getDriverTrainingLaps(state, "driver_1"));
|
||||
assert.equal(buildDriverTrainingStats(state, "driver_1").bestLapMs, 14_000);
|
||||
});
|
||||
|
||||
test("buildTrainingStintAnalysis's best-stint pick is scoped to the current track layout", () => {
|
||||
const state = {
|
||||
trackLayouts: [{ id: "layout_1", label: "Layout 1", startedAt: 0 }, { id: "layout_2", label: "Layout 2", startedAt: 50_000 }],
|
||||
training: {
|
||||
ledger: [
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 10_000, lapMs: 10_000, trackLayoutId: "layout_1" },
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 20_000, lapMs: 10_000, trackLayoutId: "layout_1" },
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "fast_old", timestamp: 30_000, lapMs: 10_000, trackLayoutId: "layout_1" },
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 100_000, lapMs: 16_000, trackLayoutId: "layout_2" },
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 116_000, lapMs: 16_000, trackLayoutId: "layout_2" },
|
||||
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "slower_new", timestamp: 132_000, lapMs: 16_000, trackLayoutId: "layout_2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
const { rows, best } = buildTrainingStintAnalysis(state, "driver_1");
|
||||
assert.equal(rows.length, 1, "the old-layout stint is excluded entirely");
|
||||
assert.equal(rows[0].sessionId, "slower_new");
|
||||
assert.equal(best.sessionId, "slower_new", "the faster old-layout stint is never picked as \"best\"");
|
||||
});
|
||||
|
||||
test("counted stint-boundary laps do not distort driver pace averages", () => {
|
||||
const state = {
|
||||
training: {
|
||||
|
||||
Reference in New Issue
Block a user