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:
@@ -49,6 +49,7 @@ export function renderTrainingView(deps) {
|
||||
buildDriverTimingChannelStats,
|
||||
buildDriverActivePracticeStats,
|
||||
buildEvenLapsProgress,
|
||||
buildStepTrainingProgress,
|
||||
buildTrainingRecommendation,
|
||||
getTrainingGoalProgress,
|
||||
getNextTrainingMilestone,
|
||||
@@ -109,6 +110,19 @@ export function renderTrainingView(deps) {
|
||||
const evenLapsConfig = (state.training.evenLaps || []).find((entry) => entry.driverId === selectedClassDriver.id && entry.classId === selectedClassId) || null;
|
||||
const evenLapsProgress = activePractice ? buildEvenLapsProgress(state, selectedProfileId, selectedClassId, evenLapsConfig, Date.now()) : null;
|
||||
const evenLapsSuggestedTargetMs = stats.bestLapMs ? Math.round(stats.bestLapMs * 1.05) : 0;
|
||||
const stepTrainingConfig = (state.training.stepTraining || []).find((entry) => entry.driverId === selectedClassDriver.id && entry.classId === selectedClassId) || null;
|
||||
const stepTrainingProgress = activePractice ? buildStepTrainingProgress(state, selectedProfileId, selectedClassId, stepTrainingConfig, Date.now()) : null;
|
||||
const stepTrainingEffortLabel = (effort) => t(`training.steptraining_effort_${effort}`);
|
||||
const defaultStepTrainingRows = [
|
||||
{ effort: "calm", lapCount: 5 },
|
||||
{ effort: "controlled", lapCount: 5 },
|
||||
{ effort: "fast", lapCount: 5 },
|
||||
{ effort: "controlled", lapCount: 5 },
|
||||
{ effort: "calm", lapCount: 5 },
|
||||
];
|
||||
const stepTrainingFormRows = stepTrainingConfig?.steps?.length
|
||||
? [...stepTrainingConfig.steps, ...Array(Math.max(0, 5 - stepTrainingConfig.steps.length)).fill({ effort: "calm", lapCount: 0 })].slice(0, 5)
|
||||
: defaultStepTrainingRows;
|
||||
const recommendation = buildTrainingRecommendation(stats);
|
||||
const nextMilestone = getNextTrainingMilestone(stats);
|
||||
const missions = buildTrainingMissions(stats);
|
||||
@@ -384,6 +398,53 @@ export function renderTrainingView(deps) {
|
||||
</form>
|
||||
${evenLapsSuggestedTargetMs && !evenLapsConfig ? `<p class="hint">${escapeHtml(t("training.evenlaps_suggestion_hint", { lap: formatLap(evenLapsSuggestedTargetMs) }))}</p>` : ""}
|
||||
</section>
|
||||
|
||||
<section class="training-steptraining-card">
|
||||
<div class="training-live-head">
|
||||
<div>
|
||||
<span class="training-kicker">${escapeHtml(t("training.steptraining_eyebrow"))}</span>
|
||||
<h3>${escapeHtml(t("training.steptraining_title"))}</h3>
|
||||
</div>
|
||||
</div>
|
||||
${stepTrainingConfig && stepTrainingProgress ? `
|
||||
${stepTrainingProgress.complete
|
||||
? `<p class="training-evenlaps-status"><strong>${escapeHtml(t("training.steptraining_complete"))}</strong></p>`
|
||||
: stepTrainingProgress.currentStep
|
||||
? `<p class="training-evenlaps-status">
|
||||
${escapeHtml(t("training.steptraining_current", { step: stepTrainingProgress.currentStepIndex + 1, total: stepTrainingProgress.totalSteps, effort: stepTrainingEffortLabel(stepTrainingProgress.currentStep.effort) }))}
|
||||
<strong>${escapeHtml(t("training.steptraining_remaining", { laps: stepTrainingProgress.currentStep.lapsRemaining }))}</strong>
|
||||
</p>`
|
||||
: ""}
|
||||
<div class="training-steptraining-steps">
|
||||
${stepTrainingProgress.steps.map((step) => `
|
||||
<span class="training-steptraining-pill training-steptraining-${escapeHtml(step.effort)} ${step.done ? "is-done" : step.index === stepTrainingProgress.currentStepIndex ? "is-current" : ""}">
|
||||
${escapeHtml(stepTrainingEffortLabel(step.effort))} · ${step.lapsCompleted}/${step.lapCount}
|
||||
</span>
|
||||
`).join("")}
|
||||
</div>
|
||||
${stepTrainingProgress.controlledComparison ? `
|
||||
<p class="hint">${escapeHtml(t("training.steptraining_controlled_compare", {
|
||||
first: formatLap(stepTrainingProgress.controlledComparison.firstAvgMs),
|
||||
last: formatLap(stepTrainingProgress.controlledComparison.lastAvgMs),
|
||||
diff: `${stepTrainingProgress.controlledComparison.diffMs >= 0 ? "+" : ""}${(stepTrainingProgress.controlledComparison.diffMs / 1000).toFixed(2)}s`,
|
||||
}))}</p>
|
||||
` : ""}
|
||||
` : `<p class="hint">${escapeHtml(t("training.steptraining_hint"))}</p>`}
|
||||
<form id="trainingStepTrainingForm" class="training-steptraining-form">
|
||||
${stepTrainingFormRows.map((row, index) => `
|
||||
<div class="training-steptraining-row">
|
||||
<select name="effort_${index}">
|
||||
<option value="calm" ${row.effort === "calm" ? "selected" : ""}>${escapeHtml(stepTrainingEffortLabel("calm"))}</option>
|
||||
<option value="controlled" ${row.effort === "controlled" ? "selected" : ""}>${escapeHtml(stepTrainingEffortLabel("controlled"))}</option>
|
||||
<option value="fast" ${row.effort === "fast" ? "selected" : ""}>${escapeHtml(stepTrainingEffortLabel("fast"))}</option>
|
||||
</select>
|
||||
<input name="laps_${index}" type="number" min="0" step="1" value="${row.lapCount || ""}" placeholder="0" />
|
||||
</div>
|
||||
`).join("")}
|
||||
<button class="btn btn-primary" type="submit">${escapeHtml(t("training.steptraining_save"))}</button>
|
||||
</form>
|
||||
<p class="hint">${escapeHtml(t("training.steptraining_config_hint"))}</p>
|
||||
</section>
|
||||
` : ""}
|
||||
|
||||
<div class="grid cols-4 training-stats">
|
||||
@@ -690,6 +751,33 @@ export function renderTrainingView(deps) {
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("trainingStepTrainingForm")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const steps = [0, 1, 2, 3, 4]
|
||||
.map((index) => ({
|
||||
effort: String(form.get(`effort_${index}`) || "calm"),
|
||||
lapCount: Math.max(0, Math.round(Number(form.get(`laps_${index}`) || 0) || 0)),
|
||||
}))
|
||||
.filter((step) => step.lapCount > 0);
|
||||
if (!steps.length) return;
|
||||
state.training.stepTraining = Array.isArray(state.training.stepTraining) ? state.training.stepTraining : [];
|
||||
const existing = state.training.stepTraining.find((entry) => entry.driverId === selectedClassDriver.id && entry.classId === selectedClassId);
|
||||
if (existing) {
|
||||
existing.steps = steps;
|
||||
existing.updatedAt = new Date().toISOString();
|
||||
} else {
|
||||
state.training.stepTraining.push({
|
||||
id: uid("step_training"),
|
||||
driverId: selectedClassDriver.id,
|
||||
classId: selectedClassId,
|
||||
steps,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("trainingMarkerForm")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
|
||||
Reference in New Issue
Block a user