Add persistent driver training history and progression

This commit is contained in:
larssand
2026-08-24 16:06:36 +02:00
parent 79a8cb24b8
commit c2ed00861a
8 changed files with 424 additions and 36 deletions

View File

@@ -9,14 +9,17 @@ export function renderTrainingView(deps) {
saveState,
renderView,
normalizeTrainingState,
syncTrainingLedger,
buildDriverTrainingStats,
buildTrainingRecommendation,
getTrainingGoalProgress,
getNextTrainingMilestone,
getSelectedDriverId,
setSelectedDriverId,
} = deps;
state.training = normalizeTrainingState(state.training);
syncTrainingLedger();
const drivers = [...state.drivers].sort((left, right) => String(left.name).localeCompare(String(right.name)));
let selectedDriverId = String(getSelectedDriverId() || "");
if (!drivers.some((driver) => driver.id === selectedDriverId)) {
@@ -37,6 +40,7 @@ export function renderTrainingView(deps) {
const selectedDriver = drivers.find((driver) => driver.id === selectedDriverId);
const stats = buildDriverTrainingStats(state, selectedDriverId);
const recommendation = buildTrainingRecommendation(stats);
const nextMilestone = getNextTrainingMilestone(stats);
const goals = state.training.goals.filter((goal) => goal.driverId === selectedDriverId);
const driverOptions = drivers
.map((driver) => `<option value="${escapeHtml(driver.id)}" ${driver.id === selectedDriverId ? "selected" : ""}>${escapeHtml(driver.name)}</option>`)
@@ -76,6 +80,19 @@ export function renderTrainingView(deps) {
const targetText = recommendation.targetLapMs
? t("training.plan_target", { lap: formatLap(recommendation.targetLapMs) })
: t("training.plan_no_target");
const lastActivity = stats.lastLapAt
? new Date(stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
: t("training.no_activity");
const dailyRows = stats.dailySummaries.length
? stats.dailySummaries.slice(0, 10).map((day) => `
<tr>
<td>${escapeHtml(new Date(day.latestTimestamp).toLocaleDateString(undefined, { dateStyle: "medium" }))}</td>
<td>${day.laps}</td>
<td>${formatLap(day.bestLapMs)}</td>
<td>${formatLap(day.averageLapMs)}</td>
</tr>
`).join("")
: `<tr><td colspan="4">${escapeHtml(t("training.no_history"))}</td></tr>`;
dom.view.innerHTML = `
<section class="training-hero">
@@ -92,11 +109,29 @@ export function renderTrainingView(deps) {
<div class="grid cols-4 training-stats">
${trainingStat(t("training.total_laps"), String(stats.totalLaps), t("training.valid_laps"))}
${trainingStat(t("training.sessions"), String(stats.totalSessions), t("training.recorded_sessions"))}
${trainingStat(t("training.training_days"), String(stats.trainingDays), t("training.sessions_count", { sessions: stats.totalSessions }))}
${trainingStat(t("training.best_lap"), formatLap(stats.bestLapMs), t("training.personal_best"))}
${trainingStat(t("training.consistency"), stats.recentLapCount ? `${stats.consistencySpreadPct.toFixed(1)}%` : "-", t("training.last_30_laps"))}
</div>
<div class="training-insight-grid">
<section class="training-milestone-card">
<div>
<span class="training-kicker">${escapeHtml(t("training.next_level"))}</span>
<h3>${escapeHtml(t("training.next_level_target", { laps: nextMilestone.targetLaps }))}</h3>
<p>${escapeHtml(t("training.goal_remaining", { laps: nextMilestone.remainingLaps }))}</p>
</div>
<strong>${nextMilestone.currentLaps} / ${nextMilestone.targetLaps}</strong>
<div class="training-progress"><span style="width:${nextMilestone.percent.toFixed(2)}%"></span></div>
</section>
<section class="training-insights">
${trainingInsight(t("training.average_lap"), formatLap(stats.averageLapMs))}
${trainingInsight(t("training.recent_average"), formatLap(stats.recentAverageMs))}
${trainingInsight(t("training.controlled_streak"), t("training.lap_count", { laps: stats.controlledLapStreak }))}
${trainingInsight(t("training.last_activity"), lastActivity)}
</section>
</div>
<div class="training-layout">
<section class="panel training-plan">
<div class="panel-header"><h3>${escapeHtml(t("training.recommendation"))}</h3><span class="badge">20 min</span></div>
@@ -127,6 +162,19 @@ export function renderTrainingView(deps) {
</section>
</div>
<section class="panel mt-16">
<div class="panel-header">
<h3>${escapeHtml(t("training.history"))}</h3>
${stats.personalBestGainMs ? `<span class="badge success">${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}</span>` : ""}
</div>
<div class="panel-body table-wrap training-history-wrap">
<table class="training-history-table">
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.average_lap"))}</th></tr></thead>
<tbody>${dailyRows}</tbody>
</table>
</div>
</section>
<section class="panel mt-16">
<div class="panel-header"><h3>${escapeHtml(t("training.goals"))}</h3><span class="badge">${goals.length}</span></div>
<div class="panel-body training-goals">${goalMarkup}</div>
@@ -179,3 +227,7 @@ export function renderTrainingView(deps) {
function trainingStat(label, value, note) {
return `<article class="stat-card"><p>${label}</p><h3>${value}</h3><small>${note}</small></article>`;
}
function trainingInsight(label, value) {
return `<div><span>${label}</span><strong>${value}</strong></div>`;
}