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
338 lines
11 KiB
JavaScript
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`]
|
|
);
|
|
}
|