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

@@ -24,7 +24,7 @@ import { renderEventManagerMarkup } from "./event_manager_view.js";
import { renderEventManagerView } from "./event_manager_controller.js";
import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFromBackendHelper, saveAmmcConfigToBackendHelper, refreshAmmcStatusHelper, startManagedAmmcHelper, stopManagedAmmcHelper, applyPersistedStateHelper, hydrateFromBackendHelper, scheduleBackendSyncHelper, syncStateToBackendHelper, pingBackendHelper, checkAppVersionHelper, startAppVersionPollingHelper, startOverlaySyncHelper, startOverlayRotationHelper, startOverlayLiveRefreshHelper, ensureAudioContextHelper, playPassingBeepHelper, playFinishSirenHelper, playLeaderCueHelper, playStartCueHelper, playBestLapCueHelper, pushOverlayEventHelper, getSpeechVoicesHelper, speakTextHelper, announcePassingHelper, announceRaceStartedHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js";
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildDriverTimingChannelStats as buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildEvenLapsProgress as buildEvenLapsProgressLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic, buildTrainingMissions as buildTrainingMissionsLogic, buildTrainingHighlights as buildTrainingHighlightsLogic, getClubTrainingGoalProgress as getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets as buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats as buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis as buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons as buildTrainingMarkerComparisonsLogic } from "./training_logic.js";
import { normalizeTrainingState as normalizeTrainingStateLogic, syncTrainingLedger as syncTrainingLedgerLogic, buildDriverTrainingStats as buildDriverTrainingStatsLogic, buildDriverTimingChannelStats as buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats as buildDriverActivePracticeStatsLogic, buildEvenLapsProgress as buildEvenLapsProgressLogic, buildStepTrainingProgress as buildStepTrainingProgressLogic, buildTrainingRecommendation as buildTrainingRecommendationLogic, getTrainingGoalProgress as getTrainingGoalProgressLogic, getNextTrainingMilestone as getNextTrainingMilestoneLogic, buildTrainingMissions as buildTrainingMissionsLogic, buildTrainingHighlights as buildTrainingHighlightsLogic, getClubTrainingGoalProgress as getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets as buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats as buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis as buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons as buildTrainingMarkerComparisonsLogic } from "./training_logic.js";
import { renderTrainingView } from "./training_views.js";
import { importDriversCsv, importCarsCsv } from "./csv_import.js";
@@ -320,10 +320,11 @@ const syncTrainingLedger = () => {
const buildDriverTrainingStats = (trainingState, driverId, classId = "") => buildDriverTrainingStatsLogic(trainingState, driverId, classId);
const buildDriverActivePracticeStats = (trainingState, driverId, nowTs = Date.now(), classId = "") => buildDriverActivePracticeStatsLogic(trainingState, driverId, nowTs, classId);
const buildEvenLapsProgress = (trainingState, driverId, classId, config, nowTs = Date.now()) => buildEvenLapsProgressLogic(trainingState, driverId, classId, config, nowTs);
const buildStepTrainingProgress = (trainingState, driverId, classId, config, nowTs = Date.now()) => buildStepTrainingProgressLogic(trainingState, driverId, classId, config, nowTs);
const buildTrainingRecommendation = (stats) => buildTrainingRecommendationLogic(stats);
const getTrainingGoalProgress = (goal, stats) => getTrainingGoalProgressLogic(goal, stats);
const getNextTrainingMilestone = (stats) => getNextTrainingMilestoneLogic(stats);
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, formatRaceClock, uid, saveState, syncStateToBackend, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildDriverTimingChannelStats: buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats, buildEvenLapsProgress, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, buildTrainingMissions: buildTrainingMissionsLogic, buildTrainingHighlights: buildTrainingHighlightsLogic, getClubTrainingGoalProgress: getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets: buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats: buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis: buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons: buildTrainingMarkerComparisonsLogic, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", value, selectedTrainingClassId); }, getSelectedClassId: () => selectedTrainingClassId, setSelectedClassId: (value) => { selectedTrainingClassId = value; updateViewUrl("training", selectedTrainingDriverId, value); } });
const renderTraining = () => renderTrainingView({ state, dom, t, escapeHtml, formatLap, formatRaceClock, uid, saveState, syncStateToBackend, renderView, normalizeTrainingState, syncTrainingLedger, buildDriverTrainingStats, buildDriverTimingChannelStats: buildDriverTimingChannelStatsLogic, buildDriverActivePracticeStats, buildEvenLapsProgress, buildStepTrainingProgress, buildTrainingRecommendation, getTrainingGoalProgress, getNextTrainingMilestone, buildTrainingMissions: buildTrainingMissionsLogic, buildTrainingHighlights: buildTrainingHighlightsLogic, getClubTrainingGoalProgress: getClubTrainingGoalProgressLogic, buildTrainingPaceBuckets: buildTrainingPaceBucketsLogic, buildTrainingWeekdayStats: buildTrainingWeekdayStatsLogic, buildTrainingStintAnalysis: buildTrainingStintAnalysisLogic, buildTrainingMarkerComparisons: buildTrainingMarkerComparisonsLogic, getSelectedDriverId: () => selectedTrainingDriverId, setSelectedDriverId: (value) => { selectedTrainingDriverId = value; updateViewUrl("training", value, selectedTrainingClassId); }, getSelectedClassId: () => selectedTrainingClassId, setSelectedClassId: (value) => { selectedTrainingClassId = value; updateViewUrl("training", selectedTrainingDriverId, value); } });
const applyCompetitorCorrection = (session, row, options = {}) => {
const result = applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
if (!options.reset && ((Number(options.timeMsDelta || 0) > 0) || (Number(options.lapDelta || 0) < 0))) {
@@ -647,6 +648,18 @@ const TRANSLATIONS = {
"training.evenlaps_lapcount_label": "Antal varv i serien",
"training.evenlaps_save": "Spara",
"training.evenlaps_suggestion_hint": "Förslag baserat på ditt bästa varv: {lap}",
"training.steptraining_eyebrow": "Övning",
"training.steptraining_title": "Stegträning",
"training.steptraining_hint": "Bygg din stegserie nedan för att börja. Standard är den klassiska pyramiden: lugnt, kontrollerat, snabbt, kontrollerat, lugnt.",
"training.steptraining_complete": "Serien klar – bra jobbat!",
"training.steptraining_current": "Steg {step}/{total}: {effort}",
"training.steptraining_remaining": "{laps} varv kvar",
"training.steptraining_controlled_compare": "Kontrollerat steg 1: {first} · Kontrollerat steg 2: {last} ({diff})",
"training.steptraining_config_hint": "Ange 0 varv för att hoppa över ett steg. Lugnt/kontrollerat/snabbt är en känsla, inte en exakt tid – systemet kan inte mäta hur hårt du kör.",
"training.steptraining_save": "Spara",
"training.steptraining_effort_calm": "Lugnt",
"training.steptraining_effort_controlled": "Kontrollerat",
"training.steptraining_effort_fast": "Snabbt",
"training.stints": "Stints",
"training.best_three": "Bästa 3 sammanhängande",
"training.best_five": "Bästa 5 min",
@@ -2047,6 +2060,18 @@ const TRANSLATIONS = {
"training.evenlaps_lapcount_label": "Laps in the streak",
"training.evenlaps_save": "Save",
"training.evenlaps_suggestion_hint": "Suggested from your best lap: {lap}",
"training.steptraining_eyebrow": "Exercise",
"training.steptraining_title": "Step training",
"training.steptraining_hint": "Build your step sequence below to get started. The default is the classic pyramid: calm, controlled, fast, controlled, calm.",
"training.steptraining_complete": "Sequence complete - nice work!",
"training.steptraining_current": "Step {step}/{total}: {effort}",
"training.steptraining_remaining": "{laps} laps remaining",
"training.steptraining_controlled_compare": "Controlled step 1: {first} · Controlled step 2: {last} ({diff})",
"training.steptraining_config_hint": "Set 0 laps to skip a step. Calm/controlled/fast is a feel, not an exact time - the system can't measure how hard you're pushing.",
"training.steptraining_save": "Save",
"training.steptraining_effort_calm": "Calm",
"training.steptraining_effort_controlled": "Controlled",
"training.steptraining_effort_fast": "Fast",
"training.stints": "Stints",
"training.best_three": "Best 3 consecutive",
"training.best_five": "Best 5 min",