From 0bafb2b317e9ac60217910a78ee56c2e9bc9564f Mon Sep 17 00:00:00 2001 From: 4aiman Date: Tue, 2 Jun 2026 18:39:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A3=D0=9F=20norms=20with=20star,=20nut?= =?UTF-8?q?rient=20half-life=20table,=20recipe=20nutrients=20in=20daily=20?= =?UTF-8?q?strip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RDI/АУП: - rdi_values.norm_type column: 'rdi' or 'aup' - АУП values seeded: omega3 1000mg, EPA/DHA 250mg, lutein 5mg, lycopene 5mg, silicon 30mg, vanadium 0.04mg (all 3 standards) - Nutrient chips show * after label for АУП norms - Supplement form placeholders show RDI/АУП values (with * for АУП) Half-life table: - nutrient_halflife: nutrient_key, min/max/avg days, source - 32 nutrients seeded with whole-body biological half-life data - Sources: PubMed, ICRP, Performance Lab, clinical studies Recipe nutrients in daily strip: - recipes.cached_nutrients TEXT column (JSON) - cacheRecipeNutrients() computes and stores per-serving nutrients - Called on recipe save in editor - calcDailyNutrients() now reads cached_nutrients for recipe food entries - getFoodForDate() JOINs r.cached_nutrients BMR.loadRDI() now loads norm_type alongside values. BMR.RDI_TYPES exposed for star rendering. --- frontend/js/bmr.js | 15 ++++++-- frontend/js/db.js | 94 ++++++++++++++++++++++++++++++++++++++++++++- frontend/js/main.js | 21 ++++++++-- 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/frontend/js/bmr.js b/frontend/js/bmr.js index e9d9d9e..b580627 100644 --- a/frontend/js/bmr.js +++ b/frontend/js/bmr.js @@ -29,6 +29,7 @@ function estimateRealBmr(weights, foodTotals) { // RDI loaded from DB (rdi_standards + rdi_values tables). // Fallback hardcoded RU values used until DB is ready. +let RDI_TYPES = {}; // { nutrient_key: "rdi" | "aup" } let RDI = { vit_a_mcg: 800, vit_c_mg: 60, vit_e_mg: 10, vit_b1_mg: 1.4, vit_b2_mg: 1.6, vit_b3_mg: 18, vit_b5_mg: 6, vit_b6_mg: 2, vit_b7_mcg: 50, folate_mcg: 200, @@ -49,14 +50,20 @@ async function loadRDI(standardCode) { if (!std.length) return; const vals = await window.__TAURI__.core.invoke("plugin:sql|select", { db: "sqlite:baldav.db", - query: "SELECT nutrient_key, value FROM rdi_values WHERE standard_id = ?1", + query: "SELECT nutrient_key, value, norm_type FROM rdi_values WHERE standard_id = ?1", values: [std[0].id], }); const loaded = {}; - for (const v of vals) loaded[v.nutrient_key] = v.value; + RDI_TYPES = {}; + for (const v of vals) { + loaded[v.nutrient_key] = v.value; + RDI_TYPES[v.nutrient_key] = v.norm_type || "rdi"; + } if (Object.keys(loaded).length > 0) RDI = loaded; } catch (e) { console.error("[BMR] loadRDI error:", e); } } -window.BMR = { calculateBmr, getAge, estimateRealBmr, RDI, loadRDI, - get rdi() { return RDI; } }; +window.BMR = { calculateBmr, getAge, estimateRealBmr, loadRDI, + get RDI() { return RDI; }, + get RDI_TYPES() { return RDI_TYPES; }, +}; diff --git a/frontend/js/db.js b/frontend/js/db.js index 14cf8d6..ed75436 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -376,7 +376,7 @@ async function getFoodForDate(date) { 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, + r.name as recipe_name, r.cached_nutrients, 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 @@ -944,6 +944,14 @@ window.DB.addRecipeIngredient = addRecipeIngredient; window.DB.deleteRecipeIngredient = deleteRecipeIngredient; window.DB.clearRecipeIngredients = clearRecipeIngredients; window.DB.calculateRecipeNutrition = calculateRecipeNutrition; + +async function cacheRecipeNutrients(recipeId) { + const nutr = await calculateRecipeNutrition(recipeId); + if (!nutr) return; + await execute("UPDATE recipes SET cached_nutrients = ?1 WHERE id = ?2", + [JSON.stringify(nutr), recipeId]); +} +window.DB.cacheRecipeNutrients = cacheRecipeNutrients; window.DB.addFoodFromRecipe = addFoodFromRecipe; window.DB.createGpsSession = createGpsSession; window.DB.addGpsPoint = addGpsPoint; @@ -1213,6 +1221,90 @@ async function runMigrations() { console.log("[db.js] Seeded 3 RDI standards (RU/EU/US)"); } + // Add norm_type to rdi_values (rdi vs aup) + const rdiValCols = (await select("PRAGMA table_info(rdi_values)")).map(r => r.name); + if (!rdiValCols.includes("norm_type")) { + await execute("ALTER TABLE rdi_values ADD COLUMN norm_type TEXT DEFAULT 'rdi'"); + } + + // Seed АУП values (one-time, check if omega3 already exists) + const hasOmega3 = await select("SELECT COUNT(*) as cnt FROM rdi_values WHERE nutrient_key = 'omega3_mg'"); + if (hasOmega3[0].cnt === 0) { + const aupData = { + // key: [RU, EU, US] — all marked as 'aup' + omega3_mg: [1000, 250, 1600], + epa_mg: [250, 250, 500], + dha_mg: [250, 250, 500], + lutein_mg: [5, 10, 6], + lycopene_mg: [5, 5, 5], + silicon_mg: [30, 30, 30], + vanadium_mg: [0.04, 0.04, 0.04], + }; + for (const [key, vals] of Object.entries(aupData)) { + for (let i = 0; i < 3; i++) { + await execute("INSERT OR IGNORE INTO rdi_values (standard_id, nutrient_key, value, norm_type) VALUES (?1, ?2, ?3, 'aup')", [i + 1, key, vals[i]]); + } + } + console.log("[db.js] Seeded АУП values for omega/lutein/lycopene/silicon/vanadium"); + } + + // Nutrient half-life table (whole-body, in days) + await execute(`CREATE TABLE IF NOT EXISTS nutrient_halflife ( + nutrient_key TEXT PRIMARY KEY, + halflife_min_days REAL, + halflife_max_days REAL, + halflife_avg_days REAL, + source TEXT + )`); + + const hlCount = await select("SELECT COUNT(*) as cnt FROM nutrient_halflife"); + if (hlCount[0].cnt === 0) { + const hl = [ + ["vit_a_mcg", 50, 180, 115, "fat-soluble, liver stores"], + ["vit_c_mg", 10, 20, 15, "Performance Lab review"], + ["vit_d_mcg", 30, 90, 60, "fat tissue, PMC4207933"], + ["vit_e_mg", 2, 5, 3.5, "ZIO review"], + ["vit_k_mcg", 1, 3, 1.5, "K1 pool turnover"], + ["vit_b1_mg", 9, 18, 13.5, "Ariaey-Nejad 1970"], + ["vit_b2_mg", 2, 7, 4, "limited storage"], + ["vit_b3_mg", 0.5, 2, 1, "rapid excretion"], + ["vit_b5_mg", 0.5, 2, 1, "rapid excretion"], + ["vit_b6_mg", 15, 20, 17.5, "muscle stores"], + ["vit_b7_mcg", 1, 3, 2, "tissue stores > plasma"], + ["folate_mcg", 60, 120, 100, "RBC folate ~8 weeks"], + ["vit_b12_mcg", 200, 770, 400, "liver, PubMed 14010994"], + ["calcium_mg", 1, 10, 5, "fast exchangeable pool"], + ["magnesium_mg", 30, 60, 42, "~1000 hours"], + ["iron_mg", 2000, 5000, 3650, "~10 years, no excretion"], + ["zinc_mg", 12, 30, 20, "fast pool"], + ["copper_mg", 13, 33, 23, "NCBI/PMC review"], + ["selenium_mcg", 50, 150, 100, "Griffith 1976"], + ["iodine_mg", 40, 80, 66, "ICRP/measured"], + ["manganese_mg", 20, 50, 37, "clinical data"], + ["molybdenum_mcg", 1, 3, 2, "plasma ~24-51h"], + ["chromium_mg", 300, 900, 616, "ICRP"], + ["phosphorus_mg", 14, 60, 30, "soft tissue pool"], + ["potassium_mg", 10, 28, 16, "K-42 tracer study"], + ["sodium_mg", 7, 15, 11, "ICRP"], + ["omega3_mg", 30, 90, 60, "fat-soluble stored"], + ["epa_mg", 30, 90, 60, "same pool as omega3"], + ["dha_mg", 30, 90, 60, "same pool as omega3"], + ["carotene_mcg", 5, 30, 15, "fat tissue"], + ["lutein_mg", 10, 30, 20, "macular pigment"], + ["lycopene_mg", 14, 28, 21, "fat tissue"], + ]; + for (const [key, min, max, avg, src] of hl) { + await execute("INSERT INTO nutrient_halflife VALUES (?1,?2,?3,?4,?5)", [key, min, max, avg, src]); + } + console.log("[db.js] Seeded nutrient half-life data (32 nutrients)"); + } + + // Recipe cached nutrients columns + const recipeCols2 = (await select("PRAGMA table_info(recipes)")).map(r => r.name); + if (!recipeCols2.includes("cached_nutrients")) { + await execute("ALTER TABLE recipes ADD COLUMN cached_nutrients TEXT"); + } + console.log("[db.js] recipe migrations done, checking seq repair..."); // Repair old tracks: assign seq to points where it's NULL, ordered by timestamp. diff --git a/frontend/js/main.js b/frontend/js/main.js index c49c2e2..235984b 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1820,6 +1820,8 @@ async function showRecipeEditorModal(recipeId) { rid, ing.product_id, ing.custom_name, ing.weight_g, ing.cooking_method_id ); } + // Cache computed nutrients on the recipe for daily tracking + await DB.cacheRecipeNutrients(rid); renderScreen("recipes"); }); @@ -3593,7 +3595,7 @@ function showEditSupplementModal(existing) { if (displayVal && f.mult) displayVal = Math.round(displayVal / f.mult * 1000) / 1000; // mcg→mg for display return `
- +
`; } ).join(""); @@ -4256,6 +4258,17 @@ function calcDailyNutrients(food, suppTotals) { } } + // From recipes (cached_nutrients JSON in recipes table) + for (const f of food) { + if (!f.recipe_id || !f.cached_nutrients) continue; + try { + const rn = typeof f.cached_nutrients === "string" ? JSON.parse(f.cached_nutrients) : f.cached_nutrients; + for (const key of Object.keys(RDI)) { + if (rn[key]) totals[key] = (totals[key] || 0) + rn[key]; + } + } catch {} + } + // From supplements if (suppTotals) { for (const key of Object.keys(RDI)) { @@ -4275,7 +4288,8 @@ function calcDailyNutrients(food, suppTotals) { const amount = totals[key] || 0; const pct = Math.round(amount / rdi * 100); const label = NUTRIENT_LABELS[key] || key; - result.push({ key, label, amount, rdi, pct }); + const normType = BMR.RDI_TYPES[key] || "rdi"; + result.push({ key, label, amount, rdi, pct, normType }); } result.sort((a, b) => a.pct - b.pct); return result; @@ -4286,7 +4300,8 @@ function renderNutrientChips(nutrients) { if (!nutrients || nutrients.length === 0) return ""; return nutrients.map(n => { const cls = n.pct < 30 ? "nut-low" : n.pct < 80 ? "nut-mid" : "nut-ok"; - return `
${n.label} ${n.pct}%
`; + const star = n.normType === "aup" ? "*" : ""; + return `
${n.label}${star} ${n.pct}%
`; }).join(""); }