Fix dedup: run BEFORE version check, RDI standards from DB

Dedup fix:
- Dedup SQL now runs BEFORE seed version check (was inside "if new version" block,
  never executed when version matched). 16675→8330 products.
- UNIQUE INDEX on (name_lower, source, COALESCE(store_id,'')) prevents future dupes
- Errors propagated via map_err (no more silent failures)

RDI standards from DB (not hardcoded):
- rdi_standards table: RU, EU, US
- rdi_values table: nutrient_key + value per standard
- 26 nutrients seeded for all 3 standards
- BMR.loadRDI(code) reads from DB, replaces hardcoded RDI object
- Settings: "Стандарт РСП" dropdown (РФ/ЕС/США)
- Profile stores selected standard, loaded at init
- Fallback: hardcoded RU values until DB ready
This commit is contained in:
4aiman 2026-06-01 16:45:30 +03:00
commit a0c7dfb5e2
4 changed files with 148 additions and 61 deletions

View file

@ -27,35 +27,36 @@ function estimateRealBmr(weights, foodTotals) {
return realExpend > 500 ? Math.round(realExpend) : null; // sanity: < 500 kcal/day is wrong data
}
// Russian daily recommended intake (РСП) per ТР ТС 022/2011
const 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, // фолиевая (= 0.2 мг = 200 мкг), в БД как folate_mcg
vit_b12_mcg: 1,
vit_d_mcg: 5,
vit_k_mcg: 120,
calcium_mg: 1000,
magnesium_mg: 400,
zinc_mg: 15,
iron_mg: 14,
iodine_mg: 0.15,
selenium_mcg: 70,
molybdenum_mcg: 70,
manganese_mg: 2,
copper_mg: 1,
chromium_mg: 0.05,
phosphorus_mg: 800,
potassium_mg: 3500,
sodium_mg: 2400,
// carotene_mcg removed — auto-converted to vit_a_mcg (carotene/12)
// RDI loaded from DB (rdi_standards + rdi_values tables).
// Fallback hardcoded RU values used until DB is ready.
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,
vit_b12_mcg: 1, vit_d_mcg: 5, vit_k_mcg: 120, calcium_mg: 1000, magnesium_mg: 400,
zinc_mg: 15, iron_mg: 14, iodine_mg: 0.15, selenium_mcg: 70, molybdenum_mcg: 70,
manganese_mg: 2, copper_mg: 1, chromium_mg: 0.05, phosphorus_mg: 800,
potassium_mg: 3500, sodium_mg: 2400,
};
window.BMR = { calculateBmr, getAge, estimateRealBmr, RDI };
// Load RDI from DB for selected standard. Called after DB ready.
async function loadRDI(standardCode) {
try {
const std = await window.__TAURI__.core.invoke("plugin:sql|select", {
db: "sqlite:baldav.db",
query: "SELECT id FROM rdi_standards WHERE code = ?1",
values: [standardCode || "RU"],
});
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",
values: [std[0].id],
});
const loaded = {};
for (const v of vals) loaded[v.nutrient_key] = v.value;
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; } };

View file

