diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 26040b8..9584c6b 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1215,6 +1215,12 @@ body { width: 100%; } +/* Block touch on canvas when chart is minimized — lets page scroll normally. + Clicks on the .chart-wrap div still work (pointer-events: auto on parent). */ +.chart-wrap:not(.chart-full) canvas { + pointer-events: none; +} + .chart-full { position: fixed; left: 0; @@ -1243,6 +1249,17 @@ body { z-index: 10000; } +.point-popup .leaflet-popup-content-wrapper { + background: var(--surface); + color: var(--on-bg); + font-size: 12px; + border-radius: 6px; + padding: 4px 8px; +} +.point-popup .leaflet-popup-tip { + background: var(--surface); +} + .gps-download-status { text-align: center; font-size: 12px; diff --git a/frontend/data/products_seed.db b/frontend/data/products_seed.db index a43eeb2..da7e4e9 100644 Binary files a/frontend/data/products_seed.db and b/frontend/data/products_seed.db differ diff --git a/frontend/js/db.js b/frontend/js/db.js index 20ca36f..69023b6 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -394,7 +394,7 @@ async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGa async function getGpsSessions(limit) { return select( - "SELECT * FROM gps_sessions ORDER BY date DESC, start_time DESC LIMIT ?1", + "SELECT * FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1", [limit ?? 50] ); } @@ -508,6 +508,22 @@ async function runMigrations() { if (!(await hasColumn("gps_points", "seq"))) { await execute("ALTER TABLE gps_points ADD COLUMN seq INTEGER"); } + + // Repair old tracks: assign seq to points where it's NULL, ordered by timestamp. + // Uses rowid to break ties when timestamps are identical. + const nullSeqSessions = await select( + "SELECT DISTINCT session_id FROM gps_points WHERE seq IS NULL" + ); + for (const row of nullSeqSessions) { + const pts = await select( + "SELECT rowid, ts FROM gps_points WHERE session_id = ?1 ORDER BY ts ASC, rowid ASC", + [row.session_id] + ); + for (let i = 0; i < pts.length; i++) { + await execute("UPDATE gps_points SET seq = ?1 WHERE rowid = ?2", [i + 1, pts[i].rowid]); + } + console.log(`[db.js] Repaired seq for session ${row.session_id}: ${pts.length} points`); + } } window.DB._migrationsReady = runMigrations().then(() => { diff --git a/frontend/js/gps.js b/frontend/js/gps.js index 2616b69..c3e48bf 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -50,11 +50,17 @@ GPS.calcKcal = function(minutes, avgSpeedKmh, weightKg, elevGainM) { GPS.computeStats = function(points, weightKg) { if (points.length < 2) return { distance: 0, elevGain: 0, elevLoss: 0, avgSpeed: 0, maxSpeed: 0, kcal: 0, duration: 0 }; + // Sort by seq (if present) or timestamp to fix out-of-order points from native service + const sorted = points.slice().sort((a, b) => { + if (a.seq != null && b.seq != null) return a.seq - b.seq; + return new Date(a.ts) - new Date(b.ts); + }); + let distance = 0, elevGain = 0, elevLoss = 0; const speeds = []; // m/s per segment - for (let i = 1; i < points.length; i++) { - const p = points[i], prev = points[i - 1]; + for (let i = 1; i < sorted.length; i++) { + const p = sorted[i], prev = sorted[i - 1]; const d = GPS.distance(prev.lat, prev.lon, p.lat, p.lon); distance += d; @@ -81,8 +87,8 @@ GPS.computeStats = function(points, weightKg) { if (smooth > maxSpeed) maxSpeed = smooth; } - const first = new Date(points[0].ts); - const last = new Date(points[points.length - 1].ts); + const first = new Date(sorted[0].ts); + const last = new Date(sorted[sorted.length - 1].ts); const duration = (last - first) / 1000; const minutes = duration / 60; const avgSpeed = duration > 0 ? (distance / duration) : 0; diff --git a/frontend/js/main.js b/frontend/js/main.js index f09ff24..78c47ff 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -10,6 +10,11 @@ let state = { latestWeight: null, }; +// 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 + // --------------------------------------------------------------------------- // Boot // --------------------------------------------------------------------------- @@ -55,6 +60,10 @@ 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 { @@ -66,11 +75,13 @@ async function init() { // Router // --------------------------------------------------------------------------- -function renderScreen(name) { +function renderScreen(name, fromPopstate) { const prevScreen = state.screen; state.screen = name; - // Push history so "back" navigates between screens instead of exiting app - if (prevScreen && prevScreen !== name) { + // Push to nav stack and browser history (unless triggered by popstate) + if (!fromPopstate && prevScreen && prevScreen !== name) { + navStack.push(name); + if (navStack.length > NAV_MAX) navStack.shift(); history.pushState({ screen: name }, ""); } const screens = { @@ -758,9 +769,9 @@ async function renderGpsScreen(container) {