Strap usage tracking: accrue real HR-delivering time + battery steps natively

The strap's battery % is coarse/laggy (reads 100% for >24h), so to judge real
battery life we accumulate the time it actually delivered HR plus every battery
step into strap_uptime.json in the foreground service (persistent across
sessions). ble_read_strap_uptime / ble_reset_strap_uptime expose and reset it;
Settings shows hours-since-charge + steps with a Charged reset button.
This commit is contained in:
4aiman 2026-10-20 23:49:17 +03:00
commit abc4e3cef0
6 changed files with 174 additions and 4 deletions

View file

@ -5597,6 +5597,7 @@ async function showSessionDetail(sessionId) {
<button id="sd-recalc" class="btn-secondary full-width" style="margin-top:8px">Пересчитать</button>
<button id="sd-refine" class="btn-secondary full-width" style="margin-top:8px">Уточнить высоты (онлайн)</button>
<button id="sd-split" class="btn-secondary full-width" style="margin-top:8px">Разрезать трек</button>
<button id="sd-unmerge" class="btn-secondary full-width" style="margin-top:8px;display:none">Разделить склейку</button>
<button id="sd-export" class="btn-secondary full-width" style="margin-top:8px">Экспорт GPX</button>
<button id="sd-share" class="btn-secondary full-width" style="margin-top:8px">Похвастаться</button>
<button id="sd-delete" class="btn-secondary full-width" style="margin-top:8px;color:#cf6679">Удалить тренировку</button>
@ -6387,6 +6388,35 @@ async function showSessionDetail(sessionId) {
};
}
// Un-merge button: only for a merged session (points carry >1 orig_session_id). Splits it
// back into the original rides, recomputing each; shown only when it's actually a merge.
let unmergeBtn = document.querySelector("#sd-unmerge");
if (unmergeBtn) (async () => {
let n = 0; try { n = await DB.countMergedOrigins(sessionId); } catch {}
if (n < 2) return;
unmergeBtn.style.display = "";
unmergeBtn.textContent = `Разделить склейку (${n})`;
unmergeBtn.onclick = async () => {
if (!(await appConfirm(`Разделить трек обратно на ${n} исходные поездки? Статистика каждой пересчитается (точную по физике можно потом «Пересчитать» в каждой).`, "Разделить"))) return;
unmergeBtn.disabled = true; unmergeBtn.textContent = "Разделяю...";
try {
let ids = await DB.unmergeGpsSession(sessionId);
if (!ids) { showToast("Это не склейка"); unmergeBtn.disabled = false; unmergeBtn.textContent = `Разделить склейку (${n})`; return; }
let w = session.rider_weight_kg ?? state.latestWeight?.weight_kg ?? state.profile?.weight_kg ?? 80;
for (let id of ids) {
let pts = await DB.getGpsPoints(id);
if (!pts.length) continue;
let st = GPS.computeStats(pts, w, { activityType: session.activity_type });
let endT = new Date(pts[pts.length - 1].ts).toTimeString().slice(0, 5);
try { await DB.finishGpsSession(id, endT, st.distance, st.kcal, st.avgSpeed, st.maxSpeed, st.elevGain, st.elevLoss, []); } catch (e) { console.error("[unmerge] recompute", e); }
}
showToast(`Разделено на ${ids.length} трека`);
if (winStack.length > 0) winStack.pop().close();
_skipPops = 1; history.back(); renderScreen("gps");
} catch (e) { showToast("Ошибка: " + (e.message || e)); unmergeBtn.disabled = false; unmergeBtn.textContent = `Разделить склейку (${n})`; }
};
})();
// Recalculate button
let recalcBtn = document.querySelector("#sd-recalc");
if (recalcBtn) recalcBtn.onclick = async () => {
@ -7395,6 +7425,13 @@ async function renderSettings(container) {
<div class="settings-item-desc">Не предлагать включить GPS при старте записи трека</div></div>
<input id="s-dontask-gps" type="checkbox" ${localStorage.getItem("baldav_dontask_gps") === "1" ? "checked" : ""} style="width:20px;height:20px;">
</label>
<div class="settings-item" id="s-strap-usage">
<div class="settings-item-left">
<div class="settings-item-title">Ремень ЧСС: наработка</div>
<div class="settings-item-desc" id="s-strap-usage-desc">—</div>
</div>
<button class="btn-secondary" id="s-strap-reset" style="padding:6px 12px;font-size:13px">Заряжен</button>
</div>
</div>
<div class="settings-section">
@ -7834,6 +7871,33 @@ async function renderSettings(container) {
let dontAskGps = container.querySelector("#s-dontask-gps");
if (dontAskGps) dontAskGps.onchange = () => { try { localStorage.setItem("baldav_dontask_gps", dontAskGps.checked ? "1" : "0"); } catch {} };
// Strap usage readout: total HR-delivering time since last "charged" reset + battery steps,
// so we can judge real battery life against the claimed 300h (the % reading is coarse/laggy).
let strapDesc = container.querySelector("#s-strap-usage-desc");
let fillStrapUsage = async () => {
if (!strapDesc) return;
try {
let raw = await window.__TAURI__.core.invoke("ble_read_strap_uptime");
let u = JSON.parse(raw || "{}");
let sec = Math.round(u.active_sec || 0);
let h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60);
let bat = (u.bat != null) ? `${u.bat}%` : "?";
let steps = Array.isArray(u.steps) ? u.steps : [];
let since = u.since ? new Date(u.since * 1000).toLocaleDateString("ru-RU") : "—";
let stepStr = steps.length
? " · шаги: " + steps.map(s => `${s.bat}%`).join("->")
: " · шагов заряда нет (ещё 100%?)";
strapDesc.textContent = `заряд ${bat} · в работе ${h}ч ${m}м с ${since}${stepStr}`;
} catch (e) { strapDesc.textContent = "нет данных"; }
};
fillStrapUsage();
let strapReset = container.querySelector("#s-strap-reset");
if (strapReset) strapReset.onclick = async () => {
if (!(await appConfirm("Сбросить счётчик наработки ремня (отметить как заряженный)?", "Сбросить"))) return;
try { await window.__TAURI__.core.invoke("ble_reset_strap_uptime"); } catch {}
fillStrapUsage();
};
let bleScanBtn = container.querySelector("#s-ble-scan");
let bleResults = container.querySelector("#s-ble-results");
if (bleScanBtn) {