Settings: - Aero drag (CdA), air density, pedal efficiency now in GPS config - Human-readable names: "Аэросопротивление", "Плотность воздуха", "КПД педалирования" - Saved to localStorage, used in computeStats Bug fixes: - Recipe detail: overlay.remove without popHandler cleanup → now uses history.back() - weightKg parseFloat in startRecording and importGPX (string "88" bug) - profile.weight_kg stored as number not string - getDailyActivityTotals: separate params for UNION ALL subqueries Docs: - PROJECT_MAP.md — full project structure, DB schema, screens, formulas - README.md — overview, build instructions, data sources
973 lines
37 KiB
JavaScript
973 lines
37 KiB
JavaScript
// 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
|
||
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) {
|
||
const r = await execute(
|
||
`INSERT INTO products
|
||
(name, 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, 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)`,
|
||
[
|
||
p.name, 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.source ?? "manual",
|
||
]
|
||
);
|
||
return r.lastInsertId;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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_b1_mg,
|
||
p.vit_b2_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) {
|
||
const r = 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]
|
||
);
|
||
return r.lastInsertId;
|
||
}
|
||
|
||
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];
|
||
}
|
||
|
||
const totals = {
|
||
kcal: 0, protein: 0, fat: 0, carbs: 0, fiber: 0,
|
||
vit_a_mcg: 0, vit_c_mg: 0, vit_b1_mg: 0, vit_b2_mg: 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,
|
||
vit_a_mcg: (ing.vit_a_mcg ?? 0) * factor,
|
||
vit_c_mg: (ing.vit_c_mg ?? 0) * factor,
|
||
vit_b1_mg: (ing.vit_b1_mg ?? 0) * factor,
|
||
vit_b2_mg: (ing.vit_b2_mg ?? 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);
|
||
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
|
||
return {
|
||
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,
|
||
vit_a_mcg: Math.round(totals.vit_a_mcg / servings * 10) / 10,
|
||
vit_c_mg: Math.round(totals.vit_c_mg / servings * 10) / 10,
|
||
vit_b1_mg: Math.round(totals.vit_b1_mg / servings * 1000) / 1000,
|
||
vit_b2_mg: Math.round(totals.vit_b2_mg / servings * 1000) / 1000,
|
||
servings: servings,
|
||
totalWeight: ingredients.reduce((s, i) => s + (i.amount_g || 0), 0),
|
||
};
|
||
}
|
||
|
||
// 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, 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_b6_mg) as vit_b6_mg,
|
||
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
|
||
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)
|
||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`,
|
||
[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]
|
||
);
|
||
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 WHERE id=?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, 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]);
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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");
|
||
|
||
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");
|
||
|
||
// 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");
|
||
}
|
||
|
||
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);
|