baldav/src/db.js
4aiman 1daf757bcb GPS point ordering + GPX export + chart zoom fixes
GPS background recording:
- Batched writes (10s flush) in native service
- Polling lock prevents parallel processing
- Track by timestamp instead of line count
- Recovery idempotent via max DB timestamp
- Seq numbers on live recorded points

GPX export:
- Button in session detail
- SAF file picker on Android (ACTION_CREATE_DOCUMENT)
- GPS config in metadata (accuracy, minSpeed, minDist)

Charts:
- Pinch zoom enabled at creation (Hammer.js registers handlers)
- touch-action: pan-y blocks pinch in normal view (page scrolls)
- Fullscreen: CSS-only expand (no canvas manipulation)
- Downsampled to 500 points for smooth rendering
- Back gesture collapses fullscreen before closing modal

Build: --real flag finds first non-emulator device
2026-05-16 21:21:13 +03:00

338 lines
11 KiB
JavaScript

// Database layer — wraps Tauri SQL plugin
// All queries go through this module; no raw SQL elsewhere
let db = null;
export async function initDb() {
const Database = (await import("@tauri-apps/plugin-sql")).default;
db = await Database.load("sqlite:baldav.db");
return db;
}
export async function getDb() {
if (!db) await initDb();
return db;
}
// ---------------------------------------------------------------------------
// Profile (key-value store)
// ---------------------------------------------------------------------------
export async function getProfileValue(key) {
const d = await getDb();
const rows = await d.select("SELECT value FROM profile WHERE key = $1", [key]);
return rows.length ? rows[0].value : null;
}
export async function setProfileValue(key, value) {
const d = await getDb();
await d.execute(
"INSERT OR REPLACE INTO profile (key, value) VALUES ($1, $2)",
[key, String(value)]
);
}
export async function getFullProfile() {
const d = await getDb();
const rows = await d.select("SELECT key, value FROM profile");
const p = {};
for (const r of rows) p[r.key] = r.value;
return p;
}
// ---------------------------------------------------------------------------
// Weight log
// ---------------------------------------------------------------------------
export async function addWeight(date, time, kg) {
const d = await getDb();
await d.execute(
"INSERT INTO weight_log (date, time, weight_kg) VALUES ($1, $2, $3)",
[date, time, kg]
);
}
export async function deleteWeight(id) {
const d = await getDb();
await d.execute("DELETE FROM weight_log WHERE id = $1", [id]);
}
export async function getWeightsForDate(date) {
const d = await getDb();
return d.select(
"SELECT * FROM weight_log WHERE date = $1 ORDER BY time ASC",
[date]
);
}
export async function getLatestWeight() {
const d = await getDb();
const rows = await d.select(
"SELECT * FROM weight_log ORDER BY date DESC, time DESC LIMIT 1"
);
return rows.length ? rows[0] : null;
}
export async function getWeightsRange(from, to) {
const d = await getDb();
return d.select(
"SELECT * FROM weight_log WHERE date >= $1 AND date <= $2 ORDER BY date ASC, time ASC",
[from, to]
);
}
// ---------------------------------------------------------------------------
// Sleep log
// ---------------------------------------------------------------------------
export async function getSleepForDate(date) {
const d = await getDb();
const rows = await d.select("SELECT * FROM sleep_log WHERE date = $1", [date]);
return rows.length ? rows[0] : null;
}
export async function saveSleep(date, bedTime, asleepTime, wakeTime, napMinutes, notes) {
const d = await getDb();
const existing = await getSleepForDate(date);
if (existing) {
await d.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 d.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
// ---------------------------------------------------------------------------
export async function searchProducts(query, limit = 30) {
const d = await getDb();
return d.select(
`SELECT id, name, brand, category, kcal_per_100g, protein_per_100g,
fat_per_100g, carbs_per_100g, fiber_per_100g
FROM products
WHERE name LIKE $1 OR brand LIKE $1
ORDER BY name COLLATE NOCASE
LIMIT $2`,
[`%${query}%`, limit]
);
}
export async function getProduct(id) {
const d = await getDb();
const rows = await d.select("SELECT * FROM products WHERE id = $1", [id]);
return rows.length ? rows[0] : null;
}
export async function addProduct(p) {
const d = await getDb();
const r = await d.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;
}
// ---------------------------------------------------------------------------
// Food log
// ---------------------------------------------------------------------------
export async function getFoodForDate(date) {
const d = await getDb();
return d.select(
`SELECT fl.*, p.name as product_name, p.brand as product_brand,
r.name as recipe_name
FROM food_log fl
LEFT JOIN products p ON fl.product_id = p.id
LEFT JOIN recipes r ON fl.recipe_id = r.id
WHERE fl.date = $1
ORDER BY fl.time ASC`,
[date]
);
}
// Add a food log entry. Calculates macros from product if product_id given.
export async function addFoodEntry(date, time, productId, recipeId, amountG, note) {
const d = await getDb();
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 d.execute(
`INSERT INTO food_log
(date, time, product_id, 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,$11)`,
[date, time, productId ?? null, recipeId ?? null, amountG,
kcal, protein, fat, carbs, fiber, note ?? null]
);
}
// Add food by manual kcal only (for backward compat with old text entries)
export async function addFoodManual(date, time, desc, kcal) {
const d = await getDb();
await d.execute(
`INSERT INTO food_log (date, time, note, amount_g, kcal)
VALUES ($1, $2, $3, 0, $4)`,
[date, time, desc, kcal ?? null]
);
}
export async function deleteFoodEntry(id) {
const d = await getDb();
await d.execute("DELETE FROM food_log WHERE id = $1", [id]);
}
// ---------------------------------------------------------------------------
// Activity log
// ---------------------------------------------------------------------------
export async function getActivitiesForDate(date) {
const d = await getDb();
return d.select(
"SELECT * FROM activity_log WHERE date = $1 ORDER BY time ASC",
[date]
);
}
export async function addActivity(date, time, type, desc, minutes, kcal) {
const d = await getDb();
await d.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]
);
}
export async function deleteActivity(id) {
const d = await getDb();
await d.execute("DELETE FROM activity_log WHERE id = $1", [id]);
}
// ---------------------------------------------------------------------------
// Supplements
// ---------------------------------------------------------------------------
export async function getSupplements() {
const d = await getDb();
return d.select("SELECT * FROM supplements ORDER BY name COLLATE NOCASE");
}
export async function addSupplement(s) {
const d = await getDb();
const r = await d.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;
}
export async function logSupplement(date, time, supplementId, doseG) {
const d = await getDb();
await d.execute(
"INSERT INTO supplement_log (date, time, supplement_id, dose_g) VALUES ($1,$2,$3,$4)",
[date, time, supplementId, doseG ?? null]
);
}
export async function getSupplementLogForDate(date) {
const d = await getDb();
return d.select(
`SELECT sl.*, s.name FROM supplement_log sl
JOIN supplements s ON sl.supplement_id = s.id
WHERE sl.date = $1 ORDER BY sl.time ASC`,
[date]
);
}
// ---------------------------------------------------------------------------
// Analytics helpers
// ---------------------------------------------------------------------------
// Returns {kcal, protein_g, fat_g, carbs_g, fiber_g} totals for a date
export async function getFoodTotalsForDate(date) {
const d = await getDb();
const rows = await d.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 };
}
export async function getActivityTotalsForDate(date) {
const d = await getDb();
const rows = await d.select(
"SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = $1",
[date]
);
return rows[0] || { kcal: 0, minutes: 0 };
}
// Weight data for charts: last N days
export async function getRecentWeights(days = 30) {
const d = await getDb();
return d.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} days`]
);
}