Fix string weight causing 40k kcal, navigation, remove egg hardcode

Critical kcal fix:
- Profile weight is stored as string ("88"), not number
- "88" + 12 = "8812" (string concat) → mass 8812kg → 40000 kcal
- Fix: parseFloat(weightKg) at top of computeStats
- Also clamp gradient to +-50% to prevent NaN from GPS spikes

Navigation:
- Chart fullscreen on Today screen now has popstate handler
- Global popstate: collapse any visible .chart-full before skipping stale entries
- Both X button and back gesture properly collapse chart

Removed egg hardcode:
- product_units no longer auto-seeded with egg data
- User adds per-product unit mappings manually
- Eliminates slow LIKE query on 13000 products at every startup

Noted bike settings from user: weight 16kg, wheel 24", chainring 40, cog 18, mixed
This commit is contained in:
4aiman 2026-05-25 03:00:47 +03:00
commit 9b5c9a204c
3 changed files with 22 additions and 25 deletions

View file

@ -906,22 +906,8 @@ async function runMigrations() {
console.log("[db.js] Seeded 5 serving units");
}
// Seed product_units for eggs (one-time)
try {
const puCnt = await select("SELECT COUNT(*) as cnt FROM product_units");
if (puCnt[0].cnt === 0) {
const eggProducts = await select(
`SELECT id FROM products WHERE name_lower LIKE '%яйц%' AND name_lower LIKE '%кур%'`
);
for (const ep of eggProducts) {
await execute(
`INSERT OR IGNORE INTO product_units (product_id, unit_id, grams_per_unit)
VALUES (?1, 1, 60)`, [ep.id]
);
}
if (eggProducts.length > 0) console.log(`[db.js] Linked ${eggProducts.length} egg products`);
}
} catch (e) { console.error("[db.js] egg unit seeding error:", e); }
// product_units: user adds per-product unit mappings manually via UI.
// No auto-seeding — product names too varied for reliable matching.
// --- Bikes table ---
await execute(`CREATE TABLE IF NOT EXISTS bikes (

View file

@ -67,12 +67,12 @@ GPS.computeStats = function(points, weightKg, opts) {
const rho = 1.225; // air density at sea level (kg/m^3)
const efficiency = 0.25; // human mechanical efficiency (~25%)
const Crr = opts?.surfaceCrr ?? 0.006; // rolling resistance (mixed surface default)
const mTotal = (weightKg ?? 80) + (opts?.bikeWeightKg ?? 0);
const wKg = parseFloat(weightKg) || 80; // ensure numeric (profile stores strings)
const mTotal = wKg + (parseFloat(opts?.bikeWeightKg) || 0);
const isBike = (opts?.activityType ?? "bike") === "bike";
let totalKcal = 0;
// Basal metabolic rate per minute (MET=1 sitting, formula: MET * 3.5 * kg / 200)
const basalPerMin = (1.0 * 3.5 * (weightKg ?? 80)) / 200;
const basalPerMin = (1.0 * 3.5 * wKg) / 200;
// Cadence tracking
let totalPedalRevs = 0;
@ -111,8 +111,8 @@ GPS.computeStats = function(points, weightKg, opts) {
const rawAlt = sorted[i].altitude ?? sorted[i].altitude_gps_m ?? 0;
const rawAltPrev = sorted[i-1].altitude ?? sorted[i-1].altitude_gps_m ?? 0;
const altDiff = rawAlt - rawAltPrev;
const gradient = d > 2 ? altDiff / d : 0; // rise/run
const sinG = gradient; // for small angles sin ~= gradient
const gradient = d > 2 ? Math.max(-0.5, Math.min(0.5, altDiff / d)) : 0; // clamp to +-50%
const sinG = gradient;
const cosG = Math.sqrt(1 - sinG * sinG);
// Power required (watts): gravity + rolling resistance + aerodynamic drag
@ -149,7 +149,7 @@ GPS.computeStats = function(points, weightKg, opts) {
else if (speedKmh < 16) met = 12.5; // fast running
else met = 15.0; // sprint
totalKcal += (dt / 60) * (met * 3.5 * (weightKg ?? 80)) / 200;
totalKcal += (dt / 60) * (met * 3.5 * wKg) / 200;
}
}

View file

@ -607,11 +607,14 @@ async function renderStatsCharts(container, days, foodTotals, bmr) {
const closeBtn = document.createElement("div");
closeBtn.className = "chart-close-btn";
closeBtn.innerHTML = "&#x2715;";
closeBtn.onclick = (e) => {
e.stopPropagation();
function collapseChart() {
wrap.classList.remove("chart-full");
closeBtn.remove();
};
window.removeEventListener("popstate", chartPopHandler);
}
closeBtn.onclick = (e) => { e.stopPropagation(); collapseChart(); history.back(); };
const chartPopHandler = () => { collapseChart(); };
window.addEventListener("popstate", chartPopHandler, { once: true });
wrap.appendChild(closeBtn);
};
});
@ -3218,6 +3221,14 @@ function esc(str) {
window.addEventListener("popstate", (e) => {
// If a modal is open, don't handle here (modal has its own handler)
if (document.querySelector(".modal-overlay")) return;
// Chart fullscreen on screen (not in modal) — collapse it
const fsChart = document.querySelector(".chart-full");
if (fsChart) {
fsChart.classList.remove("chart-full");
const btn = fsChart.querySelector(".chart-close-btn");
if (btn) btn.remove();
return;
}
// Stale modal/chart entries left after app restart — skip them
if (e.state && (e.state.modal || e.state.chartFull)) {
history.back();