diff --git a/frontend/js/db.js b/frontend/js/db.js index d5e6d0e..94a97f6 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -721,22 +721,24 @@ async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGa ); } +// NOTE: tauri-plugin-sql corrupts large i64 ids when returned as JS numbers +// (off by ±1). Always read/pass session ids via the text `id_str` column. async function getGpsSessions(limit) { return select( - "SELECT * FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1", + "SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1", [limit ?? 50] ); } async function getGpsSessionsForDate(date) { return select( - "SELECT * FROM gps_sessions WHERE date = ?1 ORDER BY start_time ASC", + "SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE date = ?1 ORDER BY start_time ASC", [date] ); } async function getGpsSession(id) { - const rows = await select("SELECT * FROM gps_sessions WHERE id = ?1", [id]); + const rows = await select("SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE id = ?1", [id]); return rows.length ? rows[0] : null; } @@ -760,9 +762,11 @@ async function importGpxSession(session, points) { session.max_speed_kmh, session.elevation_gain_m, session.elevation_loss_m, session.source ?? "gpx_import"] ); - // Get id of just-inserted session by matching unique fields + // Get id of just-inserted session by matching unique fields. + // CAST to TEXT — numeric i64 id is corrupted by the SQL plugin; use string + // sid for point inserts (SQLite coerces it to the INTEGER session_id column). const rows = await select( - "SELECT id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1", + "SELECT CAST(id AS TEXT) AS id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1", [session.date, session.start_time, session.source ?? "gpx_import"] ); const sid = rows[0].id; diff --git a/frontend/js/main.js b/frontend/js/main.js index 7ae6e7d..1dcdb92 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -399,7 +399,7 @@ async function renderToday(container) { let dist = s.total_distance_m ? (s.total_distance_m / 1000).toFixed(1) + " км" : ""; let kcal = s.total_kcal ? Math.round(s.total_kcal) + " кк" : ""; let dur = s.end_time && s.start_time ? s.start_time + "–" + s.end_time : s.start_time || ""; - return `
  • + return `
  • ${dur} ${dist}${dist && kcal ? " / " : ""}${kcal} ${s.elevation_gain_m ? `+${Math.round(s.elevation_gain_m)}м` : ""} @@ -593,15 +593,7 @@ async function renderToday(container) { // GPS session clicks → open track details container.querySelectorAll("[data-gps-session]").forEach(el => { - el.onclick = () => { - let sid = el.dataset.gpsSession; - renderScreen("recording"); - // After rendering, trigger session detail view - setTimeout(() => { - let sessionEl = document.querySelector(`[data-session-id="${sid}"]`); - if (sessionEl) sessionEl.click(); - }, 300); - }; + el.onclick = () => showSessionDetail(el.dataset.gpsSession); }); // Stats charts section @@ -2375,7 +2367,7 @@ async function renderGpsScreen(container) {

    История

    ${sessions.length ? sessions.map(s => ` -
    +
    ${s.date} ${s.start_time ?? ""} ${s.activity_type ?? ""} @@ -2423,12 +2415,10 @@ async function renderGpsScreen(container) { } }; - // Session click → detail view + // Session click → detail view. Keep sid as a STRING (numeric id from the + // SQL plugin is corrupted off-by-1 for large i64 values). container.querySelectorAll(".gps-session-item").forEach(item => { - item.onclick = () => { - let sid = parseInt(item.dataset.sid); - showSessionDetail(sid); - }; + item.onclick = () => showSessionDetail(item.dataset.sid); }); // Three-dot menu @@ -2583,7 +2573,7 @@ async function showAddGoalMarkerModal() { // Try to get last GPS point from recent sessions DB.getGpsSessions(1).then(async sessions => { if (sessions.length > 0) { - let pts = await DB.getGpsPoints(sessions[0].id); + let pts = await DB.getGpsPoints(sessions[0].id_str ?? sessions[0].id); if (pts.length > 0) { let last = pts[pts.length - 1]; defaultLat = last.lat; @@ -3538,12 +3528,12 @@ async function showSessionDetail(sessionId) { `, () => {}); - // Render map - setTimeout(() => { - if (renderToken !== _sessionDetailRender) return; - let mapEl = document.querySelector("#sd-map"); - if (!mapEl || !points.length) return; + if (renderToken !== _sessionDetailRender) return; + // Render map + let mapEl = document.querySelector("#sd-map"); + if (mapEl && points.length) { + void mapEl.offsetHeight; // force layout so Leaflet reads the real container size L.Icon.Default.imagePath = "lib/"; let map = L.map(mapEl, { attributionControl: false, zoomControl: false, scrollWheelZoom: false, dragging: false, tap: false, touchZoom: true }); mapEl._leafletMap = map; // save ref for winStack collapse @@ -3621,7 +3611,8 @@ async function showSessionDetail(sessionId) { }).bindPopup(info, { closeButton: false, className: "point-popup" }).addTo(map); } - // Fit map to all points + // Ensure Leaflet has correct container size, then fit + map.invalidateSize(); if (allCoords.length > 1) { map.fitBounds(L.latLngBounds(allCoords).pad(0.1)); } @@ -3639,7 +3630,7 @@ async function showSessionDetail(sessionId) { fsDiv.innerHTML = "✕"; map.dragging.enable(); map.scrollWheelZoom.enable(); - setTimeout(() => { map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); }, 100); + map.invalidateSize(); map.fitBounds(L.latLngBounds(allCoords).pad(0.05)); // Push fullscreen map onto winStack let parentOverlay = mapEl.closest(".modal-overlay"); winPush("fsMap", () => { @@ -3647,7 +3638,7 @@ async function showSessionDetail(sessionId) { fsDiv.innerHTML = "⛶"; map.dragging.disable(); map.scrollWheelZoom.disable(); - setTimeout(() => { map.invalidateSize(); if (mapEl._bounds) map.fitBounds(mapEl._bounds); }, 100); + map.invalidateSize(); if (mapEl._bounds) map.fitBounds(mapEl._bounds); }); winStack[winStack.length - 1].overlay = parentOverlay; } @@ -3689,7 +3680,7 @@ async function showSessionDetail(sessionId) { map.on("click", () => { if (!mapEl.classList.contains("sd-map-full")) toggleMapFs(); }); - }, 200); + } // Downsample arrays for smooth chart rendering (max ~500 points) function downsample(arr, maxPts) { @@ -3712,14 +3703,11 @@ async function showSessionDetail(sessionId) { ); // Render speed chart - setTimeout(() => { - if (renderToken !== _sessionDetailRender) return; - let ctx = document.querySelector("#sd-speed-chart"); - if (!ctx || !smoothed.length) return; - // Update maxSpeed display to match smoothed graph + let speedCtx = document.querySelector("#sd-speed-chart"); + if (speedCtx && smoothed.length) { let maxEl = document.querySelector("#sd-max-speed"); if (maxEl) maxEl.textContent = graphMaxSpeed + " км/ч"; - new Chart(ctx, { + new Chart(speedCtx, { type: "line", data: { labels: dsTimeLabels, @@ -3760,16 +3748,12 @@ async function showSessionDetail(sessionId) { }, }, }); - }, 300); + } // Render elevation chart - setTimeout(() => { - if (renderToken !== _sessionDetailRender) return; - let ctx = document.querySelector("#sd-elev-chart"); - if (!ctx || !elevations.length) return; - let elevLabels = points.filter(p => p.altitude_gps_m != null) - .map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1)); - new Chart(ctx, { + let elevCtx = document.querySelector("#sd-elev-chart"); + if (elevCtx && elevations.length) { + new Chart(elevCtx, { type: "line", data: { labels: dsElevLabels, @@ -3811,23 +3795,20 @@ async function showSessionDetail(sessionId) { }, }, }); - }, 400); + } // Render HR chart - setTimeout(() => { - if (renderToken !== _sessionDetailRender) return; - let hrData = points.map(p => p.heart_rate || null); - if (!hrData.some(v => v)) { /* no HR data — hide chart */ return; } + let hrData = points.map(p => p.heart_rate || null); + let hrCtx = document.querySelector("#sd-hr-chart"); + if (hrData.some(v => v) && hrCtx) { let wrap = document.querySelector("#sd-hr-wrap"); if (wrap) wrap.style.display = ""; - let ctx = document.querySelector("#sd-hr-chart"); - if (!ctx) return; let dsHr = downsample(hrData, MAX_CHART_PTS); let dsHrLabels = downsample( points.map(p => ((new Date(p.ts).getTime() - t0) / 60000).toFixed(1)), MAX_CHART_PTS ); - new Chart(ctx, { + new Chart(hrCtx, { type: "line", data: { labels: dsHrLabels, @@ -3873,11 +3854,10 @@ async function showSessionDetail(sessionId) { }, }, }); - }, 500); + } - // Delete button - setTimeout(() => { - if (renderToken !== _sessionDetailRender) return; + // Buttons + bike selector + { let exp = document.querySelector("#sd-export"); if (exp) exp.onclick = () => GPS.exportGPX(sessionId); @@ -3957,18 +3937,14 @@ async function showSessionDetail(sessionId) { history.back(); renderScreen("gps"); }; - }, 100); - - // IDEA 2: Goal markers — show closest approach for each marker - setTimeout(async () => { - try { - let goalSection = document.querySelector("#sd-goals-section"); - let goalsList = document.querySelector("#sd-goals-list"); - if (!goalSection || !goalsList || points.length < 2) return; - - let markers = await DB.getGoalMarkers(); - if (markers.length === 0) return; + } + // Goal markers — closest approach for each marker + try { + let goalSection = document.querySelector("#sd-goals-section"); + let goalsList = document.querySelector("#sd-goals-list"); + let markers = (goalSection && goalsList && points.length >= 2) ? await DB.getGoalMarkers() : []; + if (markers.length > 0) { // For each marker, find closest point in this track let results = []; for (let marker of markers) { @@ -3984,8 +3960,9 @@ async function showSessionDetail(sessionId) { let bestPrevDist = Infinity; let bestPrevDate = null; for (let ps of prevSessions) { - if (ps.id === sessionId) continue; - let psPts = await DB.getGpsPoints(ps.id); + let psId = ps.id_str ?? String(ps.id); + if (psId === String(sessionId)) continue; + let psPts = await DB.getGpsPoints(psId); if (psPts.length < 2) continue; for (let pp of psPts) { let d = GPS.distance(pp.lat, pp.lon, marker.lat, marker.lon); @@ -4029,23 +4006,17 @@ async function showSessionDetail(sessionId) { ${progressHtml}
    `; }).join(""); - } catch (e) { console.error("[goals]", e); } - }, 500); + } + } catch (e) { console.error("[goals]", e); } - // IDEA 8: Food cost of workout - setTimeout(async () => { - try { - let costSection = document.querySelector("#sd-food-cost-section"); - let costEl = document.querySelector("#sd-food-cost"); - if (!costSection || !costEl) return; - let sessionKcal = session.total_kcal ?? stats.kcal; - if (!sessionKcal || sessionKcal <= 0) return; - - let foods = await DB.getFrequentFoods(7); - if (foods.length === 0) return; - - // Build "cost" descriptions: N portions of food - let parts = []; + // Food cost of workout + try { + let costSection = document.querySelector("#sd-food-cost-section"); + let costEl = document.querySelector("#sd-food-cost"); + let sessionKcal = session.total_kcal ?? stats.kcal; + let foods = (costSection && costEl && sessionKcal > 0) ? await DB.getFrequentFoods(7) : []; + // Build "cost" descriptions: N portions of food + let parts = []; for (let f of foods) { if (!f.kcal || f.kcal <= 0) continue; // kcal per one serving (as user ate it) @@ -4061,19 +4032,17 @@ async function showSessionDetail(sessionId) { }); } - if (parts.length === 0) return; - + if (parts.length > 0) { // Take first 3, build readable string let display = parts.slice(0, 3); let lines = display.map(p => `${p.portions} порц. ${esc(p.name)} (по ${p.amountStr || "?"}, ${p.servingKcal} кк/порц.)` ); - costSection.style.display = ""; costEl.innerHTML = `
    ${Math.round(sessionKcal)} ккал — это как:
    ${lines.map(l => `
    ${l}
    `).join('
    или
    ')}
    `; - } catch (e) { console.error("[food-cost]", e); } - }, 600); + } + } catch (e) { console.error("[food-cost]", e); } } catch(e) { console.error("[showSessionDetail] error:", e, JSON.stringify(e)); } } @@ -4087,7 +4056,7 @@ async function showCompareModal(sessionId, currentSession, currentPoints) { let sessions = await DB.getGpsSessions(100); // Filter: same activity type, similar distance (+-30%), not the same session let similar = sessions.filter(s => - s.id !== sessionId && + (s.id_str ?? String(s.id)) !== String(sessionId) && s.activity_type === currentSession.activity_type && s.total_distance_m != null && s.total_distance_m > dist * 0.7 && s.total_distance_m < dist * 1.3 @@ -4105,31 +4074,29 @@ async function showCompareModal(sessionId, currentSession, currentPoints) { ${similar.map(s => { let d = s.total_distance_m != null ? GPS.formatDistance(Math.round(s.total_distance_m)) : "?"; let spd = s.avg_speed_kmh != null ? s.avg_speed_kmh + " км/ч" : ""; - return `
    + return `
    ${s.date} ${s.start_time ?? ""}
    ${d} | ${spd}
    `; }).join("")}
    `, () => {}); - setTimeout(() => { - let overlay = [...document.querySelectorAll(".modal-overlay")].pop(); - if (!overlay) return; - // Hide save/cancel buttons — this is a selection modal - let btns = overlay.querySelector(".modal-btns"); - if (btns) btns.style.display = "none"; + let overlay = [...document.querySelectorAll(".modal-overlay")].pop(); + if (!overlay) return; + // Hide save/cancel buttons — this is a selection modal + let btns = overlay.querySelector(".modal-btns"); + if (btns) btns.style.display = "none"; - overlay.querySelectorAll(".compare-item").forEach(item => { - item.onclick = async () => { - let compId = parseInt(item.dataset.sid); - // Close this modal, open comparison view - if (winStack.length > 0) winStack.pop().close(); - _skipPops = 1; - history.back(); - await showComparisonView(sessionId, compId, currentSession, currentPoints); - }; - }); - }, 100); + overlay.querySelectorAll(".compare-item").forEach(item => { + item.onclick = async () => { + let compId = item.dataset.sid; // keep as string (large i64 id) + // Close this modal, open comparison view + if (winStack.length > 0) winStack.pop().close(); + _skipPops = 1; + history.back(); + await showComparisonView(sessionId, compId, currentSession, currentPoints); + }; + }); } async function showComparisonView(currentId, compareId, currentSession, currentPoints) {