${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) {