АУП norms with star, nutrient half-life table, recipe nutrients in daily strip

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.
This commit is contained in:
4aiman 2026-06-02 18:39:09 +03:00
commit 0bafb2b317
3 changed files with 122 additions and 8 deletions

View file

@ -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; },
};

View file

@ -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.

View file

@ -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 `<div class="form-group micro-field">
<label>${f.label}</label>
<input id="m-${f.key}" type="number" inputmode="decimal" step="any" value="${displayVal}" placeholder="—" />
<input id="m-${f.key}" type="number" inputmode="decimal" step="any" value="${displayVal}" placeholder="${(() => { const r = BMR.RDI[f.key]; const t = BMR.RDI_TYPES[f.key]; return r ? r + (t === 'aup' ? '*' : '') : '—'; })()}" />
</div>`;
}
).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 `<div class="nut-item ${cls}">${n.label} ${n.pct}%</div>`;
const star = n.normType === "aup" ? "*" : "";
return `<div class="nut-item ${cls}">${n.label}${star} ${n.pct}%</div>`;
}).join("");
}