From 4740bdbe11c8b34a67d4eb7e50c47abd26afee89 Mon Sep 17 00:00:00 2001 From: 4aiman Date: Mon, 25 May 2026 02:09:50 +0300 Subject: [PATCH] Bike profiles, physics-based kcal, session recalculation Bike profiles: - bikes table: name, weight, wheel, chainring/cog teeth, surface type - CRUD in settings screen, active bike indicator - Default bike: 12kg, 26", 42/18, mixed surface - bike_id linked to gps_sessions Physics-based kcal (replaces MET*0.97): - Per-segment power calculation: gravity + rolling (Crr) + aero (CdA) - Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost - Power <= 0 = coasting: basal metabolic rate only - Walk/run: proper MET table from Compendium of Physical Activities - Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal) - Smoothed altitude kept for displayed elevGain/Loss only Session recalculation: - "Recalculate" button on session detail page - Uses current weight + bike settings, re-reads all points - Bike selector per session (change bike -> auto recalculate) Other fixes: - User weight fallback chain: weight_log -> profile -> 80kg - Theoretical cadence from gear ratio + wheel circumference - Crash recovery preserves bike data --- frontend/js/db.js | 95 +++++++++++++++++++++ frontend/js/gps.js | 196 +++++++++++++++++++++++++++++++++++++++----- frontend/js/main.js | 193 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 455 insertions(+), 29 deletions(-) diff --git a/frontend/js/db.js b/frontend/js/db.js index a035d14..b4272dc 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -672,6 +672,63 @@ async function importGpxSession(session, points) { return sid; } +// --------------------------------------------------------------------------- +// Bikes +// --------------------------------------------------------------------------- + +async function getBikes() { + return select("SELECT * FROM bikes ORDER BY is_active DESC, name"); +} + +async function getBike(id) { + const rows = await select("SELECT * FROM bikes WHERE id = ?1", [id]); + return rows.length ? rows[0] : null; +} + +async function getActiveBike() { + const rows = await select("SELECT * FROM bikes WHERE is_active = 1 LIMIT 1"); + return rows.length ? rows[0] : null; +} + +async function saveBike(data) { + if (data.is_active) { + await execute("UPDATE bikes SET is_active = 0 WHERE is_active = 1"); + } + const r = await execute( + `INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth, + cog_teeth, gear_count, surface_type, notes, is_active) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`, + [data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26, + data.chainring_teeth ?? null, data.cog_teeth ?? null, + data.gear_count ?? null, data.surface_type ?? "mixed", + data.notes ?? null, data.is_active ? 1 : 0] + ); + return r.lastInsertId; +} + +async function updateBike(id, data) { + if (data.is_active) { + await execute("UPDATE bikes SET is_active = 0 WHERE is_active = 1"); + } + await execute( + `UPDATE bikes SET name=?1, weight_kg=?2, wheel_diameter_inch=?3, + chainring_teeth=?4, cog_teeth=?5, gear_count=?6, surface_type=?7, + notes=?8, is_active=?9 WHERE id=?10`, + [data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26, + data.chainring_teeth ?? null, data.cog_teeth ?? null, + data.gear_count ?? null, data.surface_type ?? "mixed", + data.notes ?? null, data.is_active ? 1 : 0, id] + ); +} + +async function deleteBike(id) { + await execute("DELETE FROM bikes WHERE id = ?1", [id]); +} + +async function updateSessionBike(sessionId, bikeId) { + await execute("UPDATE gps_sessions SET bike_id = ?1 WHERE id = ?2", [bikeId, sessionId]); +} + // Expose as globals — no ES module system, loaded via