@ -1155,6 +1155,64 @@ async function runMigrations() {
// Product dedup handled in Rust import_seed_products (UNIQUE INDEX on name_lower+source+store_id)
// RDI standards tables
await execute(`CREATE TABLE IF NOT EXISTS rdi_standards (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
)`);
await execute(`CREATE TABLE IF NOT EXISTS rdi_values (
id INTEGER PRIMARY KEY,
standard_id INTEGER NOT NULL REFERENCES rdi_standards(id),
nutrient_key TEXT NOT NULL,
value REAL NOT NULL,
UNIQUE(standard_id, nutrient_key)
)`);
// Seed RDI standards if empty
const rdiCount = await select("SELECT COUNT(*) as cnt FROM rdi_standards");
if (rdiCount[0].cnt === 0) {
await execute("INSERT INTO rdi_standards (id, code, name) VALUES (1, 'RU', 'РФ (ТР ТС 022/2011)')");
await execute("INSERT INTO rdi_standards (id, code, name) VALUES (2, 'EU', 'ЕС (EU RDA)')");
await execute("INSERT INTO rdi_standards (id, code, name) VALUES (3, 'US', 'США (US DV)')");
const standards = {
// nutrient_key: [RU, EU, US]
vit_a_mcg: [800, 800, 900],
vit_c_mg: [60, 80, 90],
vit_e_mg: [10, 12, 15],
vit_b1_mg: [1.4, 1.1, 1.2],
vit_b2_mg: [1.6, 1.4, 1.3],
vit_b3_mg: [18, 16, 16],
vit_b5_mg: [6, 6, 5],
vit_b6_mg: [2, 1.4, 1.7],
vit_b7_mcg: [50, 50, 30],
folate_mcg: [200, 200, 400],
vit_b12_mcg: [1, 2.5, 2.4],
vit_d_mcg: [5, 15, 15],
vit_k_mcg: [120, 75, 120],
calcium_mg: [1000, 800, 1000],
magnesium_mg: [400, 375, 420],
zinc_mg: [15, 10, 11],
iron_mg: [14, 14, 18],
iodine_mg: [0.15, 0.15, 0.15],
selenium_mcg: [70, 55, 55],
molybdenum_mcg: [70, 50, 45],
manganese_mg: [2, 2, 2.3],
copper_mg: [1, 1, 0.9],
chromium_mg: [0.05, 0.04, 0.035],
phosphorus_mg: [800, 700, 700],
potassium_mg: [3500, 2000, 2600],
sodium_mg: [2400, 2400, 2300],
};
for (const [key, vals] of Object.entries(standards)) {
for (let i = 0; i < 3; i++) {
await execute("INSERT INTO rdi_values (standard_id, nutrient_key, value) VALUES (?1, ?2, ?3)", [i + 1, key, vals[i]]);
}
}
console.log("[db.js] Seeded 3 RDI standards (RU/EU/US)");
}
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

@ -75,6 +75,12 @@ async function init() {
// Recover any orphaned GPS recording session
try { await GPS.recoverSession(); } catch (e) { /* ignore */ }
// Load RDI standard from profile (default: RU)
try {
const rdiCode = (await DB.getProfileValue("rdi_standard")) || "RU";
await BMR.loadRDI(rdiCode);
} catch (e) { /* RDI stays at fallback RU values */ }
// Import seed products if needed — show progress only if actually importing
try {
let unlisten = null;
@ -3681,6 +3687,21 @@ async function renderSettings(container) {
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Нормы нутриентов</div>
<div class="settings-item">
<div class="settings-item-left">
<div class="settings-item-title">Стандарт РСП</div>
<div class="settings-item-desc">Пересчитает % нутриентов</div>
</div>
<select id="s-rdi-standard" style="width:80px;padding:6px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--on-bg)">
<option value="RU">РФ</option>
<option value="EU">ЕС</option>
<option value="US">США</option>
</select>
</div>
</div>
<div class="settings-section">
<div class="settings-section-title">Карты</div>
<div class="settings-item">
@ -3787,6 +3808,17 @@ async function renderSettings(container) {
// Profile button → open profile modal
container.querySelector("#s-open-profile").onclick = () => showProfileModal();
// RDI standard selector
const rdiSel = container.querySelector("#s-rdi-standard");
if (rdiSel) {
rdiSel.value = state.profile.rdi_standard || "RU";
rdiSel.onchange = async () => {
await DB.setProfileValue("rdi_standard", rdiSel.value);
state.profile.rdi_standard = rdiSel.value;
await BMR.loadRDI(rdiSel.value);
};
}
// Offline toggle
const offToggle = container.querySelector("#s-offline-toggle");
offToggle.onclick = () => {

View file

@ -120,21 +120,7 @@ fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
};
let main_db = rusqlite::Connection::open(&main_path).map_err(|e| e.to_string())?;
let current_version: i64 = main_db.query_row("SELECT COALESCE((SELECT value FROM profile WHERE key='seed_version'), '0')", [], |r| {
let s: String = r.get(0)?;
Ok(s.parse::<i64>().unwrap_or(0))
}
).unwrap_or(0);
if current_version >= seed_version {return Ok(format!("up-to-date:v{}", current_version));}
let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
// Emit initial progress
// Bulk import — no per-row progress needed (takes <5 seconds)
// Ensure all columns exist in main DB (including micronutrients from ION import)
// Ensure all columns exist in main DB
let extra_cols = [
"composition TEXT", "name_lower TEXT", "category TEXT",
"sodium_mg REAL", "potassium_mg REAL", "calcium_mg REAL",
@ -148,19 +134,11 @@ fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
let _ = main_db.execute(&format!("ALTER TABLE products ADD COLUMN {}", col), []);
}
// ATTACH seed DB to main DB and do bulk INSERT + UPDATE in SQL (no per-row loop)
let seed_str = seed_path.to_str().unwrap_or_default().to_string();
main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str]).map_err(|e| e.to_string())?;
// Dedup ALWAYS runs (even if seed version matches) — fixes legacy duplicates.
// Fast: <0.2s with index. No-op if no dupes exist.
let _ = main_db.execute(
"CREATE INDEX IF NOT EXISTS idx_products_name_lower ON products(name_lower)", []);
main_db.execute("BEGIN", []).map_err(|e| e.to_string())?;
// Create index for fast lookups during dedup and insert
let _ = main_db.execute_batch(
"CREATE INDEX IF NOT EXISTS idx_products_name_lower ON main.products(name_lower);"
);
// Dedup existing products by (name_lower, source, store_id).
// Each statement separate — execute_batch can fail silently on multi-statement strings.
main_db.execute(
"UPDATE food_log SET product_id = (
SELECT MIN(p2.id) FROM products p2
@ -190,12 +168,30 @@ fn import_seed_products(app: tauri::AppHandle) -> Result<String, String> {
GROUP BY name_lower, source, COALESCE(store_id,'')
)", []).map_err(|e| format!("dedup delete: {}", e))?;
// UNIQUE index to prevent future duplicates
main_db.execute(
let _ = main_db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_products_dedup
ON products(name_lower, source, COALESCE(store_id, ''))
WHERE name_lower IS NOT NULL", [])
.map_err(|e| format!("unique index: {}", e))?;
WHERE name_lower IS NOT NULL", []);
// Check seed version — skip import if already up to date
let current_version: i64 = main_db.query_row(
"SELECT COALESCE((SELECT value FROM profile WHERE key='seed_version'), '0')",
[], |r| { let s: String = r.get(0)?; Ok(s.parse::<i64>().unwrap_or(0)) }
).unwrap_or(0);
if current_version >= seed_version {
return Ok(format!("up-to-date:v{}", current_version));
}
// ATTACH seed DB for import
let seed_str = seed_path.to_str().unwrap_or_default().to_string();
main_db.execute("ATTACH DATABASE ?1 AS seed", rusqlite::params![seed_str])
.map_err(|e| e.to_string())?;
let total: usize = seed_db.query_row("SELECT COUNT(*) FROM products", [], |r| r.get(0))
.map_err(|e| e.to_string())?;
main_db.execute("BEGIN", []).map_err(|e| e.to_string())?;
// Bulk INSERT new products from seed (INSERT OR IGNORE now works with unique index)
let inserted = main_db.execute(