Expose guided training exercises on the driver's mobile profile link
"Jämna varv" and "Stegträning" previously only lived on the admin Training page. A driver's own personal mobile profile link now offers the same self-service configure/start/stop flow already used for the 5-minute practice timer there, with live progress computed server-side from the driver's own share token so raw lap rows and driver IDs never reach the client. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,8 @@ const {
|
||||
normalizePublicTrainingWindow,
|
||||
splitPublicTrainingStints,
|
||||
addPublicTrainingMarker,
|
||||
savePublicEvenLapsConfig,
|
||||
savePublicStepTrainingConfig,
|
||||
} = require("../server/public_training_profile.js");
|
||||
|
||||
const DRIVER_ONE_TOKEN = "driver-one-token-12345678901234567890";
|
||||
@@ -92,6 +94,64 @@ test("public mobile marker is restricted to the shared profile and selected clas
|
||||
assert.equal(addPublicTrainingMarker(state, DISABLED_TOKEN, { classId: "class_1" }, 123458), null);
|
||||
});
|
||||
|
||||
test("a driver can save their own even-laps and step-training exercise from their mobile link", () => {
|
||||
const state = buildState();
|
||||
const evenLaps = savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
||||
assert.deepEqual({ driverId: evenLaps.driverId, classId: evenLaps.classId, targetMs: evenLaps.targetMs, toleranceMs: evenLaps.toleranceMs, lapCount: evenLaps.lapCount }, { driverId: "driver_1_4wd", classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
||||
assert.equal(state.training.evenLaps.length, 1);
|
||||
|
||||
// Saving again for the same driver+class updates in place rather than adding a duplicate.
|
||||
savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 13500, toleranceMs: 250, lapCount: 5 });
|
||||
assert.equal(state.training.evenLaps.length, 1);
|
||||
assert.equal(state.training.evenLaps[0].targetMs, 13500);
|
||||
|
||||
// A different driver's token, or a class the driver isn't in, must not be able to write it.
|
||||
assert.equal(savePublicEvenLapsConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
||||
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "unknown_class", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
||||
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 0, toleranceMs: 300, lapCount: 5 }), null, "a non-positive target is rejected");
|
||||
|
||||
const stepTraining = savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }] });
|
||||
assert.equal(stepTraining.driverId, "driver_1");
|
||||
assert.deepEqual(stepTraining.steps, [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }]);
|
||||
assert.equal(savePublicStepTrainingConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", steps: [{ effort: "calm", lapCount: 3 }] }), null, "driver_2 is not in class_2, unlike class_1 which they share with driver_1");
|
||||
assert.equal(savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [] }), null, "a sequence with no laps is rejected");
|
||||
});
|
||||
|
||||
test("public profile computes even-laps and step-training progress from their own since-marker, never leaking raw lap rows", () => {
|
||||
const now = Date.now();
|
||||
const state = buildState(now);
|
||||
state.training.evenLaps = [{ id: "e1", driverId: "driver_1", classId: "class_1", targetMs: 16000, toleranceMs: 500, lapCount: 5 }];
|
||||
state.training.stepTraining = [{ id: "s1", driverId: "driver_1", classId: "class_1", steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] }];
|
||||
// buildState's class_1 ledger for driver_1, oldest to newest: 19000, 18000, 16500, 16000
|
||||
// (the 500ms lap is dropped by the ">500ms" pace filter applied before classLaps is built).
|
||||
// A since-marker far enough back captures all four.
|
||||
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "", 0, 0, now - 2_000_000, now - 2_000_000);
|
||||
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
||||
|
||||
assert.deepEqual(classOne.evenLapsConfig, { targetMs: 16000, toleranceMs: 500, lapCount: 5 });
|
||||
// 16000 and 16500 are both within 16000+/-500 of the target; 18000 before them is not.
|
||||
assert.equal(classOne.evenLapsProgress.currentStreak, 2);
|
||||
assert.equal(classOne.evenLapsProgress.active, true);
|
||||
|
||||
assert.deepEqual(classOne.stepTrainingConfig, { steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] });
|
||||
assert.equal(classOne.stepTrainingProgress.complete, true, "both configured laps (out of four recorded since the marker) are filled");
|
||||
assert.equal(classOne.stepTrainingProgress.steps[0].averageLapMs, 19000);
|
||||
assert.equal(classOne.stepTrainingProgress.steps[1].averageLapMs, 18000);
|
||||
|
||||
const serialized = JSON.stringify(profile);
|
||||
assert.equal(serialized.includes("driver_1"), false, "the internal driver id must never reach the public payload, including in the new exercise fields");
|
||||
});
|
||||
|
||||
test("public profile omits exercise progress and configuration when the driver has not configured or started one", () => {
|
||||
const now = Date.now();
|
||||
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "", 0, 0);
|
||||
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
||||
assert.equal(classOne.evenLapsConfig, null);
|
||||
assert.equal(classOne.evenLapsProgress, null);
|
||||
assert.equal(classOne.stepTrainingConfig, null);
|
||||
assert.equal(classOne.stepTrainingProgress, null);
|
||||
});
|
||||
|
||||
test("public profile contains only the selected driver's sanitized aggregates", () => {
|
||||
const now = Date.now();
|
||||
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "2026-08-24T12:00:00.000Z", now - 30000, 30000);
|
||||
|
||||
Reference in New Issue
Block a user