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: "Ликопин (мг)" },
];