Track detail: HR chart as its own collapsible section + 'what if by HR' kcal comparison

- Speed/Elevation/Пульс are now collapsible <details> sections; the HR chart
  moved out of the Elevation block into its own 'Пульс' section (avg/max + chart),
  shown when the track has HR.
- Калории now also shows the Keytel HR-based kcal as a COMPARISON when HR is
  present ('По ЧСС (сравнение)'). The activity balance keeps the physics/MET
  number unchanged - this is only to see the difference.
This commit is contained in:
4aiman 2026-10-17 14:53:21 +03:00
commit 1df24000a3

View file

@ -5468,6 +5468,21 @@ async function showSessionDetail(sessionId) {
: "—";
let kcalMin = stats.duration > 0 ? (stats.kcal / (stats.duration / 60)).toFixed(1) : "—";
// HR-based kcal "what if" (Keytel) — comparison ONLY. The activity balance keeps the physics/MET
// number (don't disturb the established diet); here we just show what HR would give, if present.
let _t0hr = points.length ? new Date(points[0].ts).getTime() : 0;
let hrSamples = points.filter(p => p.heart_rate > 0).map(p => [(new Date(p.ts).getTime() - _t0hr) / 1000, p.heart_rate]);
let hasHr = hrSamples.length > 2;
let hrProfile = {
weightKg: session.rider_weight_kg ?? state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 75,
age: BMR.getAge(parseInt(state.profile?.birth_year) || 1988),
sex: state.profile?.sex || "m",
heightCm: parseFloat(state.profile?.height_cm) || 178,
};
let hrKcal = hasHr ? GPS.hrKcalNet(hrSamples, hrProfile) : null;
let hrAvg = hasHr ? Math.round(hrSamples.reduce((a, s) => a + s[1], 0) / hrSamples.length) : null;
let hrMax = hasHr ? Math.max(...hrSamples.map(s => s[1])) : null;
showModal(`
<h3>${actName}</h3>
<div id="sd-track-id" style="font-size:11px;color:var(--muted);margin:-6px 0 8px;cursor:pointer;user-select:all">ID: ${session.id}</div>
@ -5493,27 +5508,29 @@ async function showSessionDetail(sessionId) {
<div id="sd-map" class="sd-map"></div>
</div>
<div class="sd-section">
<div class="sd-title">Скорость</div>
<details class="sd-section sd-accordion" open>
<summary class="sd-title sd-acc-title">Скорость</summary>
<div class="sd-row">
<div class="sd-stat"><span class="sd-val">${stats.avgSpeed} км/ч</span><span class="sd-lbl">Средняя</span></div>
<div class="sd-stat"><span class="sd-val" id="sd-max-speed">${stats.maxSpeed} км/ч</span><span class="sd-lbl">Макс.</span></div>
</div>
<div id="sd-walk-stat" class="prod-hint" style="text-align:center;margin-bottom:6px"></div>
<div class="chart-wrap" id="sd-speed-wrap"><canvas id="sd-speed-chart" height="150" style="touch-action:pan-y"></canvas></div>
</div>
</details>
<div class="sd-section">
<div class="sd-title">Калории</div>
<div class="sd-row">
<div class="sd-stat"><span class="sd-val">${session.total_kcal ?? stats.kcal} ккал</span><span class="sd-lbl">Всего</span></div>
<div class="sd-stat"><span class="sd-val">${session.total_kcal ?? stats.kcal} ккал</span><span class="sd-lbl">${hasHr ? "Физика (в баланс)" : "Всего"}</span></div>
<div class="sd-stat"><span class="sd-val">${kcalMin} ккал/мин</span><span class="sd-lbl">Расход</span></div>
</div>
${hasHr ? `<div class="sd-row"><div class="sd-stat"><span class="sd-val">${hrKcal} ккал</span><span class="sd-lbl">По ЧСС (сравнение)</span></div></div>
<div class="prod-hint" style="text-align:center;margin-top:4px">В баланс идёт «физика»; цифра по ЧСС (Keytel) - для сравнения.</div>` : ""}
</div>
${elevations.length > 0 ? `
<div class="sd-section">
<div class="sd-title">Высота</div>
<details class="sd-section sd-accordion" open>
<summary class="sd-title sd-acc-title">Высота</summary>
<div class="sd-row">
<div class="sd-stat"><span class="sd-val">${minElev} м</span><span class="sd-lbl">Мин.</span></div>
<div class="sd-stat"><span class="sd-val">${maxElev} м</span><span class="sd-lbl">Макс.</span></div>
@ -5523,8 +5540,17 @@ async function showSessionDetail(sessionId) {
<div class="sd-stat"><span class="sd-val">${stats.elevLoss} м</span><span class="sd-lbl">Спуск</span></div>
</div>
<div class="chart-wrap" id="sd-elev-wrap"><canvas id="sd-elev-chart" height="150" style="touch-action:pan-y"></canvas></div>
<div class="chart-wrap" id="sd-hr-wrap" style="display:none"><canvas id="sd-hr-chart" height="150" style="touch-action:pan-y"></canvas></div>
</div>` : ""}
</details>` : ""}
${hasHr ? `
<details class="sd-section sd-accordion" open>
<summary class="sd-title sd-acc-title">Пульс</summary>
<div class="sd-row">
<div class="sd-stat"><span class="sd-val">${hrAvg}</span><span class="sd-lbl">Средний</span></div>
<div class="sd-stat"><span class="sd-val">${hrMax}</span><span class="sd-lbl">Макс.</span></div>
</div>
<div class="chart-wrap" id="sd-hr-wrap"><canvas id="sd-hr-chart" height="150" style="touch-action:pan-y"></canvas></div>
</details>` : ""}
${(stats.segments && stats.segments.length > 1) ? `
<details class="sd-section sd-accordion">