Weight in profile, nav stack persistence, GPS kcal in daily expenditure

Profile:
- Added weight_kg field to profile modal (was only in weight_log)
- Weight fallback chain: weight_log -> profile -> 80kg

Bike modal:
- Active bike toggle: replaced checkbox with Да/Нет chip buttons

Navigation:
- navStack persisted to localStorage, restored on app restart
- Browser history seeded from saved navStack
- Last screen restored after restart (not always "today")

Daily expenditure:
- getActivityTotalsForDate now includes gps_sessions.total_kcal
- getDailyActivityTotals (stats chart) also includes GPS sessions
- GPS bike rides now appear in daily kcal expenditure

Physics kcal fix:
- Use raw altitude (not smoothed) for gradient in power calculation
- Smoothed altitude was killing real slopes (296 vs 460 kcal)
- Smoothed altitude kept only for displayed elevGain/Loss
This commit is contained in:
4aiman 2026-05-25 02:33:04 +03:00
commit 165e7e7e9c
2 changed files with 62 additions and 18 deletions

View file

@ -558,11 +558,17 @@ async function getFoodTotalsForDate(date) {
}
async function getActivityTotalsForDate(date) {
const rows = await select(
const manual = await select(
"SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = ?1",
[date]
);
return rows[0] ?? { kcal: 0, minutes: 0 };
const gps = await select(
"SELECT SUM(total_kcal) as kcal FROM gps_sessions WHERE date = ?1 AND total_kcal IS NOT NULL",
[date]
);
const manualKcal = manual[0]?.kcal ?? 0;
const gpsKcal = gps[0]?.kcal ?? 0;
return { kcal: manualKcal + gpsKcal, minutes: manual[0]?.minutes ?? 0 };
}
async function getDailyFoodTotals(days) {
@ -577,8 +583,11 @@ async function getDailyFoodTotals(days) {
async function getDailyActivityTotals(days) {
return select(
`SELECT date, SUM(kcal) as kcal FROM activity_log
WHERE date >= date('now', ?1) GROUP BY date ORDER BY date ASC`,
`SELECT date, SUM(kcal) as kcal FROM (
SELECT date, kcal FROM activity_log WHERE date >= date('now', ?1)
UNION ALL
SELECT date, total_kcal as kcal FROM gps_sessions WHERE date >= date('now', ?1) AND total_kcal IS NOT NULL
) GROUP BY date ORDER BY date ASC`,
[`-${days ?? 30} days`]
);
}

View file

@ -13,7 +13,18 @@ let state = {
// Navigation history stack — survives in-memory across screen changes.
// When app is restored from background, we replay the stack into browser history.
const NAV_MAX = 20;
const navStack = ["today"]; // always starts with "today" as root
// Restore nav stack from localStorage so back gesture works after app restart
const navStack = (() => {
try {
const saved = JSON.parse(localStorage.getItem("baldav_nav_stack"));
if (Array.isArray(saved) && saved.length > 0) return saved;
} catch {}
return ["today"];
})();
function persistNavStack() {
try { localStorage.setItem("baldav_nav_stack", JSON.stringify(navStack)); } catch {}
}
// ---------------------------------------------------------------------------
// Boot
@ -75,15 +86,16 @@ async function init() {
state.profile = await DB.getFullProfile();
state.latestWeight = await DB.getLatestWeight();
// Seed browser history with a few entries so back gesture doesn't immediately close app
history.replaceState({ screen: "today" }, "");
for (let i = 0; i < 5; i++) history.pushState({ screen: "today" }, "");
if (false) {
renderScreen("settings");
} else {
renderScreen("today");
// Seed browser history from navStack so back gesture works after restart
history.replaceState({ screen: navStack[0] }, "");
for (let i = 1; i < navStack.length; i++) {
history.pushState({ screen: navStack[i] }, "");
}
// Add padding entries so first back doesn't exit
for (let i = 0; i < 3; i++) history.pushState({ screen: navStack[navStack.length - 1] }, "");
// Restore last screen
renderScreen(navStack[navStack.length - 1]);
}
// ---------------------------------------------------------------------------
@ -97,6 +109,7 @@ function renderScreen(name, fromPopstate) {
if (!fromPopstate && prevScreen && prevScreen !== name) {
navStack.push(name);
if (navStack.length > NAV_MAX) navStack.shift();
persistNavStack();
history.pushState({ screen: name }, "");
}
const screens = {
@ -2851,6 +2864,10 @@ function showProfileModal() {
showModal(`
<h3>Профиль</h3>
<div class="form-group">
<label>Вес (кг)</label>
<input id="m-weight" type="number" inputmode="decimal" step="0.1" value="${p.weight_kg ?? ""}" placeholder="80" />
</div>
<div class="form-group">
<label>Рост (см)</label>
<input id="m-height" type="number" inputmode="numeric" value="${p.height_cm ?? ""}" placeholder="178" />
@ -2875,12 +2892,14 @@ function showProfileModal() {
<input id="m-bmr-est" type="number" inputmode="numeric" value="${p.bmr_estimated ?? ""}" placeholder="1580" />
</div>`,
async overlay => {
const weight = parseFloat(overlay.querySelector("#m-weight").value) || null;
const height = parseInt(overlay.querySelector("#m-height").value);
const year = parseInt(overlay.querySelector("#m-year").value);
const bFormula = parseInt(overlay.querySelector("#m-bmr-formula").value) || null;
const bEst = parseInt(overlay.querySelector("#m-bmr-est").value) || null;
if (isNaN(height) || isNaN(year)) return false;
if (weight) await DB.setProfileValue("weight_kg", weight);
await DB.setProfileValue("height_cm", height);
await DB.setProfileValue("birth_year", year);
await DB.setProfileValue("sex", selectedSex);
@ -2888,6 +2907,7 @@ function showProfileModal() {
if (bEst) await DB.setProfileValue("bmr_estimated", bEst);
state.profile = { ...state.profile,
weight_kg: weight ? String(weight) : state.profile.weight_kg,
height_cm: String(height), birth_year: String(year), sex: selectedSex,
bmr_formula: bFormula ? String(bFormula) : null,
bmr_estimated: bEst ? String(bEst) : null };
@ -2949,10 +2969,11 @@ async function showBikeModal(bikeId) {
</select>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px">
<input id="m-bike-active" type="checkbox" ${bike.is_active ? "checked" : ""} />
Активный велосипед
</label>
<label>Активный велосипед</label>
<div class="type-chips">
<button class="type-chip ${bike.is_active ? "selected" : ""}" id="m-bike-active-yes" data-active="1">Да</button>
<button class="type-chip ${!bike.is_active ? "selected" : ""}" id="m-bike-active-no" data-active="0">Нет</button>
</div>
</div>
${!isNew ? `<button id="m-del-bike" class="btn-secondary full-width" style="margin-top:12px;color:#cf6679">Удалить велосипед</button>` : ""}`,
async overlay => {
@ -2965,7 +2986,7 @@ async function showBikeModal(bikeId) {
chainring_teeth: parseInt(overlay.querySelector("#m-bike-chainring").value) || null,
cog_teeth: parseInt(overlay.querySelector("#m-bike-cog").value) || null,
surface_type: overlay.querySelector("#m-bike-surface").value,
is_active: overlay.querySelector("#m-bike-active").checked,
is_active: overlay.querySelector("#m-bike-active-yes").classList.contains("selected"),
};
if (isNew) {
await DB.saveBike(data);
@ -2976,6 +2997,19 @@ async function showBikeModal(bikeId) {
renderScreen("settings");
});
// Active toggle chips
setTimeout(() => {
const overlay = document.querySelector(".modal-overlay");
if (!overlay) return;
overlay.querySelectorAll("#m-bike-active-yes, #m-bike-active-no").forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
overlay.querySelector("#m-bike-active-yes").classList.toggle("selected", btn.dataset.active === "1");
overlay.querySelector("#m-bike-active-no").classList.toggle("selected", btn.dataset.active === "0");
};
});
}, 50);
if (!isNew) {
setTimeout(() => {
const delBtn = document.querySelector("#m-del-bike");
@ -3191,6 +3225,7 @@ window.addEventListener("popstate", (e) => {
// Pop our nav stack and navigate to previous screen
if (navStack.length > 1) {
navStack.pop();
persistNavStack();
const prev = navStack[navStack.length - 1];
renderScreen(prev, true);
} else if (e.state && e.state.screen) {