Dedup products, fix labels, fix track comparison, seed v9
1. Deduplicate products: 13482 -> 8325 in seed DB. JS migration deduplicates app DB on launch: reassigns food_log and recipe_ingredients to kept product before deleting dupes. 2. MICRO_FIELDS: full chemical names + alternatives for all nutrients. "B3 / PP / ниацин", "B9 / фолиевая", "Кальций / Ca" etc. 3. Track comparison: newer track always compared against older. Green = newer faster (progress), Red = newer slower (regression). Removed separate cyan dashed track — only colored segments. Order of selection doesn't matter — date determines which is newer. 4. Source badges in search: source column was already in SELECT, badges render via sourceBadge(p.source) in prod card. 5. Gap threshold already cosmetic-only (confirmed, no changes needed). Seed DB v9, VACUUM'd, indexed.
This commit is contained in:
parent
81125c03fa
commit
0136b8ae0e
3 changed files with 90 additions and 69 deletions
Binary file not shown.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(`
|
||||
<h3>Сравнение треков</h3>
|
||||
|
|
@ -3302,14 +3312,14 @@ async function showComparisonView(currentId, compareId, currentSession, currentP
|
|||
<div class="sd-stat"><span class="sd-val" style="color:${durDiff <= 0 ? "#4caf50" : "#f44336"}">${durLabel}</span><span class="sd-lbl">Время</span></div>
|
||||
</div>
|
||||
<div class="sd-row">
|
||||
<div class="sd-stat"><span class="sd-val">${curAvg} км/ч</span><span class="sd-lbl">Текущая средняя</span></div>
|
||||
<div class="sd-stat"><span class="sd-val">${compAvg} км/ч</span><span class="sd-lbl">Сравнение</span></div>
|
||||
<div class="sd-stat"><span class="sd-val">${newAvg} км/ч</span><span class="sd-lbl">Новый (${newerSession.date})</span></div>
|
||||
<div class="sd-stat"><span class="sd-val">${oldAvg} км/ч</span><span class="sd-lbl">Старый (${olderSession.date})</span></div>
|
||||
<div class="sd-stat"><span class="sd-val" style="color:${spdDiff >= 0 ? "#4caf50" : "#f44336"}">${spdDiff > 0 ? "+" : ""}${spdDiff}</span><span class="sd-lbl">Разница</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sd-section">
|
||||
<div class="sd-title">Карта</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:4px">Фиолетовый — текущий, голубой — сравнение. Зелёные участки — быстрее, красные — медленнее.</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:4px">Зелёные участки — прогресс (новый быстрее). Красные — регресс (новый медленнее).</div>
|
||||
<div id="compare-map" class="sd-map" style="height:300px"></div>
|
||||
</div>
|
||||
`, () => {});
|
||||
|
|
@ -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: "Ликопин (мг)" },
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue