diff --git a/frontend/data/products_seed.db b/frontend/data/products_seed.db index 7b0b48b..63eb6ad 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 5c25fdd..96f4a7d 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -1141,6 +1141,24 @@ async function runMigrations() { await addSuppCol("omega6_mg", "REAL"); // омега-6 await addSuppCol("omega9_mg", "REAL"); // омега-9 + // Deduplicate products: keep lowest id, reassign food_log and recipe_ingredients + try { + const dupes = await select( + "SELECT name_lower, MIN(id) as keep_id, COUNT(*) as cnt FROM products WHERE name_lower IS NOT NULL GROUP BY name_lower HAVING cnt > 1" + ); + if (dupes.length > 0) { + for (const d of dupes) { + // Reassign food_log entries to the kept product + await execute("UPDATE food_log SET product_id = ?1 WHERE product_id IN (SELECT id FROM products WHERE name_lower = ?2 AND id != ?1)", [d.keep_id, d.name_lower]); + // Reassign recipe_ingredients + await execute("UPDATE recipe_ingredients SET product_id = ?1 WHERE product_id IN (SELECT id FROM products WHERE name_lower = ?2 AND id != ?1)", [d.keep_id, d.name_lower]); + // Delete duplicates + await execute("DELETE FROM products WHERE name_lower = ?1 AND id != ?2", [d.name_lower, d.keep_id]); + } + console.log(`[db.js] Deduplicated ${dupes.length} product groups`); + } + } catch (e) { console.error("[db.js] dedup error:", e); } + console.log("[db.js] recipe migrations done, checking seq repair..."); // Repair old tracks: assign seq to points where it's NULL, ordered by timestamp. diff --git a/frontend/js/main.js b/frontend/js/main.js index 91f141e..cbda855 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -3281,18 +3281,28 @@ async function showComparisonView(currentId, compareId, currentSession, currentP const compPoints = GPS.filterNoise(await DB.getGpsPoints(compareId), compSession.activity_type); if (compPoints.length < 2 || currentPoints.length < 2) return; - // Compute duration diff - const curDur = currentPoints.length >= 2 - ? (new Date(currentPoints[currentPoints.length-1].ts) - new Date(currentPoints[0].ts)) / 1000 : 0; - const compDur = compPoints.length >= 2 - ? (new Date(compPoints[compPoints.length-1].ts) - new Date(compPoints[0].ts)) / 1000 : 0; - const durDiff = curDur - compDur; - const durDiffStr = (durDiff <= 0 ? "" : "+") + GPS.formatDuration(Math.abs(Math.round(durDiff))); + // Determine which is newer — newer is always "current" for color coding + // Red = newer is slower (regression), Green = newer is faster (progress) + const curDate = currentSession.date || ""; + const compDate = compSession.date || ""; + let newerPoints, olderPoints, newerSession, olderSession; + if (curDate >= compDate) { + newerPoints = currentPoints; olderPoints = compPoints; + newerSession = currentSession; olderSession = compSession; + } else { + newerPoints = compPoints; olderPoints = currentPoints; + newerSession = compSession; olderSession = currentSession; + } + + // Compute duration diff (newer - older) + const newDur = (new Date(newerPoints[newerPoints.length-1].ts) - new Date(newerPoints[0].ts)) / 1000; + const oldDur = (new Date(olderPoints[olderPoints.length-1].ts) - new Date(olderPoints[0].ts)) / 1000; + const durDiff = newDur - oldDur; const durLabel = durDiff < 0 ? `На ${GPS.formatDuration(Math.abs(Math.round(durDiff)))} быстрее` : durDiff > 0 ? `На ${GPS.formatDuration(Math.round(durDiff))} медленнее` : "Одинаково"; - const curAvg = currentSession.avg_speed_kmh ?? 0; - const compAvg = compSession.avg_speed_kmh ?? 0; - const spdDiff = Math.round((curAvg - compAvg) * 10) / 10; + const newAvg = newerSession.avg_speed_kmh ?? 0; + const oldAvg = olderSession.avg_speed_kmh ?? 0; + const spdDiff = Math.round((newAvg - oldAvg) * 10) / 10; showModal(`

Сравнение треков

@@ -3302,14 +3312,14 @@ async function showComparisonView(currentId, compareId, currentSession, currentP
${durLabel}Время
-
${curAvg} км/чТекущая средняя
-
${compAvg} км/чСравнение
+
${newAvg} км/чНовый (${newerSession.date})
+
${oldAvg} км/чСтарый (${olderSession.date})
${spdDiff > 0 ? "+" : ""}${spdDiff}Разница
Карта
-
Фиолетовый — текущий, голубой — сравнение. Зелёные участки — быстрее, красные — медленнее.
+
Зелёные участки — прогресс (новый быстрее). Красные — регресс (новый медленнее).
`, () => {}); @@ -3329,38 +3339,31 @@ async function showComparisonView(currentId, compareId, currentSession, currentP const CachedLayer = TileCache.createLayer(); new CachedLayer({ maxZoom: 19 }).addTo(map); - // Draw comparison track (thin dashed cyan) - const compCoords = compPoints.map(p => [p.lat, p.lon]); - L.polyline(compCoords, { color: "#4dd0e1", weight: 3, dashArray: "8,6", opacity: 0.7 }).addTo(map); + // Build speed lookup for older track + const olderCoords = olderPoints.map(p => [p.lat, p.lon]); - // Build speed comparison: for each point on current track, find nearest on comparison track - // Pre-compute comparison speeds for quick nearest-neighbor lookup - const compSpeeds = []; - for (let i = 0; i < compPoints.length; i++) { - const spd = compPoints[i].speed_ms != null ? compPoints[i].speed_ms * 3.6 : 0; - compSpeeds.push(spd); - } + // Pre-compute older track speeds + const olderSpeeds = olderPoints.map(p => (p.speed_ms ?? 0) * 3.6); - // For each current segment, find nearest comparison point and compare speeds - // Draw colored polyline segments - for (let i = 1; i < currentPoints.length; i++) { - const cp = currentPoints[i]; - const cpPrev = currentPoints[i - 1]; - const curSpd = cp.speed_ms != null ? cp.speed_ms * 3.6 : 0; + // Draw newer track colored by speed difference vs older + // Green = newer faster (progress), Red = newer slower (regression) + for (let i = 1; i < newerPoints.length; i++) { + const cp = newerPoints[i]; + const cpPrev = newerPoints[i - 1]; + const newSpd = (cp.speed_ms ?? 0) * 3.6; - // Find nearest comparison point (simple brute force — OK for <2000 points) + // Find nearest older point let nearestIdx = 0, nearestDist = Infinity; - for (let j = 0; j < compPoints.length; j++) { - const d = GPS.distance(cp.lat, cp.lon, compPoints[j].lat, compPoints[j].lon); + for (let j = 0; j < olderPoints.length; j++) { + const d = GPS.distance(cp.lat, cp.lon, olderPoints[j].lat, olderPoints[j].lon); if (d < nearestDist) { nearestDist = d; nearestIdx = j; } } - const compSpd = compSpeeds[nearestIdx]; + const oldSpd = olderSpeeds[nearestIdx]; - // Color: green if faster, red if slower, white if similar (within 1 km/h) let segColor; - if (nearestDist > 100) segColor = "#bb86fc"; // too far to compare — use default - else if (curSpd > compSpd + 1) segColor = "#4caf50"; // faster - else if (curSpd < compSpd - 1) segColor = "#f44336"; // slower + if (nearestDist > 100) segColor = "#bb86fc"; // too far to compare + else if (newSpd > oldSpd + 1) segColor = "#4caf50"; // progress + else if (newSpd < oldSpd - 1) segColor = "#f44336"; // regression else segColor = "#bb86fc"; // similar L.polyline( @@ -3370,7 +3373,7 @@ async function showComparisonView(currentId, compareId, currentSession, currentP } // Fit to both tracks - const allCoords = currentPoints.map(p => [p.lat, p.lon]).concat(compCoords); + const allCoords = newerPoints.map(p => [p.lat, p.lon]).concat(olderCoords); if (allCoords.length > 1) { map.fitBounds(L.latLngBounds(allCoords).pad(0.1)); } @@ -3455,41 +3458,41 @@ async function showLogSupplementModal() { // --------------------------------------------------------------------------- const MICRO_FIELDS = [ - { key: "vit_a_mcg", label: "Вит A (мкг РЭ)" }, - { key: "vit_c_mg", label: "Вит C (мг)" }, - { key: "vit_d_mcg", label: "Вит D3 (мкг)" }, - { key: "vit_e_mg", label: "Вит E (мг ТЭ)" }, - { key: "vit_k_mcg", label: "Вит K (мкг)" }, - { key: "vit_b1_mg", label: "B1 (мг)" }, - { key: "vit_b2_mg", label: "B2 (мг)" }, - { key: "vit_b3_mg", label: "B3/PP (мг)" }, - { key: "vit_b5_mg", label: "B5 (мг)" }, - { key: "vit_b6_mg", label: "B6 (мг)" }, - { key: "vit_b7_mcg", label: "B7/биотин (мкг)" }, - { key: "folate_mcg", label: "B9/фолат (мг)", mult: 1000 }, - { key: "vit_b12_mcg", label: "B12 (мкг)" }, - { key: "calcium_mg", label: "Кальций (мг)" }, - { key: "magnesium_mg", label: "Магний (мг)" }, - { key: "iron_mg", label: "Железо (мг)" }, - { key: "zinc_mg", label: "Цинк (мг)" }, - { key: "copper_mg", label: "Медь (мг)" }, - { key: "selenium_mcg", label: "Селен (мг)", mult: 1000 }, - { key: "potassium_mg", label: "Калий (мг)" }, - { key: "sodium_mg", label: "Натрий (мг)" }, - { key: "phosphorus_mg", label: "Фосфор (мг)" }, - { key: "iodine_mg", label: "Йод (мг)" }, - { key: "chromium_mg", label: "Хром (мг)" }, - { key: "manganese_mg", label: "Марганец (мг)" }, - { key: "molybdenum_mcg", label: "Молибден (мкг)" }, + { key: "vit_a_mcg", label: "A / ретинол (мкг РЭ)" }, { key: "carotene_mcg", label: "Бета-каротин (мкг)" }, + { key: "vit_c_mg", label: "C / аскорбинка (мг)" }, + { key: "vit_d_mcg", label: "D3 / холекальциферол (мкг)" }, + { key: "vit_e_mg", label: "E / токоферол (мг ТЭ)" }, + { key: "vit_k_mcg", label: "K / филлохинон (мкг)" }, + { key: "vit_b1_mg", label: "B1 / тиамин (мг)" }, + { key: "vit_b2_mg", label: "B2 / рибофлавин (мг)" }, + { key: "vit_b3_mg", label: "B3 / PP / ниацин (мг)" }, + { key: "vit_b5_mg", label: "B5 / пантотенат (мг)" }, + { key: "vit_b6_mg", label: "B6 / пиридоксин (мг)" }, + { key: "vit_b7_mcg", label: "B7 / биотин (мкг)" }, + { key: "folate_mcg", label: "B9 / фолиевая (мг)", mult: 1000 }, + { key: "vit_b12_mcg", label: "B12 / цианокобаламин (мкг)" }, + { key: "calcium_mg", label: "Кальций / Ca (мг)" }, + { key: "magnesium_mg", label: "Магний / Mg (мг)" }, + { key: "iron_mg", label: "Железо / Fe (мг)" }, + { key: "zinc_mg", label: "Цинк / Zn (мг)" }, + { key: "copper_mg", label: "Медь / Cu (мг)" }, + { key: "selenium_mcg", label: "Селен / Se (мг)", mult: 1000 }, + { key: "potassium_mg", label: "Калий / K (мг)" }, + { key: "sodium_mg", label: "Натрий / Na (мг)" }, + { key: "phosphorus_mg", label: "Фосфор / P (мг)" }, + { key: "iodine_mg", label: "Йод / I (мг)" }, + { key: "chromium_mg", label: "Хром / Cr (мг)" }, + { key: "manganese_mg", label: "Марганец / Mn (мг)" }, + { key: "molybdenum_mcg", label: "Молибден / Mo (мкг)" }, { key: "fish_oil_mg", label: "Рыбий жир (мг)" }, - { key: "omega3_mg", label: "в т.ч. Омега-3 (мг)" }, - { key: "epa_mg", label: "в т.ч. EPA (мг)" }, - { key: "dha_mg", label: "в т.ч. DHA (мг)" }, + { key: "omega3_mg", label: "-- Омега-3 ПНЖК (мг)" }, + { key: "epa_mg", label: "-- EPA / эйкозапентаеновая (мг)" }, + { key: "dha_mg", label: "-- DHA / докозагексаеновая (мг)" }, { key: "omega6_mg", label: "Омега-6 (мг)" }, { key: "omega9_mg", label: "Омега-9 (мг)" }, - { key: "silicon_mg", label: "Кремний (мг)" }, - { key: "vanadium_mg", label: "Ванадий (мг)" }, + { key: "silicon_mg", label: "Кремний / Si (мг)" }, + { key: "vanadium_mg", label: "Ванадий / V (мг)" }, { key: "lutein_mg", label: "Лютеин (мг)" }, { key: "lycopene_mg", label: "Ликопин (мг)" }, ];