lägg till stegträning-övning + v1.3.0

Andra guidade träningsövningen i Open Practice: konfigurera en
stegserie (antal varv + lugnt/kontrollerat/snabbt per steg),
förvalt till klassiska pyramiden. Training-sidan visar aktuellt
steg, kvarvarande varv och en progress-pill per steg. När
sekvensen är klar jämförs de två kontrollerade stegens snitt
neutralt (t.ex. "+0.90s"), utan att kalla en försämring för
trötthet.

syncTrainingLedger sprider nu resten av det normaliserade
training-objektet istället för att lista fält manuellt, så
samma klass av bugg som drabbade Jämna varv inte kan upprepas
för framtida fält.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-09 09:02:44 +02:00
parent 9122e91f2e
commit 01e1165748
9 changed files with 390 additions and 16 deletions

View File

@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
buildDriverActivePracticeStats,
buildEvenLapsProgress,
buildStepTrainingProgress,
buildDriverTrainingStats,
buildDriverTimingChannelStats,
buildTrainingAchievements,
@@ -363,6 +364,103 @@ test("even laps progress without a config or an active session", () => {
assert.equal(progress.lastLapMs, null);
});
test("step training progress advances through steps and has nothing to compare until two controlled steps have laps", () => {
const startedAt = 10_000;
const state = {
activeSessionId: "practice_1",
sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }],
resultsBySession: {
practice_1: {
passings: [
{ driverId: "driver_1", timestamp: 11_000, lapMs: 25_000, validLap: true },
{ driverId: "driver_1", timestamp: 37_000, lapMs: 25_500, validLap: true },
{ driverId: "driver_1", timestamp: 59_000, lapMs: 21_000, validLap: true },
{ driverId: "driver_1", timestamp: 81_200, lapMs: 21_200, validLap: true },
{ driverId: "driver_1", timestamp: 99_200, lapMs: 18_000, validLap: true },
],
},
},
};
const config = { steps: [
{ effort: "calm", lapCount: 2 },
{ effort: "controlled", lapCount: 2 },
{ effort: "fast", lapCount: 2 },
{ effort: "controlled", lapCount: 2 },
] };
const progress = buildStepTrainingProgress(state, "driver_1", "", config, 120_000);
assert.equal(progress.active, true);
assert.equal(progress.totalSteps, 4);
assert.equal(progress.totalLaps, 8);
assert.equal(progress.completedLaps, 5);
assert.equal(progress.complete, false);
assert.equal(progress.currentStepIndex, 2, "the third step (fast) is current after two calm and two controlled laps");
assert.deepEqual(progress.currentStep, { index: 2, effort: "fast", lapCount: 2, lapsCompleted: 1, lapsRemaining: 1 });
assert.equal(progress.steps[1].averageLapMs, 21_100, "the first controlled step's pace is averaged");
assert.equal(progress.controlledComparison, null, "only one controlled step has laps so far, nothing to compare yet");
});
test("step training progress compares the first and last controlled steps as a neutral pace difference", () => {
const startedAt = 10_000;
const state = {
activeSessionId: "practice_1",
sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }],
resultsBySession: {
practice_1: {
passings: [
{ driverId: "driver_1", timestamp: 11_000, lapMs: 25_000, validLap: true },
{ driverId: "driver_1", timestamp: 37_000, lapMs: 25_500, validLap: true },
{ driverId: "driver_1", timestamp: 59_000, lapMs: 21_000, validLap: true },
{ driverId: "driver_1", timestamp: 81_200, lapMs: 21_200, validLap: true },
{ driverId: "driver_1", timestamp: 99_200, lapMs: 18_000, validLap: true },
{ driverId: "driver_1", timestamp: 117_000, lapMs: 17_800, validLap: true },
{ driverId: "driver_1", timestamp: 138_800, lapMs: 21_800, validLap: true },
{ driverId: "driver_1", timestamp: 161_000, lapMs: 22_200, validLap: true },
],
},
},
};
const config = { steps: [
{ effort: "calm", lapCount: 2 },
{ effort: "controlled", lapCount: 2 },
{ effort: "fast", lapCount: 2 },
{ effort: "controlled", lapCount: 2 },
] };
const progress = buildStepTrainingProgress(state, "driver_1", "", config, 200_000);
assert.equal(progress.complete, true);
assert.equal(progress.currentStepIndex, 3);
assert.deepEqual(progress.controlledComparison, { firstAvgMs: 21_100, lastAvgMs: 22_000, diffMs: 900 });
});
test("step training progress counts an excluded lap toward the step but not its pace average", () => {
const startedAt = 10_000;
const state = {
activeSessionId: "practice_1",
sessions: [{ id: "practice_1", type: "open_practice", status: "running", startedAt }],
resultsBySession: {
practice_1: {
passings: [
{ driverId: "driver_1", timestamp: 11_000, lapMs: 21_000, validLap: true },
{ driverId: "driver_1", timestamp: 200_000, lapMs: 180_000, validLap: true, validationWarning: "above_max_stint" },
],
},
},
};
const config = { steps: [{ effort: "controlled", lapCount: 2 }] };
const progress = buildStepTrainingProgress(state, "driver_1", "", config, 400_000);
assert.equal(progress.steps[0].lapsCompleted, 2, "both laps count toward the step's lap count");
assert.equal(progress.steps[0].averageLapMs, 21_000, "the excluded lap is left out of the pace average");
assert.equal(progress.complete, true);
});
test("step training progress without a config or an active session", () => {
assert.equal(buildStepTrainingProgress({}, "driver_1", "", null), null);
assert.equal(buildStepTrainingProgress({}, "driver_1", "", { steps: [] }), null);
const progress = buildStepTrainingProgress({ activeSessionId: "x", sessions: [], resultsBySession: {} }, "driver_1", "", { steps: [{ effort: "calm", lapCount: 3 }] });
assert.equal(progress.active, false);
assert.equal(progress.completedLaps, 0);
assert.equal(progress.complete, false);
});
test("training stints split after a break over two minutes or a session change", () => {
const laps = [
{ sessionId: "practice_1", timestamp: 18_000, lapMs: 18_000 },