baldav/frontend/js/db.js
4aiman 8d9b5aaf7e Source badges on products: Магнит (red), 5ка (green), KDV (orange), ИОН (gray), Свой (yellow)
- sourceBadge() helper renders colored badge by source field
- searchProducts SELECT now includes source column
- getFoodForDate SELECT includes p.source as product_source
- Badge shown on product cards in search and food log entries
- CSS: .src-badge with per-source colors (pastel, dark-theme friendly)
2026-05-30 15:59:50 +03:00

1174 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Database layer — talks to tauri-plugin-sql via raw Tauri IPC.
// No imports, no npm. window.__TAURI__ is injected by Tauri (withGlobalTauri: true).
// The DB is preloaded at startup via tauri.conf.json plugins.sql.preload.
const DB_PATH = "sqlite:baldav.db";
function inv(cmd, args) {
return window.__TAURI__.core.invoke(`plugin:sql|${cmd}`, args);
}
async function execute(query, params) {
return inv("execute", { db: DB_PATH, query, values: params ?? [] });
}
async function select(query, params) {
return inv("select", { db: DB_PATH, query, values: params ?? [] });
}
// ---------------------------------------------------------------------------
// Profile (key-value store)
// ---------------------------------------------------------------------------
async function getProfileValue(key) {
const rows = await select("SELECT value FROM profile WHERE key = ?1", [key]);
return rows.length ? rows[0].value : null;
}
async function setProfileValue(key, value) {
await execute(
"INSERT OR REPLACE INTO profile (key, value) VALUES (?1, ?2)",
[key, String(value)]
);
}
async function getFullProfile() {
const rows = await select("SELECT key, value FROM profile");
const p = {};
for (const r of rows) p[r.key] = r.value;
return p;
}
// ---------------------------------------------------------------------------
// Weight log
// ---------------------------------------------------------------------------
async function addWeight(date, time, kg) {
await execute(
"INSERT INTO weight_log (date, time, weight_kg) VALUES (?1, ?2, ?3)",
[date, time, kg]
);
}
async function deleteWeight(id) {
await execute("DELETE FROM weight_log WHERE id = ?1", [id]);
}
async function getWeightsForDate(date) {
return select("SELECT * FROM weight_log WHERE date = ?1 ORDER BY time ASC", [date]);
}
async function getLatestWeight() {
const rows = await select(
"SELECT * FROM weight_log ORDER BY date DESC, time DESC LIMIT 1"
);
return rows.length ? rows[0] : null;
}
async function getRecentWeights(days) {
return select(
`SELECT date, MIN(weight_kg) as weight_kg
FROM weight_log
WHERE date >= date('now', ?1)
GROUP BY date ORDER BY date ASC`,
[`-${days ?? 30} days`]
);
}
// ---------------------------------------------------------------------------
// Sleep log
// ---------------------------------------------------------------------------
async function getSleepForDate(date) {
const rows = await select("SELECT * FROM sleep_log WHERE date = ?1", [date]);
return rows.length ? rows[0] : null;
}
async function saveSleep(date, bedTime, asleepTime, wakeTime, napMinutes, notes) {
const existing = await getSleepForDate(date);
if (existing) {
await execute(
`UPDATE sleep_log SET bed_time=?1, asleep_time=?2, wake_time=?3,
nap_minutes=?4, notes=?5 WHERE date=?6`,
[bedTime, asleepTime, wakeTime, napMinutes ?? 0, notes ?? null, date]
);
} else {
await execute(
`INSERT INTO sleep_log (date, bed_time, asleep_time, wake_time, nap_minutes, notes)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`,
[date, bedTime, asleepTime, wakeTime, napMinutes ?? 0, notes ?? null]
);
}
}
// ---------------------------------------------------------------------------
// Products
// ---------------------------------------------------------------------------
async function searchProducts(query, limit) {
const q = query.toLowerCase();
return select(
`SELECT id, name, brand, category, weight_g, kcal_per_100g,
protein_per_100g, fat_per_100g, carbs_per_100g, fiber_per_100g,
composition, nutrients_guessed, source,
vit_a_mcg, vit_c_mg, vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_e_mg,
calcium_mg, iron_mg, magnesium_mg, potassium_mg, sodium_mg, phosphorus_mg,
carotene_mcg, selenium_mcg, zinc_mg, copper_mg, folate_mcg,
vit_d_mcg, vit_k_mcg, vit_b6_mg, vit_b12_mcg
FROM products
WHERE name_lower LIKE ?1
GROUP BY COALESCE(store_id, id)
ORDER BY
CASE WHEN name_lower LIKE ?2 THEN 0 ELSE 1 END,
length(name),
name COLLATE NOCASE
LIMIT ?3`,
[`%${q}%`, `${q}%`, limit ?? 200]
);
}
async function getProduct(id) {
const rows = await select("SELECT * FROM products WHERE id = ?1", [id]);
return rows.length ? rows[0] : null;
}
async function addProduct(p) {
await execute(
`INSERT INTO products
(name, name_lower, brand, category, weight_g, kcal_per_100g, protein_per_100g,
fat_per_100g, carbs_per_100g, fiber_per_100g,
vit_a_mcg, vit_c_mg, vit_d_mcg, vit_e_mg, vit_k_mcg,
vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_b6_mg, vit_b12_mcg, folate_mcg,
calcium_mg, iron_mg, magnesium_mg, zinc_mg, copper_mg,
selenium_mcg, potassium_mg, sodium_mg, phosphorus_mg, carotene_mcg, source)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29,?30,?31,?32)`,
[
p.name, p.name?.toLowerCase() ?? null,
p.brand ?? null, p.category ?? null, p.weight_g ?? null,
p.kcal_per_100g ?? null, p.protein_per_100g ?? null,
p.fat_per_100g ?? null, p.carbs_per_100g ?? null, p.fiber_per_100g ?? null,
p.vit_a_mcg ?? null, p.vit_c_mg ?? null, p.vit_d_mcg ?? null,
p.vit_e_mg ?? null, p.vit_k_mcg ?? null,
p.vit_b1_mg ?? null, p.vit_b2_mg ?? null, p.vit_b3_mg ?? null,
p.vit_b6_mg ?? null, p.vit_b12_mcg ?? null, p.folate_mcg ?? null,
p.calcium_mg ?? null, p.iron_mg ?? null, p.magnesium_mg ?? null,
p.zinc_mg ?? null, p.copper_mg ?? null,
p.selenium_mcg ?? null, p.potassium_mg ?? null, p.sodium_mg ?? null,
p.phosphorus_mg ?? null, p.carotene_mcg ?? null,
p.source ?? "manual",
]
);
const rows = await select("SELECT id FROM products ORDER BY id DESC LIMIT 1");
return rows[0]?.id;
}
// ---------------------------------------------------------------------------
// Cooking methods & Recipes
// ---------------------------------------------------------------------------
async function getCookingMethods() {
return select("SELECT * FROM cooking_methods ORDER BY id");
}
async function getCookingMethod(id) {
const rows = await select("SELECT * FROM cooking_methods WHERE id = ?1", [id]);
return rows.length ? rows[0] : null;
}
async function getRecipes() {
return select(
`SELECT r.*, cm.name as cooking_method_name
FROM recipes r
LEFT JOIN cooking_methods cm ON r.cooking_method_id = cm.id
ORDER BY r.created_at DESC`
);
}
async function getRecipe(id) {
const rows = await select(
`SELECT r.*, cm.name as cooking_method_name
FROM recipes r
LEFT JOIN cooking_methods cm ON r.cooking_method_id = cm.id
WHERE r.id = ?1`,
[id]
);
return rows.length ? rows[0] : null;
}
async function getRecipeIngredients(recipeId) {
return select(
`SELECT ri.*, p.name as product_name, p.kcal_per_100g,
p.protein_per_100g, p.fat_per_100g, p.carbs_per_100g,
p.fiber_per_100g,
p.vit_a_mcg, p.vit_c_mg, p.vit_d_mcg, p.vit_e_mg, p.vit_k_mcg,
p.vit_b1_mg, p.vit_b2_mg, p.vit_b3_mg, p.vit_b6_mg,
p.vit_b12_mcg, p.folate_mcg,
p.calcium_mg, p.iron_mg, p.magnesium_mg, p.zinc_mg, p.copper_mg,
p.selenium_mcg, p.potassium_mg, p.sodium_mg,
cm.name as cooking_method_name
FROM recipe_ingredients ri
LEFT JOIN products p ON ri.product_id = p.id AND ri.product_id > 0
LEFT JOIN cooking_methods cm ON ri.cooking_method_id = cm.id
WHERE ri.recipe_id = ?1
ORDER BY ri.id`,
[recipeId]
);
}
async function saveRecipe(name, cookingMethodId, servings, notes) {
await execute(
`INSERT INTO recipes (name, cooking_method_id, servings, notes, created_at)
VALUES (?1, ?2, ?3, ?4, datetime('now'))`,
[name, cookingMethodId ?? null, servings ?? 1, notes ?? null]
);
// lastInsertId from tauri-plugin-sql is unreliable; query the actual id
const rows = await select("SELECT id FROM recipes ORDER BY id DESC LIMIT 1");
return rows[0]?.id;
}
async function updateRecipe(id, name, cookingMethodId, servings, notes) {
await execute(
`UPDATE recipes SET name=?1, cooking_method_id=?2, servings=?3, notes=?4
WHERE id=?5`,
[name, cookingMethodId ?? null, servings ?? 1, notes ?? null, id]
);
}
async function deleteRecipe(id) {
await execute("DELETE FROM recipe_ingredients WHERE recipe_id = ?1", [id]);
await execute("DELETE FROM recipes WHERE id = ?1", [id]);
}
async function addRecipeIngredient(recipeId, productId, customName, weightG, cookingMethodId) {
const r = await execute(
`INSERT INTO recipe_ingredients (recipe_id, product_id, custom_name, amount_g, cooking_method_id)
VALUES (?1, ?2, ?3, ?4, ?5)`,
[recipeId, productId ?? 0, customName ?? null, weightG, cookingMethodId ?? null]
);
return r.lastInsertId;
}
async function deleteRecipeIngredient(id) {
await execute("DELETE FROM recipe_ingredients WHERE id = ?1", [id]);
}
async function clearRecipeIngredients(recipeId) {
await execute("DELETE FROM recipe_ingredients WHERE recipe_id = ?1", [recipeId]);
}
// Calculate nutrition for a recipe per serving.
// For each ingredient: (product nutrients / 100) * weight_g, then apply cooking loss.
// Cooking loss is taken from ingredient's own cooking_method_id, or recipe's default.
async function calculateRecipeNutrition(recipeId) {
const recipe = await getRecipe(recipeId);
if (!recipe) return null;
const ingredients = await getRecipeIngredients(recipeId);
const servings = recipe.servings || 1;
// Cache cooking methods to avoid repeated queries
const cmCache = {};
async function getCM(cmId) {
if (!cmId) return null;
if (cmCache[cmId]) return cmCache[cmId];
cmCache[cmId] = await getCookingMethod(cmId);
return cmCache[cmId];
}
// All micronutrient keys tracked in products table
const microKeys = [
"vit_a_mcg", "vit_c_mg", "vit_d_mcg", "vit_e_mg", "vit_k_mcg",
"vit_b1_mg", "vit_b2_mg", "vit_b3_mg", "vit_b6_mg",
"vit_b12_mcg", "folate_mcg",
"calcium_mg", "iron_mg", "magnesium_mg", "zinc_mg", "copper_mg",
"selenium_mcg", "potassium_mg", "sodium_mg",
];
const totals = { kcal: 0, protein: 0, fat: 0, carbs: 0, fiber: 0 };
for (const mk of microKeys) totals[mk] = 0;
for (const ing of ingredients) {
if (!ing.product_id || ing.product_id === 0) continue; // skip custom ingredients without product data
const factor = (ing.amount_g || 0) / 100;
// Raw nutrient values for this ingredient
const raw = {
kcal: (ing.kcal_per_100g ?? 0) * factor,
protein: (ing.protein_per_100g ?? 0) * factor,
fat: (ing.fat_per_100g ?? 0) * factor,
carbs: (ing.carbs_per_100g ?? 0) * factor,
fiber: (ing.fiber_per_100g ?? 0) * factor,
};
for (const mk of microKeys) raw[mk] = (ing[mk] ?? 0) * factor;
// Apply cooking loss: use ingredient's method, or recipe's default
const cmId = ing.cooking_method_id || recipe.cooking_method_id;
const cm = await getCM(cmId);
if (cm) {
// loss_pct is percentage lost (positive = loss, negative = gain like frying fat)
raw.kcal *= (1 - (cm.kcal_loss_pct ?? 0) / 100);
raw.protein *= (1 - (cm.protein_loss_pct ?? 0) / 100);
raw.fat *= (1 - (cm.fat_loss_pct ?? 0) / 100);
raw.carbs *= (1 - (cm.carbs_loss_pct ?? 0) / 100);
raw.fiber *= (1 - (cm.fiber_loss_pct ?? 0) / 100);
// Vitamin losses: cooking_methods table only has columns for a, c, b1, b2
raw.vit_a_mcg *= (1 - (cm.vit_a_loss_pct ?? 0) / 100);
raw.vit_c_mg *= (1 - (cm.vit_c_loss_pct ?? 0) / 100);
raw.vit_b1_mg *= (1 - (cm.vit_b1_loss_pct ?? 0) / 100);
raw.vit_b2_mg *= (1 - (cm.vit_b2_loss_pct ?? 0) / 100);
}
for (const k of Object.keys(totals)) {
totals[k] += raw[k];
}
}
// Per serving
const result = {
kcal: Math.round(totals.kcal / servings),
protein: Math.round(totals.protein / servings * 10) / 10,
fat: Math.round(totals.fat / servings * 10) / 10,
carbs: Math.round(totals.carbs / servings * 10) / 10,
fiber: Math.round(totals.fiber / servings * 10) / 10,
servings: servings,
totalWeight: ingredients.reduce((s, i) => s + (i.amount_g || 0), 0),
};
// Add all micronutrients per serving (rounded)
for (const mk of microKeys) {
result[mk] = Math.round(totals[mk] / servings * 100) / 100;
}
return result;
}
// Log a recipe as a food entry — stores calculated nutrition per serving
async function addFoodFromRecipe(date, time, recipeId) {
const nutr = await calculateRecipeNutrition(recipeId);
if (!nutr) return;
await execute(
`INSERT INTO food_log
(date, time, recipe_id, amount_g, kcal, protein_g, fat_g, carbs_g, fiber_g, note)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
[date, time, recipeId, Math.round(nutr.totalWeight / nutr.servings),
nutr.kcal, nutr.protein, nutr.fat, nutr.carbs, nutr.fiber, null]
);
}
// ---------------------------------------------------------------------------
// Food log
// ---------------------------------------------------------------------------
async function getFoodForDate(date) {
return select(
`SELECT fl.*, p.name as product_name, p.brand as product_brand,
COALESCE(p.nutrients_guessed, 0) as nutrients_guessed, p.source as product_source,
p.vit_a_mcg, p.vit_c_mg, p.vit_d_mcg, p.vit_e_mg, p.vit_k_mcg,
p.vit_b1_mg, p.vit_b2_mg, p.vit_b3_mg, p.vit_b6_mg,
p.vit_b12_mcg, p.folate_mcg,
p.calcium_mg, p.iron_mg, p.magnesium_mg, p.zinc_mg, p.copper_mg,
p.selenium_mcg, p.potassium_mg, p.sodium_mg,
p.phosphorus_mg, p.carotene_mcg,
r.name as recipe_name,
su.name as unit_name, su.name_short as unit_short
FROM food_log fl
LEFT JOIN products p ON fl.product_id = p.id
LEFT JOIN recipes r ON fl.recipe_id = r.id
LEFT JOIN serving_units su ON fl.unit_id = su.id
WHERE fl.date = ?1 ORDER BY fl.time ASC`,
[date]
);
}
async function addFoodManual(date, time, desc, kcal) {
await execute(
"INSERT INTO food_log (date, time, note, amount_g, kcal) VALUES (?1,?2,?3,0,?4)",
[date, time, desc, kcal ?? null]
);
}
async function addFoodEntry(date, time, productId, amountG, note, unitId, unitAmount) {
let kcal = null, protein = null, fat = null, carbs = null, fiber = null;
if (productId) {
const p = await getProduct(productId);
if (p && p.kcal_per_100g != null) {
const f = amountG / 100;
kcal = Math.round((p.kcal_per_100g ?? 0) * f);
protein = Math.round((p.protein_per_100g ?? 0) * f * 10) / 10;
fat = Math.round((p.fat_per_100g ?? 0) * f * 10) / 10;
carbs = Math.round((p.carbs_per_100g ?? 0) * f * 10) / 10;
fiber = Math.round((p.fiber_per_100g ?? 0) * f * 10) / 10;
}
}
await execute(
`INSERT INTO food_log
(date, time, product_id, amount_g, kcal, protein_g, fat_g, carbs_g, fiber_g, note, unit_id, unit_amount)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)`,
[date, time, productId ?? null, amountG,
kcal, protein, fat, carbs, fiber, note ?? null,
unitId ?? null, unitAmount ?? null]
);
}
async function updateFoodEntry(id, time, amountG, note, kcalOverride, unitId, unitAmount) {
// Recalculate nutrition from product if linked
const rows = await select("SELECT product_id FROM food_log WHERE id = ?1", [id]);
if (!rows.length) return;
const productId = rows[0].product_id;
let kcal = kcalOverride, protein = null, fat = null, carbs = null, fiber = null;
if (productId && kcalOverride == null) {
const p = await getProduct(productId);
if (p && p.kcal_per_100g != null) {
const f = amountG / 100;
kcal = Math.round((p.kcal_per_100g ?? 0) * f);
protein = Math.round((p.protein_per_100g ?? 0) * f * 10) / 10;
fat = Math.round((p.fat_per_100g ?? 0) * f * 10) / 10;
carbs = Math.round((p.carbs_per_100g ?? 0) * f * 10) / 10;
fiber = Math.round((p.fiber_per_100g ?? 0) * f * 10) / 10;
}
}
await execute(
`UPDATE food_log SET time=?1, amount_g=?2, note=?3,
kcal=?4, protein_g=?5, fat_g=?6, carbs_g=?7, fiber_g=?8,
unit_id=?9, unit_amount=?10
WHERE id=?11`,
[time, amountG, note ?? null, kcal, protein, fat, carbs, fiber,
unitId ?? null, unitAmount ?? null, id]
);
}
async function deleteFoodEntry(id) {
await execute("DELETE FROM food_log WHERE id = ?1", [id]);
}
// ---------------------------------------------------------------------------
// Activity log
// ---------------------------------------------------------------------------
async function getActivitiesForDate(date) {
return select("SELECT * FROM activity_log WHERE date = ?1 ORDER BY time ASC", [date]);
}
async function addActivity(date, time, type, desc, minutes, kcal) {
await execute(
"INSERT INTO activity_log (date, time, type, desc, minutes, kcal) VALUES (?1,?2,?3,?4,?5,?6)",
[date, time, type ?? "manual", desc, minutes ?? null, kcal ?? null]
);
}
async function deleteActivity(id) {
await execute("DELETE FROM activity_log WHERE id = ?1", [id]);
}
// ---------------------------------------------------------------------------
// Supplements
// ---------------------------------------------------------------------------
async function getSupplements() {
return select("SELECT * FROM supplements ORDER BY name COLLATE NOCASE");
}
async function addSupplement(s) {
const r = await execute(
`INSERT INTO supplements
(name, daily_dose_g, vit_a_mcg, vit_c_mg, vit_d_mcg, vit_e_mg, vit_k_mcg,
vit_b1_mg, vit_b2_mg, vit_b3_mg, vit_b6_mg, vit_b12_mcg, folate_mcg,
calcium_mg, iron_mg, magnesium_mg, zinc_mg, copper_mg, selenium_mcg,
potassium_mg, sodium_mg)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21)`,
[
s.name, s.daily_dose_g ?? null,
s.vit_a_mcg ?? null, s.vit_c_mg ?? null, s.vit_d_mcg ?? null,
s.vit_e_mg ?? null, s.vit_k_mcg ?? null,
s.vit_b1_mg ?? null, s.vit_b2_mg ?? null, s.vit_b3_mg ?? null,
s.vit_b6_mg ?? null, s.vit_b12_mcg ?? null, s.folate_mcg ?? null,
s.calcium_mg ?? null, s.iron_mg ?? null, s.magnesium_mg ?? null,
s.zinc_mg ?? null, s.copper_mg ?? null, s.selenium_mcg ?? null,
s.potassium_mg ?? null, s.sodium_mg ?? null,
]
);
return r.lastInsertId;
}
async function getSupplement(id) {
const rows = await select("SELECT * FROM supplements WHERE id = ?1", [id]);
return rows.length ? rows[0] : null;
}
async function deleteSupplement(id) {
await execute("DELETE FROM supplement_log WHERE supplement_id = ?1", [id]);
await execute("DELETE FROM supplements WHERE id = ?1", [id]);
}
async function logSupplement(date, time, supplementId, doseG) {
await execute(
"INSERT INTO supplement_log (date, time, supplement_id, dose_g) VALUES (?1,?2,?3,?4)",
[date, time, supplementId, doseG ?? null]
);
}
async function getSupplementLogForDate(date) {
return select(
`SELECT sl.*, s.name as supplement_name, s.daily_dose_g
FROM supplement_log sl
JOIN supplements s ON sl.supplement_id = s.id
WHERE sl.date = ?1 ORDER BY sl.time ASC`,
[date]
);
}
async function deleteSupplementLog(id) {
await execute("DELETE FROM supplement_log WHERE id = ?1", [id]);
}
async function getSupplementTotalsForDate(date) {
const rows = await select(
`SELECT
SUM(COALESCE(sl.dose_g,1) * s.vit_a_mcg) as vit_a_mcg,
SUM(COALESCE(sl.dose_g,1) * s.vit_c_mg) as vit_c_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_d_mcg) as vit_d_mcg,
SUM(COALESCE(sl.dose_g,1) * s.vit_e_mg) as vit_e_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_k_mcg) as vit_k_mcg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b1_mg) as vit_b1_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b2_mg) as vit_b2_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b3_mg) as vit_b3_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b5_mg) as vit_b5_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b6_mg) as vit_b6_mg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b7_mcg) as vit_b7_mcg,
SUM(COALESCE(sl.dose_g,1) * s.vit_b12_mcg) as vit_b12_mcg,
SUM(COALESCE(sl.dose_g,1) * s.folate_mcg) as folate_mcg,
SUM(COALESCE(sl.dose_g,1) * s.calcium_mg) as calcium_mg,
SUM(COALESCE(sl.dose_g,1) * s.iron_mg) as iron_mg,
SUM(COALESCE(sl.dose_g,1) * s.magnesium_mg) as magnesium_mg,
SUM(COALESCE(sl.dose_g,1) * s.zinc_mg) as zinc_mg,
SUM(COALESCE(sl.dose_g,1) * s.copper_mg) as copper_mg,
SUM(COALESCE(sl.dose_g,1) * s.selenium_mcg) as selenium_mcg,
SUM(COALESCE(sl.dose_g,1) * s.potassium_mg) as potassium_mg,
SUM(COALESCE(sl.dose_g,1) * s.sodium_mg) as sodium_mg,
SUM(COALESCE(sl.dose_g,1) * s.chromium_mg) as chromium_mg,
SUM(COALESCE(sl.dose_g,1) * s.manganese_mg) as manganese_mg,
SUM(COALESCE(sl.dose_g,1) * s.molybdenum_mcg) as molybdenum_mcg,
SUM(COALESCE(sl.dose_g,1) * s.iodine_mg) as iodine_mg,
SUM(COALESCE(sl.dose_g,1) * s.phosphorus_mg) as phosphorus_mg,
SUM(COALESCE(sl.dose_g,1) * s.carotene_mcg) as carotene_mcg,
SUM(COALESCE(sl.dose_g,1) * s.omega3_mg) as omega3_mg,
SUM(COALESCE(sl.dose_g,1) * s.epa_mg) as epa_mg,
SUM(COALESCE(sl.dose_g,1) * s.dha_mg) as dha_mg,
SUM(COALESCE(sl.dose_g,1) * s.omega6_mg) as omega6_mg,
SUM(COALESCE(sl.dose_g,1) * s.omega9_mg) as omega9_mg
FROM supplement_log sl
JOIN supplements s ON sl.supplement_id = s.id
WHERE sl.date = ?1`,
[date]
);
return rows[0] || {};
}
// ---------------------------------------------------------------------------
// Serving units
// ---------------------------------------------------------------------------
async function getServingUnits() {
return select("SELECT * FROM serving_units ORDER BY id");
}
async function getUnitsForProduct(productId) {
// Returns product-specific units (with their grams_per_unit) + universal units
return select(
`SELECT su.*, COALESCE(pu.grams_per_unit, su.grams_equivalent) as grams_per_unit
FROM serving_units su
LEFT JOIN product_units pu ON pu.unit_id = su.id AND pu.product_id = ?1
WHERE su.category = 'universal' OR pu.id IS NOT NULL
ORDER BY CASE WHEN pu.id IS NOT NULL THEN 0 ELSE 1 END, su.id`,
[productId]
);
}
// ---------------------------------------------------------------------------
// Analytics
// ---------------------------------------------------------------------------
async function getFoodTotalsForDate(date) {
const rows = await select(
`SELECT SUM(kcal) as kcal, SUM(protein_g) as protein_g,
SUM(fat_g) as fat_g, SUM(carbs_g) as carbs_g, SUM(fiber_g) as fiber_g
FROM food_log WHERE date = ?1`,
[date]
);
return rows[0] ?? { kcal: 0, protein_g: 0, fat_g: 0, carbs_g: 0, fiber_g: 0 };
}
async function getActivityTotalsForDate(date) {
const manual = await select(
"SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = ?1",
[date]
);
const gps = await select(
"SELECT SUM(total_kcal) as kcal FROM gps_sessions WHERE date = ?1 AND total_kcal IS NOT NULL",
[date]
);
const manualKcal = manual[0]?.kcal ?? 0;
const gpsKcal = gps[0]?.kcal ?? 0;
return { kcal: manualKcal + gpsKcal, minutes: manual[0]?.minutes ?? 0 };
}
async function getDailyFoodTotals(days) {
return select(
`SELECT date, SUM(kcal) as kcal, SUM(protein_g) as protein_g,
SUM(fat_g) as fat_g, SUM(carbs_g) as carbs_g
FROM food_log WHERE date >= date('now', ?1)
GROUP BY date ORDER BY date ASC`,
[`-${days ?? 30} days`]
);
}
async function getDailyActivityTotals(days) {
const offset = `-${days ?? 30} days`;
return select(
`SELECT date, SUM(kcal) as kcal FROM (
SELECT date, kcal FROM activity_log WHERE date >= date('now', ?1)
UNION ALL
SELECT date, total_kcal as kcal FROM gps_sessions WHERE date >= date('now', ?2) AND total_kcal IS NOT NULL
) GROUP BY date ORDER BY date ASC`,
[offset, offset]
);
}
// ---------------------------------------------------------------------------
// GPS sessions & points
// ---------------------------------------------------------------------------
async function createGpsSession(date, startTime, activityType) {
const id = Date.now();
await execute(
`INSERT INTO gps_sessions (id, date, start_time, activity_type, source)
VALUES (?1, ?2, ?3, ?4, 'recorded')`,
[id, date, startTime, activityType]
);
return id;
}
async function addGpsPoint(sessionId, ts, lat, lon, altGps, speedMs, seq) {
await execute(
`INSERT INTO gps_points (session_id, ts, lat, lon, altitude_gps_m, speed_ms, seq)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`,
[sessionId, ts, lat, lon, altGps ?? null, speedMs ?? null, seq ?? null]
);
}
async function finishGpsSession(id, endTime, distM, kcal, avgSpd, maxSpd, elevGain, elevLoss) {
await execute(
`UPDATE gps_sessions SET end_time=?1, total_distance_m=?2, total_kcal=?3,
avg_speed_kmh=?4, max_speed_kmh=?5, elevation_gain_m=?6, elevation_loss_m=?7
WHERE id=?8`,
[endTime, distM, kcal, avgSpd, maxSpd, elevGain, elevLoss, id]
);
}
async function getGpsSessions(limit) {
return select(
"SELECT * FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1",
[limit ?? 50]
);
}
async function getGpsSession(id) {
const rows = await select("SELECT * FROM gps_sessions WHERE id = ?1", [id]);
return rows.length ? rows[0] : null;
}
async function getGpsPoints(sessionId) {
return select("SELECT * FROM gps_points WHERE session_id = ?1 ORDER BY COALESCE(seq, 999999), ts ASC", [sessionId]);
}
async function deleteGpsSession(id) {
await execute("DELETE FROM gps_points WHERE session_id = ?1", [id]);
await execute("DELETE FROM gps_sessions WHERE id = ?1", [id]);
}
async function importGpxSession(session, points) {
await execute(
`INSERT INTO gps_sessions (date, start_time, end_time, activity_type,
total_distance_m, total_kcal, avg_speed_kmh, max_speed_kmh,
elevation_gain_m, elevation_loss_m, source)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)`,
[session.date, session.start_time, session.end_time, session.activity_type,
session.total_distance_m, session.total_kcal, session.avg_speed_kmh,
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
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",
[session.date, session.start_time, session.source ?? "gpx_import"]
);
const sid = rows[0].id;
// Batch insert points — build multi-row INSERT to avoid 1 IPC per point
const BATCH = 50;
for (let i = 0; i < points.length; i += BATCH) {
const chunk = points.slice(i, i + BATCH);
const placeholders = [];
const vals = [];
let idx = 1;
for (const p of chunk) {
placeholders.push(`(?${idx},?${idx+1},?${idx+2},?${idx+3},?${idx+4},?${idx+5},?${idx+6})`);
vals.push(sid, p.ts, p.lat, p.lon, p.altitude ?? null, p.speed ?? null, p.seq ?? null);
idx += 7;
}
await execute(
`INSERT INTO gps_points (session_id, ts, lat, lon, altitude_gps_m, speed_ms, seq) VALUES ${placeholders.join(",")}`,
vals
);
}
return sid;
}
// ---------------------------------------------------------------------------
// Bikes
// ---------------------------------------------------------------------------
async function getBikes() {
return select("SELECT * FROM bikes ORDER BY is_active DESC, name");
}
async function getBike(id) {
const rows = await select("SELECT * FROM bikes WHERE id = ?1", [id]);
return rows.length ? rows[0] : null;
}
async function getActiveBike() {
const rows = await select("SELECT * FROM bikes WHERE is_active = 1 LIMIT 1");
return rows.length ? rows[0] : null;
}
async function saveBike(data) {
if (data.is_active) {
await execute("UPDATE bikes SET is_active = 0 WHERE is_active = 1");
}
const r = await execute(
`INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth,
cog_teeth, gear_count, surface_type, notes, is_active, tire_pressure_bar)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
[data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26,
data.chainring_teeth ?? null, data.cog_teeth ?? null,
data.gear_count ?? null, data.surface_type ?? "mixed",
data.notes ?? null, data.is_active ? 1 : 0,
data.tire_pressure_bar ?? null]
);
return r.lastInsertId;
}
async function updateBike(id, data) {
if (data.is_active) {
await execute("UPDATE bikes SET is_active = 0 WHERE is_active = 1");
}
await execute(
`UPDATE bikes SET name=?1, weight_kg=?2, wheel_diameter_inch=?3,
chainring_teeth=?4, cog_teeth=?5, gear_count=?6, surface_type=?7,
notes=?8, is_active=?9, tire_pressure_bar=?10 WHERE id=?11`,
[data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26,
data.chainring_teeth ?? null, data.cog_teeth ?? null,
data.gear_count ?? null, data.surface_type ?? "mixed",
data.notes ?? null, data.is_active ? 1 : 0,
data.tire_pressure_bar ?? null, id]
);
}
async function deleteBike(id) {
await execute("DELETE FROM bikes WHERE id = ?1", [id]);
}
async function updateSessionBike(sessionId, bikeId) {
await execute("UPDATE gps_sessions SET bike_id = ?1 WHERE id = ?2", [bikeId, sessionId]);
}
// ---------------------------------------------------------------------------
// Bike maintenance log
// ---------------------------------------------------------------------------
async function getBikeMaintenanceLog(bikeId) {
return select(
"SELECT * FROM bike_maintenance WHERE bike_id = ?1 ORDER BY date DESC",
[bikeId]
);
}
async function addBikeMaintenance(bikeId, date, description, mileageKm, nextServiceKm, notes) {
const r = await execute(
`INSERT INTO bike_maintenance (bike_id, date, description, mileage_km, next_service_km, notes)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)`,
[bikeId, date, description, mileageKm ?? null, nextServiceKm ?? null, notes ?? null]
);
return r.lastInsertId;
}
async function deleteBikeMaintenance(id) {
await execute("DELETE FROM bike_maintenance WHERE id = ?1", [id]);
}
async function getBikeTotalMileage(bikeId) {
const rows = await select(
"SELECT SUM(total_distance_m) as total FROM gps_sessions WHERE bike_id = ?1 AND total_distance_m IS NOT NULL",
[bikeId]
);
const totalM = rows[0]?.total ?? 0;
return Math.round(totalM / 100) / 10; // km, 1 decimal
}
async function getBikeMaintenanceDue(bikeId) {
const mileage = await getBikeTotalMileage(bikeId);
const all = await getBikeMaintenanceLog(bikeId);
return all.filter(e => e.next_service_km != null && mileage >= e.next_service_km);
}
// ---------------------------------------------------------------------------
// Goal markers
// ---------------------------------------------------------------------------
async function getGoalMarkers() {
return select("SELECT * FROM goal_markers ORDER BY created_at DESC");
}
async function addGoalMarker(name, lat, lon, notes) {
const r = await execute(
`INSERT INTO goal_markers (name, lat, lon, notes) VALUES (?1, ?2, ?3, ?4)`,
[name, lat, lon, notes ?? null]
);
return r.lastInsertId;
}
async function deleteGoalMarker(id) {
await execute("DELETE FROM goal_markers WHERE id = ?1", [id]);
}
// ---------------------------------------------------------------------------
// Food cost helper — most frequently eaten foods in last 7 days
// ---------------------------------------------------------------------------
async function getFrequentFoods(days) {
return select(
`SELECT product_name, kcal, amount_g,
CASE WHEN amount_g > 0 THEN ROUND(kcal * 100.0 / amount_g) ELSE kcal END as kcal_per_100g
FROM (
SELECT p.name as product_name, fl.kcal, fl.amount_g,
COUNT(*) as cnt
FROM food_log fl
JOIN products p ON fl.product_id = p.id
WHERE fl.date >= date('now', ?1)
AND fl.kcal > 0 AND fl.amount_g > 0
GROUP BY p.name
ORDER BY cnt DESC
LIMIT 5
)`,
[`-${days ?? 7} days`]
);
}
// ---------------------------------------------------------------------------
// Closest approach of a session to a goal marker
// Returns {minDist, closestIdx} — computed in JS since SQLite can't do haversine
// ---------------------------------------------------------------------------
async function getGoalProgress(markerId, sessionId) {
// Fetch all sessions that got close to this marker, ordered by date
// We actually compute this in JS, but store the result concept here
// This function just returns all sessions with their closest approach to a marker
const sessions = await select(
`SELECT gs.id, gs.date, gs.start_time, gs.total_distance_m
FROM gps_sessions gs
WHERE gs.id != ?1
ORDER BY gs.date DESC, gs.start_time DESC
LIMIT 20`,
[sessionId]
);
return sessions;
}
// Expose as globals — no ES module system, loaded via <script> tags.
// main.js accesses these as window.DB.xxx
window.DB = {};
window.DB.getProfileValue = getProfileValue;
window.DB.setProfileValue = setProfileValue;
window.DB.getFullProfile = getFullProfile;
window.DB.addWeight = addWeight;
window.DB.deleteWeight = deleteWeight;
window.DB.getWeightsForDate = getWeightsForDate;
window.DB.getLatestWeight = getLatestWeight;
window.DB.getRecentWeights = getRecentWeights;
window.DB.getSleepForDate = getSleepForDate;
window.DB.saveSleep = saveSleep;
window.DB.searchProducts = searchProducts;
window.DB.getProduct = getProduct;
window.DB.addProduct = addProduct;
window.DB.getFoodForDate = getFoodForDate;
window.DB.addFoodManual = addFoodManual;
window.DB.addFoodEntry = addFoodEntry;
window.DB.updateFoodEntry = updateFoodEntry;
window.DB.deleteFoodEntry = deleteFoodEntry;
window.DB.getActivitiesForDate = getActivitiesForDate;
window.DB.addActivity = addActivity;
window.DB.deleteActivity = deleteActivity;
window.DB.getSupplements = getSupplements;
window.DB.getSupplement = getSupplement;
window.DB.addSupplement = addSupplement;
window.DB.deleteSupplement = deleteSupplement;
window.DB.logSupplement = logSupplement;
window.DB.getSupplementLogForDate = getSupplementLogForDate;
window.DB.deleteSupplementLog = deleteSupplementLog;
window.DB.getSupplementTotalsForDate = getSupplementTotalsForDate;
window.DB.getFoodTotalsForDate = getFoodTotalsForDate;
window.DB.getActivityTotalsForDate = getActivityTotalsForDate;
window.DB.getServingUnits = getServingUnits;
window.DB.getUnitsForProduct = getUnitsForProduct;
window.DB.getDailyFoodTotals = getDailyFoodTotals;
window.DB.getDailyActivityTotals = getDailyActivityTotals;
window.DB.getCookingMethods = getCookingMethods;
window.DB.getCookingMethod = getCookingMethod;
window.DB.getRecipes = getRecipes;
window.DB.getRecipe = getRecipe;
window.DB.getRecipeIngredients = getRecipeIngredients;
window.DB.saveRecipe = saveRecipe;
window.DB.updateRecipe = updateRecipe;
window.DB.deleteRecipe = deleteRecipe;
window.DB.addRecipeIngredient = addRecipeIngredient;
window.DB.deleteRecipeIngredient = deleteRecipeIngredient;
window.DB.clearRecipeIngredients = clearRecipeIngredients;
window.DB.calculateRecipeNutrition = calculateRecipeNutrition;
window.DB.addFoodFromRecipe = addFoodFromRecipe;
window.DB.createGpsSession = createGpsSession;
window.DB.addGpsPoint = addGpsPoint;
window.DB.finishGpsSession = finishGpsSession;
window.DB.getGpsSessions = getGpsSessions;
window.DB.getGpsSession = getGpsSession;
window.DB.getGpsPoints = getGpsPoints;
window.DB.deleteGpsSession = deleteGpsSession;
window.DB.importGpxSession = importGpxSession;
window.DB.getBikes = getBikes;
window.DB.getBike = getBike;
window.DB.getActiveBike = getActiveBike;
window.DB.saveBike = saveBike;
window.DB.updateBike = updateBike;
window.DB.deleteBike = deleteBike;
window.DB.updateSessionBike = updateSessionBike;
window.DB.getBikeMaintenanceLog = getBikeMaintenanceLog;
window.DB.addBikeMaintenance = addBikeMaintenance;
window.DB.deleteBikeMaintenance = deleteBikeMaintenance;
window.DB.getBikeTotalMileage = getBikeTotalMileage;
window.DB.getBikeMaintenanceDue = getBikeMaintenanceDue;
window.DB.getGoalMarkers = getGoalMarkers;
window.DB.addGoalMarker = addGoalMarker;
window.DB.deleteGoalMarker = deleteGoalMarker;
window.DB.getFrequentFoods = getFrequentFoods;
window.DB.getGoalProgress = getGoalProgress;
// JS-side migrations — safe for any DB state (checks before altering)
async function runMigrations() {
// Batch column checks — one PRAGMA per table instead of N separate IPC calls
const productCols = (await select("PRAGMA table_info(products)")).map(r => r.name);
const addProdCol = async (col, type) => { if (!productCols.includes(col)) await execute(`ALTER TABLE products ADD COLUMN ${col} ${type}`); };
await addProdCol("composition", "TEXT");
await addProdCol("name_lower", "TEXT");
await addProdCol("nutrients_guessed", "INTEGER DEFAULT 0");
await addProdCol("user_edited", "INTEGER DEFAULT 0");
const gpsCols = (await select("PRAGMA table_info(gps_points)")).map(r => r.name);
if (!gpsCols.includes("seq")) await execute("ALTER TABLE gps_points ADD COLUMN seq INTEGER");
// --- Cooking methods table ---
await execute(`CREATE TABLE IF NOT EXISTS cooking_methods (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
protein_loss_pct REAL DEFAULT 0,
fat_loss_pct REAL DEFAULT 0,
carbs_loss_pct REAL DEFAULT 0,
fiber_loss_pct REAL DEFAULT 0,
vit_a_loss_pct REAL DEFAULT 0,
vit_c_loss_pct REAL DEFAULT 0,
vit_b1_loss_pct REAL DEFAULT 0,
vit_b2_loss_pct REAL DEFAULT 0,
kcal_loss_pct REAL DEFAULT 0
)`);
// Seed cooking methods if empty
const cmCount = await select("SELECT COUNT(*) as cnt FROM cooking_methods");
if (cmCount[0].cnt === 0) {
const methods = [
[1, "Без обработки", 0, 0, 0, 0, 0, 0, 0, 0, 0],
[2, "Варка", 2, 5, 3, 0, 0, 50, 30, 10, 0],
[3, "Жарка", 5, -10, 3, 0, 0, 60, 35, 0, 0],
[4, "Тушение", 2, 10, 2, 0, 0, 40, 25, 0, 0],
[5, "Запекание", 3, 8, 2, 0, 0, 45, 20, 0, 0],
[6, "На пару", 1, 2, 1, 0, 0, 25, 15, 0, 0],
];
for (const m of methods) {
await execute(
`INSERT INTO cooking_methods (id, name, protein_loss_pct, fat_loss_pct,
carbs_loss_pct, fiber_loss_pct, vit_a_loss_pct, vit_c_loss_pct,
vit_b1_loss_pct, vit_b2_loss_pct, kcal_loss_pct)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)`,
m
);
}
console.log("[db.js] Seeded 6 cooking methods");
}
// --- Recipes + ingredients: batch column checks ---
const recipeCols = (await select("PRAGMA table_info(recipes)")).map(r => r.name);
if (!recipeCols.includes("cooking_method_id")) await execute("ALTER TABLE recipes ADD COLUMN cooking_method_id INTEGER");
if (!recipeCols.includes("created_at")) await execute("ALTER TABLE recipes ADD COLUMN created_at TEXT DEFAULT (datetime('now'))");
const riCols = (await select("PRAGMA table_info(recipe_ingredients)")).map(r => r.name);
if (!riCols.includes("custom_name")) await execute("ALTER TABLE recipe_ingredients ADD COLUMN custom_name TEXT");
if (!riCols.includes("cooking_method_id")) await execute("ALTER TABLE recipe_ingredients ADD COLUMN cooking_method_id INTEGER");
// --- Serving units table ---
await execute(`CREATE TABLE IF NOT EXISTS serving_units (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
name_short TEXT,
grams_equivalent REAL,
category TEXT DEFAULT 'universal'
)`);
// --- Product units junction table ---
await execute(`CREATE TABLE IF NOT EXISTS product_units (
id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL REFERENCES products(id),
unit_id INTEGER NOT NULL REFERENCES serving_units(id),
grams_per_unit REAL NOT NULL,
UNIQUE(product_id, unit_id)
)`);
// --- Add unit columns to food_log ---
const flCols = (await select("PRAGMA table_info(food_log)")).map(r => r.name);
if (!flCols.includes("unit_id")) await execute("ALTER TABLE food_log ADD COLUMN unit_id INTEGER");
if (!flCols.includes("unit_amount")) await execute("ALTER TABLE food_log ADD COLUMN unit_amount REAL");
// Seed serving_units if empty
const suCnt = await select("SELECT COUNT(*) as cnt FROM serving_units");
if (suCnt[0].cnt === 0) {
const units = [
[1, "шт", "шт", null, "product_specific"],
[2, "ст.л.", "ст.л.", 15, "universal"],
[3, "ч.л.", "ч.л.", 5, "universal"],
[4, "стакан", "стак.", 250, "universal"],
[5, "мл", "мл", 1, "universal"],
];
for (const u of units) {
await execute(
`INSERT INTO serving_units (id, name, name_short, grams_equivalent, category)
VALUES (?1, ?2, ?3, ?4, ?5)`, u
);
}
console.log("[db.js] Seeded 5 serving units");
}
// product_units: user adds per-product unit mappings manually via UI.
// No auto-seeding — product names too varied for reliable matching.
// --- Bikes table ---
await execute(`CREATE TABLE IF NOT EXISTS bikes (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
weight_kg REAL,
wheel_diameter_inch REAL,
chainring_teeth INTEGER,
cog_teeth INTEGER,
gear_count INTEGER,
surface_type TEXT DEFAULT 'mixed',
notes TEXT,
is_active INTEGER DEFAULT 0
)`);
// Add bike_id column to gps_sessions
const gsCols = (await select("PRAGMA table_info(gps_sessions)")).map(r => r.name);
if (!gsCols.includes("bike_id")) await execute("ALTER TABLE gps_sessions ADD COLUMN bike_id INTEGER");
// Add tire_pressure_bar to bikes (for Crr correction by tire pressure)
const bikeCols = (await select("PRAGMA table_info(bikes)")).map(r => r.name);
if (!bikeCols.includes("tire_pressure_bar")) await execute("ALTER TABLE bikes ADD COLUMN tire_pressure_bar REAL");
// --- Bike maintenance log table ---
await execute(`CREATE TABLE IF NOT EXISTS bike_maintenance (
id INTEGER PRIMARY KEY,
bike_id INTEGER NOT NULL REFERENCES bikes(id) ON DELETE CASCADE,
date TEXT NOT NULL,
description TEXT NOT NULL,
mileage_km REAL,
next_service_km REAL,
notes TEXT
)`);
// Seed default bike if table is empty
const bikeCnt = await select("SELECT COUNT(*) as cnt FROM bikes");
if (bikeCnt[0].cnt === 0) {
await execute(
`INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth,
cog_teeth, surface_type, is_active)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`,
["Мой велосипед", 12, 26, 42, 18, "mixed", 1]
);
console.log("[db.js] Seeded default bike");
}
// --- Goal markers table ---
await execute(`CREATE TABLE IF NOT EXISTS goal_markers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL,
notes TEXT,
created_at TEXT DEFAULT (datetime('now'))
)`);
// --- Supplement columns migration: add missing micronutrient columns ---
const suppCols = (await select("PRAGMA table_info(supplements)")).map(r => r.name);
const addSuppCol = async (col, type) => { if (!suppCols.includes(col)) await execute(`ALTER TABLE supplements ADD COLUMN ${col} ${type}`); };
await addSuppCol("vit_b5_mg", "REAL");
await addSuppCol("vit_b7_mcg", "REAL"); // biotin
await addSuppCol("chromium_mg", "REAL");
await addSuppCol("manganese_mg", "REAL");
await addSuppCol("molybdenum_mcg", "REAL");
await addSuppCol("iodine_mg", "REAL");
await addSuppCol("phosphorus_mg", "REAL");
await addSuppCol("silicon_mg", "REAL"); // кремний — no RDI
await addSuppCol("vanadium_mg", "REAL"); // ванадий — no RDI
await addSuppCol("lutein_mg", "REAL"); // лютеин — no RDI
await addSuppCol("lycopene_mg", "REAL"); // ликопин — no RDI
await addSuppCol("carotene_mcg", "REAL"); // бета-каротин
await addSuppCol("fish_oil_mg", "REAL"); // рыбий жир (общий объём)
await addSuppCol("omega3_mg", "REAL"); // омега-3 общая
await addSuppCol("epa_mg", "REAL"); // EPA (эйкозапентаеновая)
await addSuppCol("dha_mg", "REAL"); // DHA (докозагексаеновая)
await addSuppCol("omega6_mg", "REAL"); // омега-6
await addSuppCol("omega9_mg", "REAL"); // омега-9
console.log("[db.js] recipe migrations done, checking seq repair...");
// Repair old tracks: assign seq to points where it's NULL, ordered by timestamp.
// Only runs if there are NULL-seq points (should be one-time after migration).
try {
const nullSeqSessions = await select(
"SELECT DISTINCT session_id FROM gps_points WHERE seq IS NULL"
);
if (nullSeqSessions.length > 0) {
console.log(`[db.js] Repairing seq for ${nullSeqSessions.length} sessions...`);
for (const row of nullSeqSessions) {
const pts = await select(
"SELECT rowid, ts FROM gps_points WHERE session_id = ?1 ORDER BY ts ASC, rowid ASC",
[row.session_id]
);
for (let i = 0; i < pts.length; i++) {
await execute("UPDATE gps_points SET seq = ?1 WHERE rowid = ?2", [i + 1, pts[i].rowid]);
}
console.log(`[db.js] Repaired seq for session ${row.session_id}: ${pts.length} points`);
}
}
} catch (e) { console.error("[db.js] seq repair error:", e); }
}
window.DB._migrationsReady = runMigrations().then(() => {
console.log("[db.js] migrations done");
}).catch(e => {
console.error("[db.js] migration error:", e);
});
console.log("[db.js] window.DB ready, keys:", Object.keys(window.DB).length);