2026-05-13 10:21:24 +03:00
// Database layer — talks to tauri-plugin-sql via raw Tauri IPC.
// No imports, no npm. window.__TAURI__ is injected by Tauri (withGlobalTauri: true).
// The DB is preloaded at startup via tauri.conf.json plugins.sql.preload.
const DB _PATH = "sqlite:baldav.db" ;
function inv ( cmd , args ) {
return window . _ _TAURI _ _ . core . invoke ( ` plugin:sql| ${ cmd } ` , args ) ;
}
async function execute ( query , params ) {
return inv ( "execute" , { db : DB _PATH , query , values : params ? ? [ ] } ) ;
}
async function select ( query , params ) {
return inv ( "select" , { db : DB _PATH , query , values : params ? ? [ ] } ) ;
}
// ---------------------------------------------------------------------------
// Profile (key-value store)
// ---------------------------------------------------------------------------
async function getProfileValue ( key ) {
const rows = await select ( "SELECT value FROM profile WHERE key = ?1" , [ key ] ) ;
return rows . length ? rows [ 0 ] . value : null ;
}
async function setProfileValue ( key , value ) {
await execute (
"INSERT OR REPLACE INTO profile (key, value) VALUES (?1, ?2)" ,
[ key , String ( value ) ]
) ;
}
async function getFullProfile ( ) {
const rows = await select ( "SELECT key, value FROM profile" ) ;
const p = { } ;
for ( const r of rows ) p [ r . key ] = r . value ;
return p ;
}
// ---------------------------------------------------------------------------
// Weight log
// ---------------------------------------------------------------------------
async function addWeight ( date , time , kg ) {
await execute (
"INSERT INTO weight_log (date, time, weight_kg) VALUES (?1, ?2, ?3)" ,
[ date , time , kg ]
) ;
2026-09-22 11:12:24 +03:00
// Keep the settings weight in sync with the latest weigh-in (today's if there is one).
const latest = await getLatestWeight ( ) ;
if ( latest && latest . weight _kg != null ) await setProfileValue ( "weight_kg" , latest . weight _kg ) ;
2026-05-13 10:21:24 +03:00
}
async function deleteWeight ( id ) {
await execute ( "DELETE FROM weight_log WHERE id = ?1" , [ id ] ) ;
}
async function getWeightsForDate ( date ) {
return select ( "SELECT * FROM weight_log WHERE date = ?1 ORDER BY time ASC" , [ date ] ) ;
}
async function getLatestWeight ( ) {
const rows = await select (
"SELECT * FROM weight_log ORDER BY date DESC, time DESC LIMIT 1"
) ;
return rows . length ? rows [ 0 ] : null ;
}
2026-09-22 11:12:24 +03:00
// `days` calendar days ENDING at endDate (default today). Range is (end-days, end] so it is
// exactly `days` days (the old `>= date('now','-N')` gave N+1) and follows the selected day.
async function getRecentWeights ( days , endDate ) {
const end = endDate || "now" ;
2026-05-13 10:21:24 +03:00
return select (
` SELECT date, MIN(weight_kg) as weight_kg
FROM weight _log
2026-09-22 11:12:24 +03:00
WHERE date > date ( ? 2 , ? 1 ) AND date <= date ( ? 2 )
2026-05-13 10:21:24 +03:00
GROUP BY date ORDER BY date ASC ` ,
2026-09-22 11:12:24 +03:00
[ ` - ${ days ? ? 30 } days ` , end ]
2026-05-13 10:21:24 +03:00
) ;
}
// ---------------------------------------------------------------------------
// Sleep log
// ---------------------------------------------------------------------------
async function getSleepForDate ( date ) {
const rows = await select ( "SELECT * FROM sleep_log WHERE date = ?1" , [ date ] ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function saveSleep ( date , bedTime , asleepTime , wakeTime , napMinutes , notes ) {
const existing = await getSleepForDate ( date ) ;
if ( existing ) {
await 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 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
// ---------------------------------------------------------------------------
async function searchProducts ( query , limit ) {
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
const q = ( query || "" ) . toLowerCase ( ) . trim ( ) ;
// Order-independent: every whitespace-separated word must appear in the name
// (in any order). Fixes "надо вводить слова в порядке названия".
const tokens = q . split ( /\s+/ ) . filter ( Boolean ) ;
if ( tokens . length === 0 ) return [ ] ;
const cols = ` id, name, brand, category, weight_g, kcal_per_100g,
2026-05-13 10:21:24 +03:00
protein _per _100g , fat _per _100g , carbs _per _100g , fiber _per _100g ,
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
composition , nutrients _guessed , source , water _pct ,
Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
vit _a _mcg , vit _c _mg , vit _b1 _mg , vit _b2 _mg , vit _b3 _mg , vit _e _mg ,
Fix carotene missing from food query, add fish oil field, full nutrient SELECT
- getFoodForDate: added p.phosphorus_mg, p.carotene_mcg (were missing)
- searchProducts: added carotene, selenium, zinc, copper, folate, D, K, B6, B12
- Supplement form: "Рыбий жир (мг)" field for total capsule weight
- Omega fields relabeled: "в т.ч." prefix to show they're part of fish oil
Carrot (12000 mcg carotene) was showing 0% because carotene_mcg
was not in the getFoodForDate SELECT.
2026-05-29 18:00:02 +03:00
calcium _mg , iron _mg , magnesium _mg , potassium _mg , sodium _mg , phosphorus _mg ,
carotene _mcg , selenium _mcg , zinc _mg , copper _mg , folate _mcg ,
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
vit _d _mcg , vit _k _mcg , vit _b6 _mg , vit _b12 _mcg ` ;
const where = tokens . map ( ( _ , i ) => ` name_lower LIKE ? ${ i + 1 } ` ) . join ( " AND " ) ;
const vals = tokens . map ( t => ` % ${ t } % ` ) ;
const nFull = vals . length + 1 , nPrefix = vals . length + 2 , nLimit = vals . length + 3 ;
vals . push ( ` % ${ q } % ` ) ; // full contiguous string present → rank first
vals . push ( ` ${ q } % ` ) ; // name starts with the query → rank next
vals . push ( limit ? ? 200 ) ;
return select (
` SELECT ${ cols }
2026-05-13 10:21:24 +03:00
FROM products
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
WHERE $ { where }
2026-05-13 10:21:24 +03:00
GROUP BY COALESCE ( store _id , id )
ORDER BY
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
CASE WHEN name _lower LIKE ? $ { nFull } THEN 0 ELSE 1 END ,
CASE WHEN name _lower LIKE ? $ { nPrefix } THEN 0 ELSE 1 END ,
2026-05-21 00:06:50 +03:00
length ( name ) ,
2026-05-13 10:21:24 +03:00
name COLLATE NOCASE
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
LIMIT ? $ { nLimit } ` ,
vals
2026-05-13 10:21:24 +03:00
) ;
}
async function getProduct ( id ) {
const rows = await select ( "SELECT * FROM products WHERE id = ?1" , [ id ] ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function addProduct ( p ) {
2026-05-30 15:33:59 +03:00
await execute (
2026-05-13 10:21:24 +03:00
` INSERT INTO products
2026-05-30 15:33:59 +03:00
( name , name _lower , brand , category , weight _g , kcal _per _100g , protein _per _100g ,
2026-05-13 10:21:24 +03:00
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 ,
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
selenium _mcg , potassium _mg , sodium _mg , phosphorus _mg , carotene _mcg , source , water _pct )
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 , ? 30 , ? 31 , ? 32 , ? 33 ) ` ,
2026-05-13 10:21:24 +03:00
[
2026-05-30 15:33:59 +03:00
p . name , p . name ? . toLowerCase ( ) ? ? null ,
p . brand ? ? null , p . category ? ? null , p . weight _g ? ? null ,
2026-05-13 10:21:24 +03:00
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 ,
2026-05-30 15:33:59 +03:00
p . phosphorus _mg ? ? null , p . carotene _mcg ? ? null ,
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
p . source ? ? "manual" , p . water _pct ? ? null ,
2026-05-13 10:21:24 +03:00
]
) ;
Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
const rows = await select ( "SELECT id FROM products ORDER BY id DESC LIMIT 1" ) ;
return rows [ 0 ] ? . id ;
2026-05-13 10:21:24 +03:00
}
2026-05-31 00:48:23 +03:00
async function getProductRefs ( id ) {
const food = await select ( "SELECT COUNT(*) as cnt FROM food_log WHERE product_id = ?1" , [ id ] ) ;
const recipes = await select ( "SELECT COUNT(*) as cnt FROM recipe_ingredients WHERE product_id = ?1" , [ id ] ) ;
return { food : food [ 0 ] ? . cnt || 0 , recipes : recipes [ 0 ] ? . cnt || 0 } ;
}
2026-05-30 21:08:32 +03:00
async function deleteProduct ( id ) {
await execute ( "DELETE FROM products WHERE id = ?1" , [ id ] ) ;
}
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
// Catalog cleanup: sources that carry a "плашка" (badge) and are worth keeping even
// if never eaten. Everything else (health-diet / ashan / fast food / null source) is
// removed ONLY when unused — i.e. not referenced by food_log or recipe_ingredients,
// so eaten products and recipe ingredients always survive.
const PRODUCT _KEEP _SOURCES = "'magnit','5ka','ion','kdv','manual','recipe_import','gridnev'" ;
const UNLABELED _UNUSED _WHERE =
` (source IS NULL OR source NOT IN ( ${ PRODUCT _KEEP _SOURCES } ))
AND id NOT IN ( SELECT product _id FROM food _log WHERE product _id IS NOT NULL )
AND id NOT IN ( SELECT product _id FROM recipe _ingredients WHERE product _id > 0 ) ` ;
async function countUnlabeledUnusedProducts ( ) {
const rows = await select ( ` SELECT COUNT(*) AS cnt FROM products WHERE ${ UNLABELED _UNUSED _WHERE } ` ) ;
return rows [ 0 ] ? . cnt || 0 ;
}
async function cleanupUnlabeledProducts ( ) {
const n = await countUnlabeledUnusedProducts ( ) ;
await execute ( ` DELETE FROM products WHERE ${ UNLABELED _UNUSED _WHERE } ` ) ;
return n ;
}
2026-05-21 00:06:50 +03:00
// ---------------------------------------------------------------------------
// Cooking methods & Recipes
// ---------------------------------------------------------------------------
async function getCookingMethods ( ) {
return select ( "SELECT * FROM cooking_methods ORDER BY id" ) ;
}
async function getCookingMethod ( id ) {
const rows = await select ( "SELECT * FROM cooking_methods WHERE id = ?1" , [ id ] ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function getRecipes ( ) {
return select (
` SELECT r.*, cm.name as cooking_method_name
FROM recipes r
LEFT JOIN cooking _methods cm ON r . cooking _method _id = cm . id
ORDER BY r . created _at DESC `
) ;
}
async function getRecipe ( id ) {
const rows = await select (
` SELECT r.*, cm.name as cooking_method_name
FROM recipes r
LEFT JOIN cooking _methods cm ON r . cooking _method _id = cm . id
WHERE r . id = ? 1 ` ,
[ id ]
) ;
return rows . length ? rows [ 0 ] : null ;
}
async function getRecipeIngredients ( recipeId ) {
return select (
` SELECT ri.*, p.name as product_name, p.kcal_per_100g,
p . protein _per _100g , p . fat _per _100g , p . carbs _per _100g ,
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
p . fiber _per _100g ,
p . vit _a _mcg , p . vit _c _mg , p . vit _d _mcg , p . vit _e _mg , p . vit _k _mcg ,
p . vit _b1 _mg , p . vit _b2 _mg , p . vit _b3 _mg , p . vit _b6 _mg ,
p . vit _b12 _mcg , p . folate _mcg ,
p . calcium _mg , p . iron _mg , p . magnesium _mg , p . zinc _mg , p . copper _mg ,
p . selenium _mcg , p . potassium _mg , p . sodium _mg ,
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
p . water _pct , cm . name as cooking _method _name
2026-05-21 00:06:50 +03:00
FROM recipe _ingredients ri
LEFT JOIN products p ON ri . product _id = p . id AND ri . product _id > 0
LEFT JOIN cooking _methods cm ON ri . cooking _method _id = cm . id
WHERE ri . recipe _id = ? 1
ORDER BY ri . id ` ,
[ recipeId ]
) ;
}
async function saveRecipe ( name , cookingMethodId , servings , notes ) {
2026-05-29 09:25:30 +03:00
await execute (
2026-05-21 00:06:50 +03:00
` INSERT INTO recipes (name, cooking_method_id, servings, notes, created_at)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , datetime ( 'now' ) ) ` ,
[ name , cookingMethodId ? ? null , servings ? ? 1 , notes ? ? null ]
) ;
2026-05-29 09:25:30 +03:00
// lastInsertId from tauri-plugin-sql is unreliable; query the actual id
const rows = await select ( "SELECT id FROM recipes ORDER BY id DESC LIMIT 1" ) ;
return rows [ 0 ] ? . id ;
2026-05-21 00:06:50 +03:00
}
async function updateRecipe ( id , name , cookingMethodId , servings , notes ) {
await execute (
` UPDATE recipes SET name=?1, cooking_method_id=?2, servings=?3, notes=?4
WHERE id = ? 5 ` ,
[ name , cookingMethodId ? ? null , servings ? ? 1 , notes ? ? null , id ]
) ;
}
async function deleteRecipe ( id ) {
await execute ( "DELETE FROM recipe_ingredients WHERE recipe_id = ?1" , [ id ] ) ;
await execute ( "DELETE FROM recipes WHERE id = ?1" , [ id ] ) ;
}
async function addRecipeIngredient ( recipeId , productId , customName , weightG , cookingMethodId ) {
const r = await execute (
` INSERT INTO recipe_ingredients (recipe_id, product_id, custom_name, amount_g, cooking_method_id)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 ) ` ,
[ recipeId , productId ? ? 0 , customName ? ? null , weightG , cookingMethodId ? ? null ]
) ;
return r . lastInsertId ;
}
async function deleteRecipeIngredient ( id ) {
await execute ( "DELETE FROM recipe_ingredients WHERE id = ?1" , [ id ] ) ;
}
async function clearRecipeIngredients ( recipeId ) {
await execute ( "DELETE FROM recipe_ingredients WHERE recipe_id = ?1" , [ recipeId ] ) ;
}
// Calculate nutrition for a recipe per serving.
// For each ingredient: (product nutrients / 100) * weight_g, then apply cooking loss.
// Cooking loss is taken from ingredient's own cooking_method_id, or recipe's default.
async function calculateRecipeNutrition ( recipeId ) {
const recipe = await getRecipe ( recipeId ) ;
if ( ! recipe ) return null ;
const ingredients = await getRecipeIngredients ( recipeId ) ;
const servings = recipe . servings || 1 ;
// Cache cooking methods to avoid repeated queries
const cmCache = { } ;
async function getCM ( cmId ) {
if ( ! cmId ) return null ;
if ( cmCache [ cmId ] ) return cmCache [ cmId ] ;
cmCache [ cmId ] = await getCookingMethod ( cmId ) ;
return cmCache [ cmId ] ;
}
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
// All micronutrient keys tracked in products table
const microKeys = [
"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" ,
] ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
const totals = { kcal : 0 , protein : 0 , fat : 0 , carbs : 0 , fiber : 0 , water _ml : 0 } ;
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
for ( const mk of microKeys ) totals [ mk ] = 0 ;
2026-05-21 00:06:50 +03:00
for ( const ing of ingredients ) {
if ( ! ing . product _id || ing . product _id === 0 ) continue ; // skip custom ingredients without product data
const factor = ( ing . amount _g || 0 ) / 100 ;
// Raw nutrient values for this ingredient
const raw = {
kcal : ( ing . kcal _per _100g ? ? 0 ) * factor ,
protein : ( ing . protein _per _100g ? ? 0 ) * factor ,
fat : ( ing . fat _per _100g ? ? 0 ) * factor ,
carbs : ( ing . carbs _per _100g ? ? 0 ) * factor ,
fiber : ( ing . fiber _per _100g ? ? 0 ) * factor ,
} ;
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
for ( const mk of microKeys ) raw [ mk ] = ( ing [ mk ] ? ? 0 ) * factor ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
raw . water _ml = ( ing . water _pct ? ? 0 ) * ( ing . amount _g || 0 ) / 100 ;
2026-05-21 00:06:50 +03:00
// Apply cooking loss: use ingredient's method, or recipe's default
const cmId = ing . cooking _method _id || recipe . cooking _method _id ;
const cm = await getCM ( cmId ) ;
if ( cm ) {
// loss_pct is percentage lost (positive = loss, negative = gain like frying fat)
raw . kcal *= ( 1 - ( cm . kcal _loss _pct ? ? 0 ) / 100 ) ;
raw . protein *= ( 1 - ( cm . protein _loss _pct ? ? 0 ) / 100 ) ;
raw . fat *= ( 1 - ( cm . fat _loss _pct ? ? 0 ) / 100 ) ;
raw . carbs *= ( 1 - ( cm . carbs _loss _pct ? ? 0 ) / 100 ) ;
raw . fiber *= ( 1 - ( cm . fiber _loss _pct ? ? 0 ) / 100 ) ;
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
// Vitamin losses: cooking_methods table only has columns for a, c, b1, b2
2026-05-21 00:06:50 +03:00
raw . vit _a _mcg *= ( 1 - ( cm . vit _a _loss _pct ? ? 0 ) / 100 ) ;
raw . vit _c _mg *= ( 1 - ( cm . vit _c _loss _pct ? ? 0 ) / 100 ) ;
raw . vit _b1 _mg *= ( 1 - ( cm . vit _b1 _loss _pct ? ? 0 ) / 100 ) ;
raw . vit _b2 _mg *= ( 1 - ( cm . vit _b2 _loss _pct ? ? 0 ) / 100 ) ;
}
for ( const k of Object . keys ( totals ) ) {
totals [ k ] += raw [ k ] ;
}
}
// Per serving
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
const result = {
2026-05-21 00:06:50 +03:00
kcal : Math . round ( totals . kcal / servings ) ,
protein : Math . round ( totals . protein / servings * 10 ) / 10 ,
fat : Math . round ( totals . fat / servings * 10 ) / 10 ,
carbs : Math . round ( totals . carbs / servings * 10 ) / 10 ,
fiber : Math . round ( totals . fiber / servings * 10 ) / 10 ,
servings : servings ,
totalWeight : ingredients . reduce ( ( s , i ) => s + ( i . amount _g || 0 ) , 0 ) ,
} ;
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
// Add all micronutrients per serving (rounded)
for ( const mk of microKeys ) {
result [ mk ] = Math . round ( totals [ mk ] / servings * 100 ) / 100 ;
}
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
result . water _ml = Math . round ( totals . water _ml / servings ) ;
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
return result ;
2026-05-21 00:06:50 +03:00
}
// Log a recipe as a food entry — stores calculated nutrition per serving
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
async function addFoodFromRecipe ( date , time , recipeId , customAmountG ) {
2026-05-21 00:06:50 +03:00
const nutr = await calculateRecipeNutrition ( recipeId ) ;
if ( ! nutr ) return ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
const servingG = Math . round ( nutr . totalWeight / nutr . servings ) ;
const amountG = customAmountG ? ? servingG ;
const scale = servingG > 0 ? amountG / servingG : 1 ;
2026-05-21 00:06:50 +03:00
await execute (
` INSERT INTO food_log
( date , time , 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 ) ` ,
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
[ date , time , recipeId , amountG ,
Math . round ( nutr . kcal * scale ) ,
Math . round ( nutr . protein * scale * 10 ) / 10 ,
Math . round ( nutr . fat * scale * 10 ) / 10 ,
Math . round ( nutr . carbs * scale * 10 ) / 10 ,
Math . round ( nutr . fiber * scale * 10 ) / 10 , null ]
2026-05-21 00:06:50 +03:00
) ;
}
2026-05-13 10:21:24 +03:00
// ---------------------------------------------------------------------------
// Food log
// ---------------------------------------------------------------------------
async function getFoodForDate ( date ) {
return select (
` SELECT fl.*, p.name as product_name, p.brand as product_brand,
2026-05-30 15:59:50 +03:00
COALESCE ( p . nutrients _guessed , 0 ) as nutrients _guessed , p . source as product _source ,
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
p . vit _a _mcg , p . vit _c _mg , p . vit _d _mcg , p . vit _e _mg , p . vit _k _mcg ,
p . vit _b1 _mg , p . vit _b2 _mg , p . vit _b3 _mg , p . vit _b6 _mg ,
p . vit _b12 _mcg , p . folate _mcg ,
p . calcium _mg , p . iron _mg , p . magnesium _mg , p . zinc _mg , p . copper _mg ,
p . selenium _mcg , p . potassium _mg , p . sodium _mg ,
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
p . phosphorus _mg , p . carotene _mcg , p . water _pct ,
АУП 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.
2026-06-02 18:39:09 +03:00
r . name as recipe _name , r . cached _nutrients ,
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
su . name as unit _name , su . name _short as unit _short
2026-05-13 10:21:24 +03:00
FROM food _log fl
LEFT JOIN products p ON fl . product _id = p . id
LEFT JOIN recipes r ON fl . recipe _id = r . id
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
LEFT JOIN serving _units su ON fl . unit _id = su . id
2026-05-13 10:21:24 +03:00
WHERE fl . date = ? 1 ORDER BY fl . time ASC ` ,
[ date ]
) ;
}
async function addFoodManual ( date , time , desc , kcal ) {
await execute (
"INSERT INTO food_log (date, time, note, amount_g, kcal) VALUES (?1,?2,?3,0,?4)" ,
[ date , time , desc , kcal ? ? null ]
) ;
}
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
async function addFoodEntry ( date , time , productId , amountG , note , unitId , unitAmount ) {
2026-05-13 10:21:24 +03:00
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 execute (
` INSERT INTO food_log
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
( date , time , product _id , amount _g , kcal , protein _g , fat _g , carbs _g , fiber _g , note , unit _id , unit _amount )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 , ? 11 , ? 12 ) ` ,
2026-05-13 10:21:24 +03:00
[ date , time , productId ? ? null , amountG ,
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
kcal , protein , fat , carbs , fiber , note ? ? null ,
unitId ? ? null , unitAmount ? ? null ]
2026-05-13 10:21:24 +03:00
) ;
}
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
async function updateFoodEntry ( id , time , amountG , note , kcalOverride , unitId , unitAmount ) {
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
// Recalculate nutrition from product or recipe
const rows = await select (
"SELECT product_id, recipe_id, amount_g, kcal, protein_g, fat_g, carbs_g, fiber_g FROM food_log WHERE id = ?1" , [ id ] ) ;
2026-05-13 10:21:24 +03:00
if ( ! rows . length ) return ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
const row = rows [ 0 ] ;
2026-05-13 10:21:24 +03:00
let kcal = kcalOverride , protein = null , fat = null , carbs = null , fiber = null ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
if ( row . product _id && kcalOverride == null ) {
// Product: recalculate from kcal_per_100g
const p = await getProduct ( row . product _id ) ;
2026-05-13 10:21:24 +03:00
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 ;
}
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
} else if ( row . recipe _id && kcalOverride == null && row . amount _g > 0 ) {
// Recipe: scale proportionally to amount change
const scale = amountG / row . amount _g ;
kcal = Math . round ( ( row . kcal ? ? 0 ) * scale ) ;
protein = Math . round ( ( row . protein _g ? ? 0 ) * scale * 10 ) / 10 ;
fat = Math . round ( ( row . fat _g ? ? 0 ) * scale * 10 ) / 10 ;
carbs = Math . round ( ( row . carbs _g ? ? 0 ) * scale * 10 ) / 10 ;
fiber = Math . round ( ( row . fiber _g ? ? 0 ) * scale * 10 ) / 10 ;
2026-05-13 10:21:24 +03:00
}
await execute (
` UPDATE food_log SET time=?1, amount_g=?2, note=?3,
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
kcal = ? 4 , protein _g = ? 5 , fat _g = ? 6 , carbs _g = ? 7 , fiber _g = ? 8 ,
unit _id = ? 9 , unit _amount = ? 10
WHERE id = ? 11 ` ,
[ time , amountG , note ? ? null , kcal , protein , fat , carbs , fiber ,
unitId ? ? null , unitAmount ? ? null , id ]
2026-05-13 10:21:24 +03:00
) ;
}
async function deleteFoodEntry ( id ) {
await execute ( "DELETE FROM food_log WHERE id = ?1" , [ id ] ) ;
}
// ---------------------------------------------------------------------------
// Activity log
// ---------------------------------------------------------------------------
async function getActivitiesForDate ( date ) {
return select ( "SELECT * FROM activity_log WHERE date = ?1 ORDER BY time ASC" , [ date ] ) ;
}
Activity types (squats/jumprope/stretch/swim), road map, tile import, fixes
Activity types:
- New types: squats, jump rope, stretching, swimming pool
- MET-based kcal auto-calculation (6.0/12.3/2.3/8.0)
- Dynamic modal fields per type (sets/reps, distance, minutes)
- Fix: state.latestWeight is object {weight_kg}, not number
Road map:
- road_points table with 2m dedup, activity type (bike/walk)
- Rust rebuild_roads + get_road_points (bypass SQL plugin 28K limit)
- Segment detection by 100m gap, bike=blue walk=brown
Tile system:
- CSP fix: a.tile.openstreetmap.org matches *.tile pattern
- pack_tiles.js tool, import from file/router, ureq body limit bypass
- Tile errors: done(null,tile) prevents crash cascade
Other fixes:
- Manual routes: skip gap segmentation (synthetic timestamps)
- Draw route: own CSS class, not affected by landscape rotation
- Orientation lock button (A/L), hysteresis 65/35 degrees
- GPS placeholder only when state=waiting_gps
- Food amount inputmode=text for formula support (4*30)
- showModal onMount callback
2026-06-26 21:04:21 +03:00
async function addActivity ( date , time , type , desc , minutes , kcal , opts ) {
// Build column list dynamically — new columns may not exist yet on first run
const cols = [ "date" , "time" , "type" , "desc" , "minutes" , "kcal" ] ;
const vals = [ date , time , type ? ? "manual" , desc , minutes ? ? null , kcal ? ? null ] ;
if ( opts ) {
const actCols = ( await select ( "PRAGMA table_info(activity_log)" ) ) . map ( c => c . name ) ;
if ( actCols . includes ( "activity_type" ) ) { cols . push ( "activity_type" ) ; vals . push ( opts . activity _type ? ? "manual" ) ; }
if ( actCols . includes ( "sets" ) && opts . sets ) { cols . push ( "sets" ) ; vals . push ( opts . sets ) ; }
if ( actCols . includes ( "reps" ) && opts . reps ) { cols . push ( "reps" ) ; vals . push ( opts . reps ) ; }
if ( actCols . includes ( "distance_m" ) && opts . distance _m ) { cols . push ( "distance_m" ) ; vals . push ( opts . distance _m ) ; }
}
const placeholders = cols . map ( ( _ , i ) => ` ? ${ i + 1 } ` ) . join ( "," ) ;
await execute ( ` INSERT INTO activity_log ( ${ cols . join ( "," ) } ) VALUES ( ${ placeholders } ) ` , vals ) ;
2026-05-13 10:21:24 +03:00
}
async function deleteActivity ( id ) {
await execute ( "DELETE FROM activity_log WHERE id = ?1" , [ id ] ) ;
}
// ---------------------------------------------------------------------------
// Supplements
// ---------------------------------------------------------------------------
async function getSupplements ( ) {
return select ( "SELECT * FROM supplements ORDER BY name COLLATE NOCASE" ) ;
}
async function addSupplement ( s ) {
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
const cols = [
"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_b5_mg" , "vit_b6_mg" ,
"vit_b7_mcg" , "vit_b12_mcg" , "folate_mcg" ,
"calcium_mg" , "iron_mg" , "magnesium_mg" , "zinc_mg" , "copper_mg" ,
"selenium_mcg" , "potassium_mg" , "sodium_mg" , "phosphorus_mg" ,
"iodine_mg" , "chromium_mg" , "manganese_mg" , "molybdenum_mcg" ,
"silicon_mg" , "vanadium_mg" , "lutein_mg" , "lycopene_mg" , "carotene_mcg" ,
"fish_oil_mg" , "omega3_mg" , "epa_mg" , "dha_mg" , "omega6_mg" , "omega9_mg" ,
"ginseng_mg" , "resveratrol_mg" , "coq10_mg" ,
] ;
const placeholders = cols . map ( ( _ , i ) => ` ? ${ i + 1 } ` ) . join ( "," ) ;
const vals = cols . map ( c => s [ c ] ? ? null ) ;
2026-05-13 10:21:24 +03:00
const r = await execute (
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
` INSERT INTO supplements ( ${ cols . join ( "," ) } ) VALUES ( ${ placeholders } ) ` ,
vals
2026-05-13 10:21:24 +03:00
) ;
return r . lastInsertId ;
}
async function getSupplement ( id ) {
const rows = await select ( "SELECT * FROM supplements WHERE id = ?1" , [ id ] ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function deleteSupplement ( id ) {
await execute ( "DELETE FROM supplement_log WHERE supplement_id = ?1" , [ id ] ) ;
await execute ( "DELETE FROM supplements WHERE id = ?1" , [ id ] ) ;
}
async function logSupplement ( date , time , supplementId , doseG ) {
await execute (
"INSERT INTO supplement_log (date, time, supplement_id, dose_g) VALUES (?1,?2,?3,?4)" ,
[ date , time , supplementId , doseG ? ? null ]
) ;
}
async function getSupplementLogForDate ( date ) {
return select (
` SELECT sl.*, s.name as supplement_name, s.daily_dose_g
FROM supplement _log sl
JOIN supplements s ON sl . supplement _id = s . id
WHERE sl . date = ? 1 ORDER BY sl . time ASC ` ,
[ date ]
) ;
}
async function deleteSupplementLog ( id ) {
await execute ( "DELETE FROM supplement_log WHERE id = ?1" , [ id ] ) ;
}
async function getSupplementTotalsForDate ( date ) {
const rows = await select (
` SELECT
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _a _mcg ) as vit _a _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _c _mg ) as vit _c _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _d _mcg ) as vit _d _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _e _mg ) as vit _e _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _k _mcg ) as vit _k _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b1 _mg ) as vit _b1 _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b2 _mg ) as vit _b2 _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b3 _mg ) as vit _b3 _mg ,
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b5 _mg ) as vit _b5 _mg ,
2026-05-13 10:21:24 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b6 _mg ) as vit _b6 _mg ,
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b7 _mcg ) as vit _b7 _mcg ,
2026-05-13 10:21:24 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . vit _b12 _mcg ) as vit _b12 _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . folate _mcg ) as folate _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . calcium _mg ) as calcium _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . iron _mg ) as iron _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . magnesium _mg ) as magnesium _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . zinc _mg ) as zinc _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . copper _mg ) as copper _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . selenium _mcg ) as selenium _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . potassium _mg ) as potassium _mg ,
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . sodium _mg ) as sodium _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . chromium _mg ) as chromium _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . manganese _mg ) as manganese _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . molybdenum _mcg ) as molybdenum _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . iodine _mg ) as iodine _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . phosphorus _mg ) as phosphorus _mg ,
Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . carotene _mcg ) as carotene _mcg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . omega3 _mg ) as omega3 _mg ,
2026-05-29 15:21:59 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . epa _mg ) as epa _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . dha _mg ) as dha _mg ,
Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
SUM ( COALESCE ( sl . dose _g , 1 ) * s . omega6 _mg ) as omega6 _mg ,
SUM ( COALESCE ( sl . dose _g , 1 ) * s . omega9 _mg ) as omega9 _mg
2026-05-13 10:21:24 +03:00
FROM supplement _log sl
JOIN supplements s ON sl . supplement _id = s . id
WHERE sl . date = ? 1 ` ,
[ date ]
) ;
return rows [ 0 ] || { } ;
}
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
// ---------------------------------------------------------------------------
// Serving units
// ---------------------------------------------------------------------------
async function getServingUnits ( ) {
return select ( "SELECT * FROM serving_units ORDER BY id" ) ;
}
async function getUnitsForProduct ( productId ) {
// Returns product-specific units (with their grams_per_unit) + universal units
return select (
` SELECT su.*, COALESCE(pu.grams_per_unit, su.grams_equivalent) as grams_per_unit
FROM serving _units su
LEFT JOIN product _units pu ON pu . unit _id = su . id AND pu . product _id = ? 1
WHERE su . category = 'universal' OR pu . id IS NOT NULL
ORDER BY CASE WHEN pu . id IS NOT NULL THEN 0 ELSE 1 END , su . id ` ,
[ productId ]
) ;
}
2026-05-13 10:21:24 +03:00
// ---------------------------------------------------------------------------
// Analytics
// ---------------------------------------------------------------------------
async function getFoodTotalsForDate ( date ) {
const rows = await 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 } ;
}
async function getActivityTotalsForDate ( date ) {
2026-05-25 02:33:04 +03:00
const manual = await select (
2026-05-13 10:21:24 +03:00
"SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = ?1" ,
[ date ]
) ;
2026-05-25 02:33:04 +03:00
const gps = await select (
"SELECT SUM(total_kcal) as kcal FROM gps_sessions WHERE date = ?1 AND total_kcal IS NOT NULL" ,
[ date ]
) ;
const manualKcal = manual [ 0 ] ? . kcal ? ? 0 ;
const gpsKcal = gps [ 0 ] ? . kcal ? ? 0 ;
return { kcal : manualKcal + gpsKcal , minutes : manual [ 0 ] ? . minutes ? ? 0 } ;
2026-05-13 10:21:24 +03:00
}
2026-09-22 11:12:24 +03:00
// N calendar days ending at endDate (default today), range (end-N, end] = exactly N days.
async function getDailyFoodTotals ( days , endDate ) {
const end = endDate || "now" ;
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
return select (
` SELECT date, SUM(kcal) as kcal, SUM(protein_g) as protein_g,
SUM ( fat _g ) as fat _g , SUM ( carbs _g ) as carbs _g
2026-09-22 11:12:24 +03:00
FROM food _log WHERE date > date ( ? 2 , ? 1 ) AND date <= date ( ? 2 )
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
GROUP BY date ORDER BY date ASC ` ,
2026-09-22 11:12:24 +03:00
[ ` - ${ days ? ? 30 } days ` , end ]
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
) ;
}
2026-09-22 11:12:24 +03:00
async function getDailyActivityTotals ( days , endDate ) {
Physics settings in UI, recipe modal cleanup, weight parseFloat fixes
Settings:
- Aero drag (CdA), air density, pedal efficiency now in GPS config
- Human-readable names: "Аэросопротивление", "Плотность воздуха", "КПД педалирования"
- Saved to localStorage, used in computeStats
Bug fixes:
- Recipe detail: overlay.remove without popHandler cleanup → now uses history.back()
- weightKg parseFloat in startRecording and importGPX (string "88" bug)
- profile.weight_kg stored as number not string
- getDailyActivityTotals: separate params for UNION ALL subqueries
Docs:
- PROJECT_MAP.md — full project structure, DB schema, screens, formulas
- README.md — overview, build instructions, data sources
2026-05-26 02:14:43 +03:00
const offset = ` - ${ days ? ? 30 } days ` ;
2026-09-22 11:12:24 +03:00
const end = endDate || "now" ;
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
return select (
2026-05-25 02:33:04 +03:00
` SELECT date, SUM(kcal) as kcal FROM (
2026-09-22 11:12:24 +03:00
SELECT date , kcal FROM activity _log WHERE date > date ( ? 2 , ? 1 ) AND date <= date ( ? 2 )
2026-05-25 02:33:04 +03:00
UNION ALL
2026-09-22 11:12:24 +03:00
SELECT date , total _kcal as kcal FROM gps _sessions WHERE date > date ( ? 2 , ? 1 ) AND date <= date ( ? 2 ) AND total _kcal IS NOT NULL
2026-05-25 02:33:04 +03:00
) GROUP BY date ORDER BY date ASC ` ,
2026-09-22 11:12:24 +03:00
[ offset , end ]
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
) ;
}
2026-05-13 10:21:24 +03:00
// ---------------------------------------------------------------------------
// GPS sessions & points
// ---------------------------------------------------------------------------
async function createGpsSession ( date , startTime , activityType ) {
2026-05-14 14:17:57 +03:00
const id = Date . now ( ) ;
2026-05-13 10:21:24 +03:00
await execute (
2026-05-14 14:17:57 +03:00
` INSERT INTO gps_sessions (id, date, start_time, activity_type, source)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , 'recorded' ) ` ,
[ id , date , startTime , activityType ]
2026-05-13 10:21:24 +03:00
) ;
2026-05-14 14:17:57 +03:00
return id ;
2026-05-13 10:21:24 +03:00
}
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
// Column list cached after first call (avoids PRAGMA per insert)
let _gpsPointCols = null ;
async function addGpsPoint ( sessionId , ts , lat , lon , altGps , speedMs , seq , bleData , sensorData ) {
// Build INSERT dynamically based on existing columns (avoids crash if migration hasn't run)
if ( ! _gpsPointCols ) {
try {
const info = await select ( "PRAGMA table_info(gps_points)" ) ;
_gpsPointCols = new Set ( info . map ( r => r . name ) ) ;
} catch { _gpsPointCols = new Set ( [ "session_id" , "ts" , "lat" , "lon" , "altitude_gps_m" , "speed_ms" , "seq" ] ) ; }
}
const cols = [ "session_id" , "ts" , "lat" , "lon" , "altitude_gps_m" , "speed_ms" , "seq" ] ;
const vals = [ sessionId , ts , lat , lon , altGps ? ? null , speedMs ? ? null , seq ? ? null ] ;
const extra = [
[ "heart_rate" , bleData ? . hr ] , [ "spo2" , bleData ? . spo2 ] ,
[ "bp_sys" , bleData ? . bp _sys ] , [ "bp_dia" , bleData ? . bp _dia ] ,
[ "accel_rms" , sensorData ? . accel _rms ] , [ "accel_max" , sensorData ? . accel _max ] ,
[ "gyro_pitch" , sensorData ? . gyro _pitch ] , [ "gyro_yaw" , sensorData ? . gyro _yaw ] ,
2026-07-31 22:11:55 +03:00
[ "bearing_deg" , sensorData ? . bearing ] , [ "accuracy_m" , sensorData ? . accuracy ] ,
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
] ;
for ( const [ col , val ] of extra ) {
if ( _gpsPointCols . has ( col ) ) { cols . push ( col ) ; vals . push ( val ? ? null ) ; }
}
const ph = cols . map ( ( _ , i ) => ` ? ${ i + 1 } ` ) . join ( "," ) ;
await execute ( ` INSERT INTO gps_points ( ${ cols . join ( "," ) } ) VALUES ( ${ ph } ) ` , vals ) ;
2026-05-13 10:21:24 +03:00
}
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
// Bulk insert native points (recovery). Multi-row INSERT in chunks — MUCH faster
// than per-point addGpsPoint (one IPC per ~75 rows instead of per row). `points`
// are native point objects {ts, lat, lon, altitude, speed_ms, bearing, accel_*, gyro_*}.
// Returns the number of rows inserted. seq continues from startSeq.
async function addGpsPointsBulk ( sessionId , points , startSeq ) {
if ( ! points || ! points . length ) return 0 ;
if ( ! _gpsPointCols ) {
try { const info = await select ( "PRAGMA table_info(gps_points)" ) ; _gpsPointCols = new Set ( info . map ( r => r . name ) ) ; }
catch { _gpsPointCols = new Set ( [ "session_id" , "ts" , "lat" , "lon" , "altitude_gps_m" , "speed_ms" , "seq" ] ) ; }
}
const base = [ "session_id" , "ts" , "lat" , "lon" , "altitude_gps_m" , "speed_ms" , "seq" ] ;
2026-07-31 22:11:55 +03:00
const extra = [ "accel_rms" , "accel_max" , "gyro_pitch" , "gyro_yaw" , "bearing_deg" , "accuracy_m" ] . filter ( c => _gpsPointCols . has ( c ) ) ;
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
const allCols = base . concat ( extra ) ;
const perRow = allCols . length ;
const maxRows = Math . max ( 1 , Math . floor ( 900 / perRow ) ) ; // stay under SQLite bind-var limit
let seq = startSeq || 0 ;
let inserted = 0 ;
for ( let i = 0 ; i < points . length ; i += maxRows ) {
const chunk = points . slice ( i , i + maxRows ) ;
const rowSql = [ ] ;
const vals = [ ] ;
let ph = 1 ;
for ( const p of chunk ) {
seq ++ ;
const row = [ sessionId , p . ts , p . lat , p . lon , p . altitude ? ? null , p . speed _ms ? ? null , seq ] ;
2026-07-31 22:11:55 +03:00
for ( const c of extra ) row . push ( c === "bearing_deg" ? ( p . bearing ? ? null ) : c === "accuracy_m" ? ( p . accuracy ? ? null ) : ( p [ c ] ? ? null ) ) ;
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
rowSql . push ( "(" + row . map ( ( ) => ` ? ${ ph ++ } ` ) . join ( "," ) + ")" ) ;
vals . push ( ... row ) ;
}
await execute ( ` INSERT INTO gps_points ( ${ allCols . join ( "," ) } ) VALUES ${ rowSql . join ( "," ) } ` , vals ) ;
inserted += chunk . length ;
}
return inserted ;
}
2026-09-10 15:26:05 +03:00
// Gap-fill (BRouter): persist the routed dropout points as estimated=1, reversibly.
// `points` is the full fillGaps result array (real + estimated interleaved, ordered).
// Each estimated point gets seq = the preceding real point's seq, so the existing
// ORDER BY COALESCE(seq,999999), ts places it inside the gap; ts is already interpolated.
// Original points are untouched → revertGapFill removes only estimated=1.
async function saveGapFill ( sessionId , points ) {
let lastSeq = 0 , toInsert = [ ] ;
for ( let p of points ) {
if ( p . estimated ) toInsert . push ( { ts : p . ts , lat : p . lat , lon : p . lon , seq : lastSeq } ) ;
else if ( p . seq != null ) lastSeq = p . seq ;
}
if ( ! toInsert . length ) return 0 ;
let BATCH = 120 ; // 5 params/row → under SQLite bind-var limit
for ( let i = 0 ; i < toInsert . length ; i += BATCH ) {
let chunk = toInsert . slice ( i , i + BATCH ) ;
let rowSql = [ ] , vals = [ ] , ph = 1 ;
for ( let p of chunk ) {
rowSql . push ( ` (? ${ ph } ,? ${ ph + 1 } ,? ${ ph + 2 } ,? ${ ph + 3 } ,? ${ ph + 4 } ,1) ` ) ;
vals . push ( sessionId , p . ts , p . lat , p . lon , p . seq ) ; ph += 5 ;
}
await execute ( ` INSERT INTO gps_points (session_id, ts, lat, lon, seq, estimated) VALUES ${ rowSql . join ( "," ) } ` , vals ) ;
}
2026-09-22 20:58:06 +03:00
// Re-sequence the WHOLE session by ts so getGpsPoints (ORDER BY seq,ts) returns the true
// order. Without this, on merged/split tracks the real points have seq=NULL, the inserted
// gap-fill points get seq=0, and they sort to the FRONT — a straight line from the start to
// the gap ("returns to the beginning"). Re-numbering by ts is a no-op for dense tracks and
// fixes NULL/mixed ones. Batched CASE update (like saveSmoothedTrack) to beat the IPC/bind cost.
let ordered = await select ( "SELECT id FROM gps_points WHERE session_id=?1 ORDER BY ts ASC, id ASC" , [ sessionId ] ) ;
let SEQCHUNK = 300 ; // 2 params/row (id, seq) → under SQLite's 999 bind-var limit
for ( let i = 0 ; i < ordered . length ; i += SEQCHUNK ) {
let chunk = ordered . slice ( i , i + SEQCHUNK ) ;
let when = [ ] , ids = [ ] , params = [ ] ;
for ( let k = 0 ; k < chunk . length ; k ++ ) {
params . push ( chunk [ k ] . id ) ; let idi = params . length ;
params . push ( i + k + 1 ) ; let sqi = params . length ;
when . push ( ` WHEN ? ${ idi } THEN ? ${ sqi } ` ) ;
ids . push ( ` ? ${ idi } ` ) ;
}
await execute ( ` UPDATE gps_points SET seq=CASE id ${ when . join ( " " ) } END WHERE id IN ( ${ ids . join ( "," ) } ) ` , params ) ;
}
2026-09-10 15:26:05 +03:00
await execute ( "UPDATE gps_sessions SET gap_filled=1 WHERE id=?1" , [ sessionId ] ) ;
return toInsert . length ;
}
async function revertGapFill ( sessionId ) {
await execute ( "DELETE FROM gps_points WHERE session_id=?1 AND estimated=1" , [ sessionId ] ) ;
await execute ( "UPDATE gps_sessions SET gap_filled=0 WHERE id=?1" , [ sessionId ] ) ;
}
2026-09-14 12:14:04 +03:00
// Persist the per-point "walking" flag (walkIds = point ids in walking runs). Used by the
// track colour, the ridden-roads overlay (rebuild_roads) and the ride-graph router.
async function setWalkFlags ( sessionId , walkIds ) {
await execute ( "UPDATE gps_points SET walk=0 WHERE session_id=?1" , [ sessionId ] ) ;
let CHUNK = 200 ;
for ( let i = 0 ; i < walkIds . length ; i += CHUNK ) {
let ch = walkIds . slice ( i , i + CHUNK ) ;
let ph = ch . map ( ( _ , k ) => "?" + ( k + 1 ) ) . join ( "," ) ;
await execute ( ` UPDATE gps_points SET walk=1 WHERE id IN ( ${ ph } ) ` , ch ) ;
}
}
2026-09-07 13:53:40 +03:00
async function finishGpsSession ( id , endTime , distM , kcal , avgSpd , maxSpd , elevGain , elevLoss , pauses ) {
Health-diet.ru products (24K), GPS session safety, build.sh upload fix
Products:
- health-diet.ru scraper (Tampermonkey) + import script
- 11469 products with 37 nutrients, Dodo 258, McDonalds 619, KFC 361, Ashan 2000
- Rust import_seed_products: dynamic column list, auto-create missing columns
- Seed version bumped to 10
GPS session safety:
- Separate gps_points_{ts}.jsonl per session, deleted only after DB save
- Toast on DB errors, NaN guard in finishGpsSession
Other:
- build.sh: router/VPS upload only with -u flag
- Orientation buttons bottom-right, weather via Yandex Pogoda
2026-06-28 23:17:51 +03:00
// Guard against NaN — SQLite may reject or corrupt NaN values
const safe = v => ( v != null && ! isNaN ( v ) ) ? v : null ;
2026-09-07 13:53:40 +03:00
// pauses: array of manual-pause intervals; store as JSON. undefined = leave column as-is.
const hasPauses = pauses !== undefined ;
const pausesJson = hasPauses ? JSON . stringify ( pauses || [ ] ) : null ;
2026-05-13 10:21:24 +03:00
await execute (
` UPDATE gps_sessions SET end_time=?1, total_distance_m=?2, total_kcal=?3,
2026-09-07 13:53:40 +03:00
avg _speed _kmh = ? 4 , max _speed _kmh = ? 5 , elevation _gain _m = ? 6 , elevation _loss _m = ? 7 $ { hasPauses ? ", pauses=?9" : "" }
2026-05-13 10:21:24 +03:00
WHERE id = ? 8 ` ,
2026-09-07 13:53:40 +03:00
hasPauses
? [ endTime , safe ( distM ) , safe ( kcal ) , safe ( avgSpd ) , safe ( maxSpd ) , safe ( elevGain ) , safe ( elevLoss ) , id , pausesJson ]
: [ endTime , safe ( distM ) , safe ( kcal ) , safe ( avgSpd ) , safe ( maxSpd ) , safe ( elevGain ) , safe ( elevLoss ) , id ]
2026-05-13 10:21:24 +03:00
) ;
}
2026-07-31 22:11:55 +03:00
// Persist a de-jittered (smoothed) variant: write lat_smoothed/lon_smoothed per
// point (original lat/lon untouched) and flag the session. Reversible via
// setSessionSmoothed(id, false). `smoothedPoints` carry each point's `id` + the
// smoothed `lat`/`lon`.
async function saveSmoothedTrack ( sessionId , smoothedPoints , window , iters , onProgress ) {
Track detail de-jitter: fix overlay stacking, native dialogs, slow save
Fixes to the smoothing/routes UI reported on device:
- Blank detail after save/revert: showSessionDetail was called from inside the
open detail modal, stacking a second overlay; querySelector("#sd-map") then
hit the stale, already-initialized Leaflet map and threw, aborting the rest
of the render (map + speed chart). New reopenDetail() closes the current
detail (and its fullscreen-map child) and reopens fresh, like recalc/refine.
- Native confirm()/prompt() replaced with in-app appConfirm/appPrompt dialogs
(winStack + history integrated, hardware back cancels). Used for smoothed
save, route delete, route rename.
- Slow smoothed save: one UPDATE per point (4000+ IPC round-trips) replaced
with a chunked CASE update — 18 statements for 4366 points, params kept
under SQLite's 999 limit. Verified value-for-value on a copy DB.
- Closing the fullscreen map now also closes the smoothing panel (it is a
fixed element that otherwise lingered over the collapsed map).
- "≈" button active state now reflects panel-open only, not session.smoothed
(it looked permanently pressed after a save).
Docs: README + PROJECT_MAP updated for saved routes, de-jitter, accuracy_m,
routes table and new gps_points/gps_sessions columns. Version 0.1.393.
2026-07-31 23:51:20 +03:00
// One UPDATE per point over IPC is thousands of round-trips (slow, ~1 pt/ms).
// Batch into a single CASE-per-chunk UPDATE — ~15 statements instead of 4000+.
// Chunk keeps bound params under SQLite's 999 limit (250 pts × 3 params = 750).
let pts = smoothedPoints . filter ( p => p . id != null ) ;
let CHUNK = 250 ;
for ( let start = 0 ; start < pts . length ; start += CHUNK ) {
let chunk = pts . slice ( start , start + CHUNK ) ;
let latWhen = [ ] , lonWhen = [ ] , ids = [ ] , params = [ ] ;
for ( let p of chunk ) {
params . push ( p . id ) ; let idi = params . length ;
params . push ( p . lat ) ; let lati = params . length ;
params . push ( p . lon ) ; let loni = params . length ;
latWhen . push ( ` WHEN ? ${ idi } THEN ? ${ lati } ` ) ;
lonWhen . push ( ` WHEN ? ${ idi } THEN ? ${ loni } ` ) ;
ids . push ( ` ? ${ idi } ` ) ;
}
await execute (
` UPDATE gps_points SET lat_smoothed=CASE id ${ latWhen . join ( " " ) } END, ` +
` lon_smoothed=CASE id ${ lonWhen . join ( " " ) } END WHERE id IN ( ${ ids . join ( "," ) } ) ` ,
params
) ;
if ( onProgress ) { try { onProgress ( Math . min ( start + CHUNK , pts . length ) , pts . length ) ; } catch { } }
2026-07-31 22:11:55 +03:00
}
await execute ( "UPDATE gps_sessions SET smoothed=1, smooth_window=?1, smooth_iters=?2 WHERE id=?3" , [ window , iters , sessionId ] ) ;
}
async function setSessionSmoothed ( sessionId , on ) {
await execute ( "UPDATE gps_sessions SET smoothed=?1 WHERE id=?2" , [ on ? 1 : 0 , sessionId ] ) ;
}
2026-07-03 22:19:58 +03:00
// NOTE: tauri-plugin-sql corrupts large i64 ids when returned as JS numbers
// (off by ±1). Always read/pass session ids via the text `id_str` column.
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
async function getGpsSessions ( limit , offset ) {
2026-05-13 10:21:24 +03:00
return select (
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
"SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions ORDER BY date DESC, start_time DESC, id DESC LIMIT ?1 OFFSET ?2" ,
[ limit ? ? 50 , offset ? ? 0 ]
2026-05-13 10:21:24 +03:00
) ;
}
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
async function getGpsSessionsForDate ( date ) {
return select (
2026-07-03 22:19:58 +03:00
"SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE date = ?1 ORDER BY start_time ASC" ,
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
[ date ]
) ;
}
2026-05-13 10:21:24 +03:00
async function getGpsSession ( id ) {
2026-07-03 22:19:58 +03:00
const rows = await select ( "SELECT *, CAST(id AS TEXT) AS id_str FROM gps_sessions WHERE id = ?1" , [ id ] ) ;
2026-09-07 13:53:40 +03:00
if ( ! rows . length ) return null ;
const s = rows [ 0 ] ;
if ( typeof s . pauses === "string" ) { try { s . pauses = JSON . parse ( s . pauses ) ; } catch { s . pauses = [ ] ; } }
2026-09-07 17:10:16 +03:00
if ( typeof s . wind _marks === "string" ) { try { s . wind _marks = JSON . parse ( s . wind _marks ) ; } catch { s . wind _marks = [ ] ; } }
2026-09-07 13:53:40 +03:00
return s ;
2026-05-13 10:21:24 +03:00
}
2026-09-07 17:10:16 +03:00
// Save headwind labels for a session (JSON). Kept separate from finishGpsSession so
// recompute paths preserve marks (they never call this). Marks are ground truth for
// the wind estimate — they do not affect distance/kcal/power.
async function setSessionWindMarks ( id , marks ) {
await execute ( "UPDATE gps_sessions SET wind_marks=?1 WHERE id=?2" , [ JSON . stringify ( marks || [ ] ) , id ] ) ;
}
2026-05-13 10:21:24 +03:00
async function getGpsPoints ( sessionId ) {
return select ( "SELECT * FROM gps_points WHERE session_id = ?1 ORDER BY COALESCE(seq, 999999), ts ASC" , [ sessionId ] ) ;
}
async function deleteGpsSession ( id ) {
await execute ( "DELETE FROM gps_points WHERE session_id = ?1" , [ id ] ) ;
await execute ( "DELETE FROM gps_sessions WHERE id = ?1" , [ id ] ) ;
}
2026-09-18 00:08:25 +03:00
// Merge several sessions into one. Points from every session move into the EARLIEST
// (by start time) session; each moved point keeps its original session in
// orig_session_id (so a future "split" can undo this), the target's end_time extends to
// the latest, and the other session rows are deleted. Ids are text (large i64 read back
// off-by-one), passed as strings and CAST to INTEGER for the id/session_id columns.
// Returns the surviving (target) session id_str. Caller recomputes stats afterwards.
async function mergeGpsSessions ( idStrs ) {
if ( ! Array . isArray ( idStrs ) || idStrs . length < 2 ) return null ;
const sessions = [ ] ;
for ( const id of idStrs ) { const s = await getGpsSession ( id ) ; if ( s ) sessions . push ( s ) ; }
if ( sessions . length < 2 ) return null ;
const ord = ( s ) => ( s . date || "1970-01-01" ) + "T" + ( s . start _time || "00:00:00" ) ;
sessions . sort ( ( a , b ) => ord ( a ) < ord ( b ) ? - 1 : ord ( a ) > ord ( b ) ? 1 : 0 ) ;
const target = sessions [ 0 ] ;
const targetId = target . id _str ;
const endOf = ( s ) => ( s . date || "1970-01-01" ) + "T" + ( s . end _time || s . start _time || "00:00:00" ) ;
const latestEnd = sessions . reduce ( ( m , s ) => endOf ( s ) > endOf ( m ) ? s : m , target ) . end _time ;
// Tag the target's own points too, so orig_session_id is complete for a later split.
await execute ( "UPDATE gps_points SET orig_session_id = CAST(?1 AS TEXT) WHERE session_id = CAST(?1 AS INTEGER) AND orig_session_id IS NULL" , [ targetId ] ) ;
for ( const s of sessions . slice ( 1 ) ) {
await execute (
"UPDATE gps_points SET orig_session_id = COALESCE(orig_session_id, CAST(session_id AS TEXT)), session_id = CAST(?1 AS INTEGER) WHERE session_id = CAST(?2 AS INTEGER)" ,
[ targetId , s . id _str ]
) ;
await execute ( "DELETE FROM gps_sessions WHERE id = CAST(?1 AS INTEGER)" , [ s . id _str ] ) ;
}
if ( latestEnd ) await execute ( "UPDATE gps_sessions SET end_time = ?1 WHERE id = CAST(?2 AS INTEGER)" , [ latestEnd , targetId ] ) ;
// Each source session numbered its points seq 1..N, so after the move the seq values
// collide and "ORDER BY seq" would interleave the tracks (measured: a 43 km "segment"
// and 6745 km/h passes). Clear seq so getGpsPoints falls back to timestamp order, which
// is the true order across the merged tracks.
await execute ( "UPDATE gps_points SET seq = NULL WHERE session_id = CAST(?1 AS INTEGER)" , [ targetId ] ) ;
return targetId ;
}
2026-09-21 12:36:03 +03:00
// Split one session in two at a chosen point. The second part (points whose ids are in
// part2Ids) moves to a NEW session that copies the original's date / activity / bike /
// source; the first part keeps the original session. Bike stays the same on both halves -
// the rider then reassigns the bike on a half and recomputes if the gear differed. Times
// are "HH:MM:SS". Returns the new (second-part) session id_str. Caller recomputes both.
async function splitGpsSession ( sessionId , part2Ids , part1EndTime , part2StartTime ) {
if ( ! Array . isArray ( part2Ids ) || part2Ids . length === 0 ) return null ;
const s = await getGpsSession ( sessionId ) ;
if ( ! s ) return null ;
const newId = Date . now ( ) ;
await execute (
` INSERT INTO gps_sessions (id, date, start_time, end_time, activity_type, source, bike_id)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 ) ` ,
[ newId , s . date , part2StartTime , s . end _time , s . activity _type , s . source ? ? "recorded" , s . bike _id ? ? null ]
) ;
let CHUNK = 200 ;
for ( let i = 0 ; i < part2Ids . length ; i += CHUNK ) {
let ch = part2Ids . slice ( i , i + CHUNK ) ;
let ph = ch . map ( ( _ , k ) => "?" + ( k + 2 ) ) . join ( "," ) ;
await execute ( ` UPDATE gps_points SET session_id = CAST(?1 AS INTEGER), seq = NULL WHERE id IN ( ${ ph } ) ` , [ newId , ... ch ] ) ;
}
if ( part1EndTime ) await execute ( "UPDATE gps_sessions SET end_time = ?1 WHERE id = CAST(?2 AS INTEGER)" , [ part1EndTime , sessionId ] ) ;
return String ( newId ) ;
}
2026-10-20 23:49:17 +03:00
// Undo a merge: split a merged session back into its original sessions by orig_session_id
// (merge preserved it on every point). The current session keeps its own original group; each
// other orig_session_id gets its session row recreated (metadata copied). Points are retagged
// (orig_session_id cleared, seq cleared → timestamp order). Returns the list of resulting
// session id_strs (incl. the current one), or null if this session isn't a merge. The caller
// recomputes stats + times for each (computeStats + finishGpsSession).
async function unmergeGpsSession ( sessionId ) {
const s = await getGpsSession ( sessionId ) ;
if ( ! s ) return null ;
const rows = await select (
"SELECT DISTINCT CAST(orig_session_id AS TEXT) AS o FROM gps_points WHERE session_id = CAST(?1 AS INTEGER) AND orig_session_id IS NOT NULL" ,
[ s . id _str ]
) ;
const origs = rows . map ( r => r . o ) . filter ( Boolean ) ;
if ( origs . length < 2 ) return null ; // not a merge — nothing to split
const resultIds = [ ] ;
for ( const oid of origs ) {
if ( oid !== s . id _str ) {
await execute (
` INSERT INTO gps_sessions (id, date, start_time, end_time, activity_type, source, bike_id)
VALUES ( CAST ( ? 1 AS INTEGER ) , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 ) ` ,
[ oid , s . date , s . start _time , s . end _time , s . activity _type , s . source ? ? "recorded" , s . bike _id ? ? null ]
) ;
await execute (
"UPDATE gps_points SET session_id = CAST(?1 AS INTEGER), orig_session_id = NULL, seq = NULL WHERE session_id = CAST(?2 AS INTEGER) AND orig_session_id = CAST(?1 AS TEXT)" ,
[ oid , s . id _str ]
) ;
} else {
await execute (
"UPDATE gps_points SET orig_session_id = NULL, seq = NULL WHERE session_id = CAST(?1 AS INTEGER) AND orig_session_id = CAST(?1 AS TEXT)" ,
[ s . id _str ]
) ;
}
resultIds . push ( oid ) ;
}
// Fix each piece's date/times from its own points (merge had stretched them to the whole span).
for ( const id of resultIds ) {
const ts = await select ( "SELECT MIN(ts) AS a, MAX(ts) AS b FROM gps_points WHERE session_id = CAST(?1 AS INTEGER)" , [ id ] ) ;
const a = ts [ 0 ] && ts [ 0 ] . a ;
if ( a ) {
const hm = ( t ) => new Date ( t ) . toTimeString ( ) . slice ( 0 , 8 ) ;
await execute ( "UPDATE gps_sessions SET date = ?1, start_time = ?2, end_time = ?3 WHERE id = CAST(?4 AS INTEGER)" ,
[ new Date ( a ) . toISOString ( ) . slice ( 0 , 10 ) , hm ( a ) , hm ( ts [ 0 ] . b ) , id ] ) ;
}
}
return resultIds ;
}
// How many distinct original sessions are glued into this one (>1 ⇒ it's a merge that can be split).
async function countMergedOrigins ( sessionId ) {
const s = await getGpsSession ( sessionId ) ;
if ( ! s ) return 0 ;
const r = await select (
"SELECT COUNT(DISTINCT orig_session_id) AS c FROM gps_points WHERE session_id = CAST(?1 AS INTEGER) AND orig_session_id IS NOT NULL" ,
[ s . id _str ]
) ;
return ( r [ 0 ] && r [ 0 ] . c ) || 0 ;
}
2026-05-13 10:21:24 +03:00
async function importGpxSession ( session , points ) {
await execute (
` INSERT INTO gps_sessions (date, start_time, end_time, activity_type,
total _distance _m , total _kcal , avg _speed _kmh , max _speed _kmh ,
elevation _gain _m , elevation _loss _m , source )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 , ? 11 ) ` ,
[ session . date , session . start _time , session . end _time , session . activity _type ,
session . total _distance _m , session . total _kcal , session . avg _speed _kmh ,
session . max _speed _kmh , session . elevation _gain _m , session . elevation _loss _m ,
session . source ? ? "gpx_import" ]
) ;
2026-07-03 22:19:58 +03:00
// Get id of just-inserted session by matching unique fields.
// CAST to TEXT — numeric i64 id is corrupted by the SQL plugin; use string
// sid for point inserts (SQLite coerces it to the INTEGER session_id column).
2026-05-14 14:17:57 +03:00
const rows = await select (
2026-07-03 22:19:58 +03:00
"SELECT CAST(id AS TEXT) AS id FROM gps_sessions WHERE date=?1 AND start_time=?2 AND source=?3 ORDER BY id DESC LIMIT 1" ,
2026-05-14 14:17:57 +03:00
[ session . date , session . start _time , session . source ? ? "gpx_import" ]
) ;
2026-05-13 10:21:24 +03:00
const sid = rows [ 0 ] . id ;
// Batch insert points — build multi-row INSERT to avoid 1 IPC per point
const BATCH = 50 ;
for ( let i = 0 ; i < points . length ; i += BATCH ) {
const chunk = points . slice ( i , i + BATCH ) ;
const placeholders = [ ] ;
const vals = [ ] ;
let idx = 1 ;
for ( const p of chunk ) {
placeholders . push ( ` (? ${ idx } ,? ${ idx + 1 } ,? ${ idx + 2 } ,? ${ idx + 3 } ,? ${ idx + 4 } ,? ${ idx + 5 } ,? ${ idx + 6 } ) ` ) ;
vals . push ( sid , p . ts , p . lat , p . lon , p . altitude ? ? null , p . speed ? ? null , p . seq ? ? null ) ;
idx += 7 ;
}
await execute (
` INSERT INTO gps_points (session_id, ts, lat, lon, altitude_gps_m, speed_ms, seq) VALUES ${ placeholders . join ( "," ) } ` ,
vals
) ;
}
return sid ;
}
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
// ---------------------------------------------------------------------------
// Bikes
// ---------------------------------------------------------------------------
async function getBikes ( ) {
return select ( "SELECT * FROM bikes ORDER BY is_active DESC, name" ) ;
}
async function getBike ( id ) {
const rows = await select ( "SELECT * FROM bikes WHERE id = ?1" , [ id ] ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function getActiveBike ( ) {
const rows = await select ( "SELECT * FROM bikes WHERE is_active = 1 LIMIT 1" ) ;
return rows . length ? rows [ 0 ] : null ;
}
async function saveBike ( data ) {
if ( data . is _active ) {
await execute ( "UPDATE bikes SET is_active = 0 WHERE is_active = 1" ) ;
}
const r = await execute (
` INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth,
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
cog _teeth , gear _count , surface _type , notes , is _active , tire _pressure _bar )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 ) ` ,
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
[ data . name , data . weight _kg ? ? null , data . wheel _diameter _inch ? ? 26 ,
data . chainring _teeth ? ? null , data . cog _teeth ? ? null ,
data . gear _count ? ? null , data . surface _type ? ? "mixed" ,
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
data . notes ? ? null , data . is _active ? 1 : 0 ,
data . tire _pressure _bar ? ? null ]
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
) ;
return r . lastInsertId ;
}
async function updateBike ( id , data ) {
if ( data . is _active ) {
await execute ( "UPDATE bikes SET is_active = 0 WHERE is_active = 1" ) ;
}
await execute (
` UPDATE bikes SET name=?1, weight_kg=?2, wheel_diameter_inch=?3,
chainring _teeth = ? 4 , cog _teeth = ? 5 , gear _count = ? 6 , surface _type = ? 7 ,
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
notes = ? 8 , is _active = ? 9 , tire _pressure _bar = ? 10 WHERE id = ? 11 ` ,
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
[ data . name , data . weight _kg ? ? null , data . wheel _diameter _inch ? ? 26 ,
data . chainring _teeth ? ? null , data . cog _teeth ? ? null ,
data . gear _count ? ? null , data . surface _type ? ? "mixed" ,
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
data . notes ? ? null , data . is _active ? 1 : 0 ,
data . tire _pressure _bar ? ? null , id ]
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
) ;
}
async function deleteBike ( id ) {
await execute ( "DELETE FROM bikes WHERE id = ?1" , [ id ] ) ;
}
async function updateSessionBike ( sessionId , bikeId ) {
await execute ( "UPDATE gps_sessions SET bike_id = ?1 WHERE id = ?2" , [ bikeId , sessionId ] ) ;
}
2026-09-22 11:12:24 +03:00
// Store the rider's weight at ride time (set once when a recording finalises). A separate
// setter from finishGpsSession, which recompute also calls — recompute must NOT overwrite it.
async function setSessionRiderWeight ( sessionId , kg ) {
if ( kg == null || isNaN ( kg ) ) return ;
await execute ( "UPDATE gps_sessions SET rider_weight_kg = ?1 WHERE id = ?2" , [ kg , sessionId ] ) ;
}
Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.
- gps.js: GPS.hrKcalNet; startRecording/stopRecording 'workout' branches;
the HR poll appends [relSec,bpm] samples.
- db.js: gps_sessions.hr_samples column + setSessionHrSamples.
- main.js: 'Зарядка (пульс)' option; renderWorkoutInline (map-less live
screen: big HR, timer, kcal, connect-sensor button); renderWorkoutDetail
(HR chart by zones, load, avg/max).
- tools/hr_log.py: background HR logger (standard 0x180D, no pairing).
Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
// HR-only "workout" session: store the heart-rate time series as JSON [[relSec,bpm],...].
async function setSessionHrSamples ( sessionId , samplesJson ) {
await execute ( "UPDATE gps_sessions SET hr_samples = ?1 WHERE id = ?2" , [ samplesJson , sessionId ] ) ;
}
2026-07-07 00:28:27 +03:00
// Bulk-set terrain altitude (altitude_srtm_m) for points by id. rows: [{id, srtm}].
// Batched CASE update — values are numbers (ids small, no i64 corruption), safe to inline.
async function updatePointsSrtm ( rows ) {
const BATCH = 200 ;
for ( let i = 0 ; i < rows . length ; i += BATCH ) {
const batch = rows . slice ( i , i + BATCH ) . filter ( r => r . id != null && r . srtm != null && isFinite ( r . srtm ) ) ;
if ( ! batch . length ) continue ;
const cases = batch . map ( r => ` WHEN ${ parseInt ( r . id , 10 ) } THEN ${ Number ( r . srtm ) } ` ) . join ( " " ) ;
const ids = batch . map ( r => parseInt ( r . id , 10 ) ) . join ( "," ) ;
await execute ( ` UPDATE gps_points SET altitude_srtm_m = CASE id ${ cases } END WHERE id IN ( ${ ids } ) ` , [ ] ) ;
}
}
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
// ---------------------------------------------------------------------------
// Bike maintenance log
// ---------------------------------------------------------------------------
async function getBikeMaintenanceLog ( bikeId ) {
return select (
"SELECT * FROM bike_maintenance WHERE bike_id = ?1 ORDER BY date DESC" ,
[ bikeId ]
) ;
}
async function addBikeMaintenance ( bikeId , date , description , mileageKm , nextServiceKm , notes ) {
const r = await execute (
` INSERT INTO bike_maintenance (bike_id, date, description, mileage_km, next_service_km, notes)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 ) ` ,
[ bikeId , date , description , mileageKm ? ? null , nextServiceKm ? ? null , notes ? ? null ]
) ;
return r . lastInsertId ;
}
async function deleteBikeMaintenance ( id ) {
await execute ( "DELETE FROM bike_maintenance WHERE id = ?1" , [ id ] ) ;
}
async function getBikeTotalMileage ( bikeId ) {
const rows = await select (
"SELECT SUM(total_distance_m) as total FROM gps_sessions WHERE bike_id = ?1 AND total_distance_m IS NOT NULL" ,
[ bikeId ]
) ;
const totalM = rows [ 0 ] ? . total ? ? 0 ;
return Math . round ( totalM / 100 ) / 10 ; // km, 1 decimal
}
async function getBikeMaintenanceDue ( bikeId ) {
const mileage = await getBikeTotalMileage ( bikeId ) ;
const all = await getBikeMaintenanceLog ( bikeId ) ;
return all . filter ( e => e . next _service _km != null && mileage >= e . next _service _km ) ;
}
2026-05-27 17:45:50 +03:00
// ---------------------------------------------------------------------------
// Goal markers
// ---------------------------------------------------------------------------
async function getGoalMarkers ( ) {
return select ( "SELECT * FROM goal_markers ORDER BY created_at DESC" ) ;
}
async function addGoalMarker ( name , lat , lon , notes ) {
const r = await execute (
` INSERT INTO goal_markers (name, lat, lon, notes) VALUES (?1, ?2, ?3, ?4) ` ,
[ name , lat , lon , notes ? ? null ]
) ;
return r . lastInsertId ;
}
async function deleteGoalMarker ( id ) {
await execute ( "DELETE FROM goal_markers WHERE id = ?1" , [ id ] ) ;
}
2026-09-12 22:55:51 +03:00
// ---------------------------------------------------------------------------
// Nogo areas — impassable spots the router must avoid (circle: lat/lon + radius)
// ---------------------------------------------------------------------------
async function getNogoAreas ( ) {
return select ( "SELECT * FROM nogo_areas ORDER BY created_at DESC" ) ;
}
async function addNogoArea ( lat , lon , radiusM , note ) {
await execute (
"INSERT INTO nogo_areas (lat, lon, radius_m, note) VALUES (?1, ?2, ?3, ?4)" ,
[ lat , lon , radiusM ? ? 25 , note ? ? null ]
) ;
let rows = await select ( "SELECT CAST(id AS TEXT) AS id FROM nogo_areas ORDER BY id DESC LIMIT 1" ) ;
return rows . length ? rows [ 0 ] . id : null ;
}
async function deleteNogoArea ( id ) {
await execute ( "DELETE FROM nogo_areas WHERE id = ?1" , [ id ] ) ;
}
2026-07-31 22:11:55 +03:00
// ---------------------------------------------------------------------------
// Saved routes (hand-drawn reference paths overlaid on the map)
// ---------------------------------------------------------------------------
async function getRoutes ( ) {
// Routes are few and small — load the blob to report a point count, drop it.
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
let rows = await select ( "SELECT id, name, color, created_at, points_json, waypoints_json FROM routes ORDER BY created_at DESC" ) ;
2026-07-31 22:11:55 +03:00
return rows . map ( r => {
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
let n = 0 , hasEle = false , wpts = null ;
Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.
- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
tappable altitude/grade points, elevation profile chart (steep >8% in red),
length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
drawn in a dedicated pane below the overlay pane, so a recorded track stays on
top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
getRoutes reports hasEle. Route points are now {lat, lon, ele?}.
Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
try { let pts = JSON . parse ( r . points _json ) || [ ] ; n = pts . length ; hasEle = pts . some ( p => p . ele != null ) ; } catch { }
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
// wpts = count of editable waypoints (routes built in the on-phone planner); null = imported
try { if ( r . waypoints _json ) { let w = JSON . parse ( r . waypoints _json ) ; wpts = ( w . waypoints || w ) . length ; } } catch { }
return { id : r . id , name : r . name , color : r . color , created _at : r . created _at , pts : n , hasEle , wpts } ;
2026-07-31 22:11:55 +03:00
} ) ;
}
async function getRoute ( id ) {
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
let rows = await select ( "SELECT * FROM routes WHERE id = ?1" , [ id ] ) ;
2026-07-31 22:11:55 +03:00
if ( ! rows . length ) return null ;
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
let r = rows [ 0 ] ;
2026-07-31 22:11:55 +03:00
let points = [ ] ;
try { points = JSON . parse ( r . points _json ) || [ ] ; } catch { }
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
// waypoints = { waypoints:[{lat,lon}], legMode:[...] } for planner-built routes; null otherwise
let waypoints = null ;
try { if ( r . waypoints _json ) waypoints = JSON . parse ( r . waypoints _json ) ; } catch { }
return { id : r . id , name : r . name , color : r . color , created _at : r . created _at , points , waypoints } ;
2026-07-31 22:11:55 +03:00
}
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
async function addRoute ( name , color , points , waypoints ) {
2026-09-11 01:25:28 +03:00
await execute (
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
"INSERT INTO routes (name, color, points_json, waypoints_json) VALUES (?1, ?2, ?3, ?4)" ,
[ name , color || "#e53935" , JSON . stringify ( points || [ ] ) , waypoints ? JSON . stringify ( waypoints ) : null ]
2026-07-31 22:11:55 +03:00
) ;
2026-09-11 01:25:28 +03:00
// lastInsertId comes back as an i64 the SQL plugin can corrupt by ±1 — re-read the id as
// text instead (same guard as importGpxSession), so the caller opens the right route.
let rows = await select ( "SELECT CAST(id AS TEXT) AS id FROM routes ORDER BY id DESC LIMIT 1" ) ;
return rows . length ? rows [ 0 ] . id : null ;
2026-07-31 22:11:55 +03:00
}
async function renameRoute ( id , name ) {
await execute ( "UPDATE routes SET name = ?1 WHERE id = ?2" , [ name , id ] ) ;
}
async function setRouteColor ( id , color ) {
await execute ( "UPDATE routes SET color = ?1 WHERE id = ?2" , [ color , id ] ) ;
}
async function deleteRoute ( id ) {
await execute ( "DELETE FROM routes WHERE id = ?1" , [ id ] ) ;
}
Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.
- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
tappable altitude/grade points, elevation profile chart (steep >8% in red),
length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
drawn in a dedicated pane below the overlay pane, so a recorded track stays on
top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
getRoutes reports hasEle. Route points are now {lat, lon, ele?}.
Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
// Overwrite a route's points (used to persist fetched elevations back into
// points_json — points are {lat, lon, ele?}).
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
async function updateRoutePoints ( id , points , waypoints ) {
if ( waypoints !== undefined ) {
await execute ( "UPDATE routes SET points_json = ?1, waypoints_json = ?2 WHERE id = ?3" ,
[ JSON . stringify ( points || [ ] ) , waypoints ? JSON . stringify ( waypoints ) : null , id ] ) ;
} else {
await execute ( "UPDATE routes SET points_json = ?1 WHERE id = ?2" , [ JSON . stringify ( points || [ ] ) , id ] ) ;
}
Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.
- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
tappable altitude/grade points, elevation profile chart (steep >8% in red),
length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
drawn in a dedicated pane below the overlay pane, so a recorded track stays on
top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
getRoutes reports hasEle. Route points are now {lat, lon, ele?}.
Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
}
2026-05-27 17:45:50 +03:00
// ---------------------------------------------------------------------------
// Food cost helper — most frequently eaten foods in last 7 days
// ---------------------------------------------------------------------------
async function getFrequentFoods ( days ) {
return select (
` SELECT product_name, kcal, amount_g,
CASE WHEN amount _g > 0 THEN ROUND ( kcal * 100.0 / amount _g ) ELSE kcal END as kcal _per _100g
FROM (
SELECT p . name as product _name , fl . kcal , fl . amount _g ,
COUNT ( * ) as cnt
FROM food _log fl
JOIN products p ON fl . product _id = p . id
WHERE fl . date >= date ( 'now' , ? 1 )
AND fl . kcal > 0 AND fl . amount _g > 0
GROUP BY p . name
ORDER BY cnt DESC
LIMIT 5
) ` ,
[ ` - ${ days ? ? 7 } days ` ]
) ;
}
// ---------------------------------------------------------------------------
// Closest approach of a session to a goal marker
// Returns {minDist, closestIdx} — computed in JS since SQLite can't do haversine
// ---------------------------------------------------------------------------
async function getGoalProgress ( markerId , sessionId ) {
// Fetch all sessions that got close to this marker, ordered by date
// We actually compute this in JS, but store the result concept here
// This function just returns all sessions with their closest approach to a marker
const sessions = await select (
` SELECT gs.id, gs.date, gs.start_time, gs.total_distance_m
FROM gps _sessions gs
WHERE gs . id != ? 1
ORDER BY gs . date DESC , gs . start _time DESC
LIMIT 20 ` ,
[ sessionId ]
) ;
return sessions ;
}
2026-05-13 10:21:24 +03:00
// Expose as globals — no ES module system, loaded via <script> tags.
// main.js accesses these as window.DB.xxx
window . DB = { } ;
window . DB . getProfileValue = getProfileValue ;
window . DB . setProfileValue = setProfileValue ;
window . DB . getFullProfile = getFullProfile ;
window . DB . addWeight = addWeight ;
window . DB . deleteWeight = deleteWeight ;
window . DB . getWeightsForDate = getWeightsForDate ;
window . DB . getLatestWeight = getLatestWeight ;
window . DB . getRecentWeights = getRecentWeights ;
window . DB . getSleepForDate = getSleepForDate ;
window . DB . saveSleep = saveSleep ;
window . DB . searchProducts = searchProducts ;
window . DB . getProduct = getProduct ;
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
async function getManualProducts ( ) {
return select ( "SELECT * FROM products WHERE source = 'manual' ORDER BY name" ) ;
}
window . DB . getManualProducts = getManualProducts ;
2026-05-13 10:21:24 +03:00
window . DB . addProduct = addProduct ;
2026-05-30 21:08:32 +03:00
window . DB . deleteProduct = deleteProduct ;
2026-05-31 00:48:23 +03:00
window . DB . getProductRefs = getProductRefs ;
Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
in name, any order), ranked full-substring > prefix > length. Fixes needing to
type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
= magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
window . DB . countUnlabeledUnusedProducts = countUnlabeledUnusedProducts ;
window . DB . cleanupUnlabeledProducts = cleanupUnlabeledProducts ;
2026-05-13 10:21:24 +03:00
window . DB . getFoodForDate = getFoodForDate ;
window . DB . addFoodManual = addFoodManual ;
window . DB . addFoodEntry = addFoodEntry ;
window . DB . updateFoodEntry = updateFoodEntry ;
window . DB . deleteFoodEntry = deleteFoodEntry ;
window . DB . getActivitiesForDate = getActivitiesForDate ;
window . DB . addActivity = addActivity ;
window . DB . deleteActivity = deleteActivity ;
window . DB . getSupplements = getSupplements ;
window . DB . getSupplement = getSupplement ;
window . DB . addSupplement = addSupplement ;
window . DB . deleteSupplement = deleteSupplement ;
window . DB . logSupplement = logSupplement ;
window . DB . getSupplementLogForDate = getSupplementLogForDate ;
window . DB . deleteSupplementLog = deleteSupplementLog ;
window . DB . getSupplementTotalsForDate = getSupplementTotalsForDate ;
window . DB . getFoodTotalsForDate = getFoodTotalsForDate ;
window . DB . getActivityTotalsForDate = getActivityTotalsForDate ;
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
window . DB . getServingUnits = getServingUnits ;
window . DB . getUnitsForProduct = getUnitsForProduct ;
window . DB . getDailyFoodTotals = getDailyFoodTotals ;
window . DB . getDailyActivityTotals = getDailyActivityTotals ;
2026-05-21 00:06:50 +03:00
window . DB . getCookingMethods = getCookingMethods ;
window . DB . getCookingMethod = getCookingMethod ;
window . DB . getRecipes = getRecipes ;
window . DB . getRecipe = getRecipe ;
window . DB . getRecipeIngredients = getRecipeIngredients ;
window . DB . saveRecipe = saveRecipe ;
window . DB . updateRecipe = updateRecipe ;
window . DB . deleteRecipe = deleteRecipe ;
window . DB . addRecipeIngredient = addRecipeIngredient ;
window . DB . deleteRecipeIngredient = deleteRecipeIngredient ;
window . DB . clearRecipeIngredients = clearRecipeIngredients ;
window . DB . calculateRecipeNutrition = calculateRecipeNutrition ;
АУП 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.
2026-06-02 18:39:09 +03:00
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 ;
2026-05-21 00:06:50 +03:00
window . DB . addFoodFromRecipe = addFoodFromRecipe ;
2026-05-13 10:21:24 +03:00
window . DB . createGpsSession = createGpsSession ;
window . DB . addGpsPoint = addGpsPoint ;
GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
app_data_dir() is the root directly on this Tauri; the old .parent().parent()
read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
(gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
recalc with noDelete (never runs the destructive glitch/>24h filter that
once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
would roll unpowered) = basal only. Elevation via cumulative hysteresis.
recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
without touching points.
- live recording: the native file is the DB source of truth; watchPosition
only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
window . DB . addGpsPointsBulk = addGpsPointsBulk ;
2026-05-13 10:21:24 +03:00
window . DB . finishGpsSession = finishGpsSession ;
2026-09-07 17:10:16 +03:00
window . DB . setSessionWindMarks = setSessionWindMarks ;
2026-07-31 22:11:55 +03:00
window . DB . saveSmoothedTrack = saveSmoothedTrack ;
window . DB . setSessionSmoothed = setSessionSmoothed ;
2026-05-13 10:21:24 +03:00
window . DB . getGpsSessions = getGpsSessions ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
window . DB . getGpsSessionsForDate = getGpsSessionsForDate ;
2026-05-13 10:21:24 +03:00
window . DB . getGpsSession = getGpsSession ;
window . DB . getGpsPoints = getGpsPoints ;
window . DB . deleteGpsSession = deleteGpsSession ;
2026-09-18 00:08:25 +03:00
window . DB . mergeGpsSessions = mergeGpsSessions ;
2026-10-20 23:49:17 +03:00
window . DB . unmergeGpsSession = unmergeGpsSession ;
window . DB . countMergedOrigins = countMergedOrigins ;
2026-09-21 12:36:03 +03:00
window . DB . splitGpsSession = splitGpsSession ;
2026-05-13 10:21:24 +03:00
window . DB . importGpxSession = importGpxSession ;
2026-09-10 15:26:05 +03:00
window . DB . saveGapFill = saveGapFill ;
window . DB . revertGapFill = revertGapFill ;
2026-09-14 12:14:04 +03:00
window . DB . setWalkFlags = setWalkFlags ;
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
async function findNearestAltitude ( lat , lon , radiusM ) {
const dLat = radiusM / 111000 ;
const dLon = radiusM / ( 111000 * Math . cos ( lat * Math . PI / 180 ) ) ;
const rows = await select ( `
SELECT altitude _gps _m FROM gps _points
WHERE altitude _gps _m IS NOT NULL
AND lat BETWEEN ? 1 AND ? 2 AND lon BETWEEN ? 3 AND ? 4
ORDER BY ( lat - ? 5 ) * ( lat - ? 5 ) + ( lon - ? 6 ) * ( lon - ? 6 )
LIMIT 1 ` , [lat - dLat, lat + dLat, lon - dLon, lon + dLon, lat, lon]);
return rows [ 0 ] ? . altitude _gps _m ? ? null ;
}
window . DB . findNearestAltitude = findNearestAltitude ;
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
window . DB . getBikes = getBikes ;
window . DB . getBike = getBike ;
window . DB . getActiveBike = getActiveBike ;
window . DB . saveBike = saveBike ;
window . DB . updateBike = updateBike ;
window . DB . deleteBike = deleteBike ;
window . DB . updateSessionBike = updateSessionBike ;
2026-09-22 11:12:24 +03:00
window . DB . setSessionRiderWeight = setSessionRiderWeight ;
Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.
- gps.js: GPS.hrKcalNet; startRecording/stopRecording 'workout' branches;
the HR poll appends [relSec,bpm] samples.
- db.js: gps_sessions.hr_samples column + setSessionHrSamples.
- main.js: 'Зарядка (пульс)' option; renderWorkoutInline (map-less live
screen: big HR, timer, kcal, connect-sensor button); renderWorkoutDetail
(HR chart by zones, load, avg/max).
- tools/hr_log.py: background HR logger (standard 0x180D, no pairing).
Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
window . DB . setSessionHrSamples = setSessionHrSamples ;
2026-07-07 00:28:27 +03:00
window . DB . updatePointsSrtm = updatePointsSrtm ;
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
window . DB . getBikeMaintenanceLog = getBikeMaintenanceLog ;
window . DB . addBikeMaintenance = addBikeMaintenance ;
window . DB . deleteBikeMaintenance = deleteBikeMaintenance ;
window . DB . getBikeTotalMileage = getBikeTotalMileage ;
window . DB . getBikeMaintenanceDue = getBikeMaintenanceDue ;
2026-05-27 17:45:50 +03:00
window . DB . getGoalMarkers = getGoalMarkers ;
window . DB . addGoalMarker = addGoalMarker ;
window . DB . deleteGoalMarker = deleteGoalMarker ;
2026-09-12 22:55:51 +03:00
window . DB . getNogoAreas = getNogoAreas ;
window . DB . addNogoArea = addNogoArea ;
window . DB . deleteNogoArea = deleteNogoArea ;
2026-07-31 22:11:55 +03:00
window . DB . getRoutes = getRoutes ;
window . DB . getRoute = getRoute ;
window . DB . addRoute = addRoute ;
window . DB . renameRoute = renameRoute ;
window . DB . setRouteColor = setRouteColor ;
window . DB . deleteRoute = deleteRoute ;
Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.
- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
tappable altitude/grade points, elevation profile chart (steep >8% in red),
length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
drawn in a dedicated pane below the overlay pane, so a recorded track stays on
top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
getRoutes reports hasEle. Route points are now {lat, lon, ele?}.
Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
window . DB . updateRoutePoints = updateRoutePoints ;
2026-05-27 17:45:50 +03:00
window . DB . getFrequentFoods = getFrequentFoods ;
window . DB . getGoalProgress = getGoalProgress ;
2026-05-13 10:21:24 +03:00
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
// ---------------------------------------------------------------------------
// Nutrient reserves (accumulated body stores)
// ---------------------------------------------------------------------------
async function getReserves ( date ) {
const rows = await select ( "SELECT reserves FROM nutrient_reserves WHERE date = ?1" , [ date ] ) ;
if ( ! rows . length || ! rows [ 0 ] . reserves ) return { } ;
try { return JSON . parse ( rows [ 0 ] . reserves ) ; } catch { return { } ; }
}
async function saveReserves ( date , reserves ) {
await execute (
` INSERT INTO nutrient_reserves (date, reserves) VALUES (?1, ?2)
ON CONFLICT ( date ) DO UPDATE SET reserves = ? 2 ` ,
[ date , JSON . stringify ( reserves ) ]
) ;
}
async function getAllReservesDates ( ) {
return select ( "SELECT date FROM nutrient_reserves ORDER BY date" ) ;
}
async function getDaysWithFood ( ) {
return select (
` SELECT DISTINCT date FROM (
SELECT date FROM food _log
UNION SELECT date FROM supplement _log
) ORDER BY date `
) ;
}
async function getDailyWaterMl ( date ) {
// From products with water_pct
const prodRows = await select (
` SELECT COALESCE(SUM(fl.amount_g * p.water_pct / 100.0), 0) as ml
FROM food _log fl JOIN products p ON fl . product _id = p . id
WHERE fl . date = ? 1 AND p . water _pct IS NOT NULL AND p . water _pct > 0 ` ,
[ date ]
) ;
let total = prodRows [ 0 ] ? . ml || 0 ;
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
// From recipes — calculate water from ingredients on-the-fly (cache may be stale)
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
const recipeRows = await select (
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
` SELECT fl.amount_g, fl.recipe_id, r.servings,
( SELECT SUM ( ri . amount _g ) FROM recipe _ingredients ri WHERE ri . recipe _id = r . id ) as recipe _weight
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
FROM food _log fl JOIN recipes r ON fl . recipe _id = r . id
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
WHERE fl . date = ? 1 ` ,
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
[ date ]
) ;
for ( const rr of recipeRows ) {
try {
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
// Sum water from recipe ingredients
const ings = await select (
` SELECT ri.amount_g, COALESCE(p.water_pct, 0) as water_pct
FROM recipe _ingredients ri LEFT JOIN products p ON ri . product _id = p . id
WHERE ri . recipe _id = ? 1 ` ,
[ rr . recipe _id ]
) ;
let recipeWaterMl = 0 ;
for ( const ing of ings ) {
recipeWaterMl += ( ing . amount _g || 0 ) * ( ing . water _pct || 0 ) / 100 ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
}
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
// Scale by portion: how much of the recipe was eaten
const servingG = Math . round ( ( rr . recipe _weight || 0 ) / ( rr . servings || 1 ) ) ;
const scale = servingG > 0 ? rr . amount _g / servingG : 1 ;
total += recipeWaterMl / ( rr . servings || 1 ) * scale ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
} catch { }
}
return Math . round ( total ) ;
}
window . DB . getReserves = getReserves ;
window . DB . saveReserves = saveReserves ;
window . DB . getAllReservesDates = getAllReservesDates ;
window . DB . getDaysWithFood = getDaysWithFood ;
window . DB . getDailyWaterMl = getDailyWaterMl ;
Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
// All GPS tracks as arrays of [lat, lon] grouped by session_id.
// minSpeedMs filters out stopped/slow points. minPoints filters out tiny sessions.
// Fetches per-session to avoid Tauri SQL plugin row limit.
// Road points — all GPS points merged into one flat table.
// Rebuild: INSERT...SELECT runs entirely in SQLite (no JS iteration, no ID issues).
// Reading: paginated to avoid Tauri SQL plugin row limit (~28K).
async function rebuildRoads ( ) {
const invoke = window . _ _TAURI _ _ . core . invoke ;
const raw = await invoke ( "rebuild_roads" , { minSpeedMs : 0.5 } ) ;
const resp = JSON . parse ( raw ) ;
console . log ( "[rebuildRoads]" , resp ) ;
return resp . road _points ;
}
// Read road points via Rust command (Tauri SQL plugin has ~28K row limit)
async function getRoadPoints ( ) {
const invoke = window . _ _TAURI _ _ . core . invoke ;
const BATCH = 10000 ;
let offset = 0 ;
const all = [ ] ;
while ( true ) {
const raw = await invoke ( "get_road_points" , { offset , batchSize : BATCH } ) ;
const resp = JSON . parse ( raw ) ;
2026-10-22 00:49:18 +03:00
for ( const p of resp . points ) all . push ( { lat : p [ 0 ] , lon : p [ 1 ] , atype : p [ 2 ] || "b" , passes : p [ 3 ] || 1 } ) ;
Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
console . log ( ` [getRoadPoints] batch offset= ${ offset } got= ${ resp . points . length } total_db= ${ resp . total } ` ) ;
if ( ! resp . has _more ) break ;
offset += BATCH ;
}
console . log ( ` [getRoadPoints] total= ${ all . length } ` ) ;
return all ;
}
window . DB . rebuildRoads = rebuildRoads ;
window . DB . getRoadPoints = getRoadPoints ;
2026-05-14 14:17:57 +03:00
// JS-side migrations — safe for any DB state (checks before altering)
async function runMigrations ( ) {
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
// Disable WAL — all writes go directly to main DB file (no -wal/-shm files)
await execute ( "PRAGMA journal_mode=DELETE" ) ;
Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
// Sync columns: sync_id (UUID), updated_at (ISO timestamp), sync_dirty (0/1)
const syncTables = [ "food_log" , "weight_log" , "supplement_log" , "sleep_log" , "activity_log" ,
"gps_sessions" , "recipes" , "recipe_ingredients" , "supplements" ] ;
for ( const tbl of syncTables ) {
const cols = ( await select ( ` PRAGMA table_info( ${ tbl } ) ` ) ) . map ( r => r . name ) ;
if ( ! cols . includes ( "sync_id" ) ) {
await execute ( ` ALTER TABLE ${ tbl } ADD COLUMN sync_id TEXT ` ) ;
await execute ( ` ALTER TABLE ${ tbl } ADD COLUMN updated_at TEXT ` ) ;
await execute ( ` ALTER TABLE ${ tbl } ADD COLUMN sync_dirty INTEGER DEFAULT 0 ` ) ;
// Backfill sync_id for existing rows
await execute ( ` UPDATE ${ tbl } SET sync_id = lower(hex(randomblob(4)) || '-' || hex(randomblob(2)) || '-' || hex(randomblob(2)) || '-' || hex(randomblob(2)) || '-' || hex(randomblob(6))) WHERE sync_id IS NULL ` ) ;
await execute ( ` UPDATE ${ tbl } SET updated_at = datetime('now') WHERE updated_at IS NULL ` ) ;
}
// Triggers: auto-set sync_id, updated_at, sync_dirty on INSERT/UPDATE
await execute ( ` CREATE TRIGGER IF NOT EXISTS ${ tbl } _sync_insert
AFTER INSERT ON $ { tbl } WHEN NEW . sync _id IS NULL
BEGIN UPDATE $ { tbl } SET
sync _id = lower ( hex ( randomblob ( 4 ) ) || '-' || hex ( randomblob ( 2 ) ) || '-' || hex ( randomblob ( 2 ) ) || '-' || hex ( randomblob ( 2 ) ) || '-' || hex ( randomblob ( 6 ) ) ) ,
updated _at = datetime ( 'now' ) , sync _dirty = 1
WHERE rowid = NEW . rowid ; END ` );
await execute ( ` CREATE TRIGGER IF NOT EXISTS ${ tbl } _sync_update
AFTER UPDATE ON $ { tbl } WHEN NEW . sync _dirty != 0 OR OLD . sync _dirty = 0
BEGIN UPDATE $ { tbl } SET
updated _at = datetime ( 'now' ) , sync _dirty = 1
WHERE rowid = NEW . rowid AND sync _dirty = 0 ; END ` );
// First sync: mark all existing rows as dirty so they get pushed
const dirtyCount = ( await select ( ` SELECT COUNT(*) as c FROM ${ tbl } WHERE sync_dirty = 1 ` ) ) [ 0 ] . c ;
if ( dirtyCount === 0 ) {
const total = ( await select ( ` SELECT COUNT(*) as c FROM ${ tbl } ` ) ) [ 0 ] . c ;
if ( total > 0 ) {
await execute ( ` UPDATE ${ tbl } SET sync_dirty = 1 ` ) ;
console . log ( ` [Sync] Marked ${ total } rows in ${ tbl } as dirty for first sync ` ) ;
}
}
}
2026-05-25 02:42:52 +03:00
// Batch column checks — one PRAGMA per table instead of N separate IPC calls
const productCols = ( await select ( "PRAGMA table_info(products)" ) ) . map ( r => r . name ) ;
const addProdCol = async ( col , type ) => { if ( ! productCols . includes ( col ) ) await execute ( ` ALTER TABLE products ADD COLUMN ${ col } ${ type } ` ) ; } ;
await addProdCol ( "composition" , "TEXT" ) ;
await addProdCol ( "name_lower" , "TEXT" ) ;
await addProdCol ( "nutrients_guessed" , "INTEGER DEFAULT 0" ) ;
2026-05-29 22:12:37 +03:00
await addProdCol ( "user_edited" , "INTEGER DEFAULT 0" ) ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
await addProdCol ( "ginseng_mg" , "REAL" ) ;
await addProdCol ( "resveratrol_mg" , "REAL" ) ;
await addProdCol ( "coq10_mg" , "REAL" ) ;
// New supplement columns (ginseng, resveratrol, coq10) — added after existing suppCols migration below
Seed DB: +5ka (3103) +ION (1078) with micronutrients, GPS fixes, UI improvements
Products:
- 5ka scraper: recursive category BFS, 3103 products imported
- ION scraper (web.ion.ru): fetch-based, 1111 products with full vitamins/minerals
- Seed DB now 13480 products (magnit 8627, 5ka 3103, ion 1078, kdv 672)
- 13 micronutrient columns added: Na, K, Ca, Mg, P, Fe, vit A/B1/B2/B3/C/E, carotene
GPS fixes:
- computeStats sorts points by seq/ts before calculating (fixes wrong elevation/kcal)
- Trail restore on recording screen sorts points
- Old tracks with NULL seq repaired via migration (db.js)
- Activity type saved to localStorage, restored on GPS screen
UI:
- Track points invisible but tappable (popup with time/alt/speed)
- Charts: pointer-events:none on canvas when minimized (page scrolls)
- Navigation history stack (20 entries), back gesture doesn't close app
- Session list sort: added id DESC tiebreaker
2026-05-19 23:53:49 +03:00
2026-05-25 02:42:52 +03:00
const gpsCols = ( await select ( "PRAGMA table_info(gps_points)" ) ) . map ( r => r . name ) ;
if ( ! gpsCols . includes ( "seq" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN seq INTEGER" ) ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
if ( ! gpsCols . includes ( "spo2" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN spo2 INTEGER" ) ;
if ( ! gpsCols . includes ( "bp_sys" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN bp_sys INTEGER" ) ;
if ( ! gpsCols . includes ( "bp_dia" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN bp_dia INTEGER" ) ;
Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
if ( ! gpsCols . includes ( "accel_rms" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN accel_rms REAL" ) ;
if ( ! gpsCols . includes ( "accel_max" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN accel_max REAL" ) ;
if ( ! gpsCols . includes ( "gyro_pitch" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN gyro_pitch REAL" ) ;
if ( ! gpsCols . includes ( "gyro_yaw" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN gyro_yaw REAL" ) ;
2026-07-07 00:28:27 +03:00
if ( ! gpsCols . includes ( "bearing_deg" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN bearing_deg REAL" ) ;
2026-07-31 22:11:55 +03:00
if ( ! gpsCols . includes ( "accuracy_m" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN accuracy_m REAL" ) ;
if ( ! gpsCols . includes ( "lat_smoothed" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN lat_smoothed REAL" ) ;
if ( ! gpsCols . includes ( "lon_smoothed" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN lon_smoothed REAL" ) ;
2026-09-10 15:26:05 +03:00
if ( ! gpsCols . includes ( "estimated" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN estimated INTEGER DEFAULT 0" ) ; // gap-fill (BRouter) points, reversible
2026-09-14 12:14:04 +03:00
if ( ! gpsCols . includes ( "walk" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN walk INTEGER DEFAULT 0" ) ; // point is in a "walking" run inside a ride (excluded from ridden roads)
2026-09-18 00:08:25 +03:00
if ( ! gpsCols . includes ( "orig_session_id" ) ) await execute ( "ALTER TABLE gps_points ADD COLUMN orig_session_id TEXT" ) ; // set on merge = the session this point first belonged to (lets a future split undo the merge)
2026-05-21 00:06:50 +03:00
// --- Cooking methods table ---
await execute ( ` CREATE TABLE IF NOT EXISTS cooking_methods (
id INTEGER PRIMARY KEY ,
name TEXT NOT NULL ,
protein _loss _pct REAL DEFAULT 0 ,
fat _loss _pct REAL DEFAULT 0 ,
carbs _loss _pct REAL DEFAULT 0 ,
fiber _loss _pct REAL DEFAULT 0 ,
vit _a _loss _pct REAL DEFAULT 0 ,
vit _c _loss _pct REAL DEFAULT 0 ,
vit _b1 _loss _pct REAL DEFAULT 0 ,
vit _b2 _loss _pct REAL DEFAULT 0 ,
kcal _loss _pct REAL DEFAULT 0
) ` );
// Seed cooking methods if empty
const cmCount = await select ( "SELECT COUNT(*) as cnt FROM cooking_methods" ) ;
if ( cmCount [ 0 ] . cnt === 0 ) {
const methods = [
[ 1 , "Без обработки" , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ,
[ 2 , "Варка" , 2 , 5 , 3 , 0 , 0 , 50 , 30 , 10 , 0 ] ,
[ 3 , "Жарка" , 5 , - 10 , 3 , 0 , 0 , 60 , 35 , 0 , 0 ] ,
[ 4 , "Тушение" , 2 , 10 , 2 , 0 , 0 , 40 , 25 , 0 , 0 ] ,
[ 5 , "Запекание" , 3 , 8 , 2 , 0 , 0 , 45 , 20 , 0 , 0 ] ,
[ 6 , "Н а пару" , 1 , 2 , 1 , 0 , 0 , 25 , 15 , 0 , 0 ] ,
] ;
for ( const m of methods ) {
await execute (
` INSERT INTO cooking_methods (id, name, protein_loss_pct, fat_loss_pct,
carbs _loss _pct , fiber _loss _pct , vit _a _loss _pct , vit _c _loss _pct ,
vit _b1 _loss _pct , vit _b2 _loss _pct , kcal _loss _pct )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 , ? 11 ) ` ,
m
) ;
}
console . log ( "[db.js] Seeded 6 cooking methods" ) ;
}
2026-05-25 02:42:52 +03:00
// --- Recipes + ingredients: batch column checks ---
const recipeCols = ( await select ( "PRAGMA table_info(recipes)" ) ) . map ( r => r . name ) ;
if ( ! recipeCols . includes ( "cooking_method_id" ) ) await execute ( "ALTER TABLE recipes ADD COLUMN cooking_method_id INTEGER" ) ;
if ( ! recipeCols . includes ( "created_at" ) ) await execute ( "ALTER TABLE recipes ADD COLUMN created_at TEXT DEFAULT (datetime('now'))" ) ;
2026-05-21 00:06:50 +03:00
2026-05-25 02:42:52 +03:00
const riCols = ( await select ( "PRAGMA table_info(recipe_ingredients)" ) ) . map ( r => r . name ) ;
if ( ! riCols . includes ( "custom_name" ) ) await execute ( "ALTER TABLE recipe_ingredients ADD COLUMN custom_name TEXT" ) ;
if ( ! riCols . includes ( "cooking_method_id" ) ) await execute ( "ALTER TABLE recipe_ingredients ADD COLUMN cooking_method_id INTEGER" ) ;
2026-05-21 00:06:50 +03:00
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
// --- Serving units table ---
await execute ( ` CREATE TABLE IF NOT EXISTS serving_units (
id INTEGER PRIMARY KEY ,
name TEXT NOT NULL ,
name _short TEXT ,
grams _equivalent REAL ,
category TEXT DEFAULT 'universal'
) ` );
// --- Product units junction table ---
await execute ( ` CREATE TABLE IF NOT EXISTS product_units (
id INTEGER PRIMARY KEY ,
product _id INTEGER NOT NULL REFERENCES products ( id ) ,
unit _id INTEGER NOT NULL REFERENCES serving _units ( id ) ,
grams _per _unit REAL NOT NULL ,
UNIQUE ( product _id , unit _id )
) ` );
// --- Add unit columns to food_log ---
2026-05-25 02:42:52 +03:00
const flCols = ( await select ( "PRAGMA table_info(food_log)" ) ) . map ( r => r . name ) ;
if ( ! flCols . includes ( "unit_id" ) ) await execute ( "ALTER TABLE food_log ADD COLUMN unit_id INTEGER" ) ;
if ( ! flCols . includes ( "unit_amount" ) ) await execute ( "ALTER TABLE food_log ADD COLUMN unit_amount REAL" ) ;
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
// Seed serving_units if empty
const suCnt = await select ( "SELECT COUNT(*) as cnt FROM serving_units" ) ;
if ( suCnt [ 0 ] . cnt === 0 ) {
const units = [
[ 1 , "шт" , "шт" , null , "product_specific" ] ,
[ 2 , "ст.л." , "ст.л." , 15 , "universal" ] ,
[ 3 , "ч.л." , "ч.л." , 5 , "universal" ] ,
[ 4 , "стакан" , "стак." , 250 , "universal" ] ,
[ 5 , "мл" , "мл" , 1 , "universal" ] ,
] ;
for ( const u of units ) {
await execute (
` INSERT INTO serving_units (id, name, name_short, grams_equivalent, category)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 ) ` , u
) ;
}
console . log ( "[db.js] Seeded 5 serving units" ) ;
}
2026-05-25 03:00:47 +03:00
// product_units: user adds per-product unit mappings manually via UI.
// No auto-seeding — product names too varied for reliable matching.
Alternative units, analytics charts, elevation smoothing, batch GPS update
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
2026-05-23 17:12:19 +03:00
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
// --- Bikes table ---
await execute ( ` CREATE TABLE IF NOT EXISTS bikes (
id INTEGER PRIMARY KEY ,
name TEXT NOT NULL ,
weight _kg REAL ,
wheel _diameter _inch REAL ,
chainring _teeth INTEGER ,
cog _teeth INTEGER ,
gear _count INTEGER ,
surface _type TEXT DEFAULT 'mixed' ,
notes TEXT ,
is _active INTEGER DEFAULT 0
) ` );
// Add bike_id column to gps_sessions
2026-05-25 02:42:52 +03:00
const gsCols = ( await select ( "PRAGMA table_info(gps_sessions)" ) ) . map ( r => r . name ) ;
if ( ! gsCols . includes ( "bike_id" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN bike_id INTEGER" ) ;
2026-07-31 22:11:55 +03:00
// Track de-jitter: reversible smoothing (points keep both original lat/lon and
// lat_smoothed/lon_smoothed; `smoothed`=1 means the smoothed track is active).
if ( ! gsCols . includes ( "smoothed" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN smoothed INTEGER DEFAULT 0" ) ;
if ( ! gsCols . includes ( "smooth_window" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN smooth_window INTEGER" ) ;
if ( ! gsCols . includes ( "smooth_iters" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN smooth_iters INTEGER" ) ;
2026-09-07 13:53:40 +03:00
if ( ! gsCols . includes ( "pauses" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN pauses TEXT" ) ; // JSON [{startTs,endTs,...}] manual pauses
2026-09-07 17:10:16 +03:00
if ( ! gsCols . includes ( "wind_marks" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN wind_marks TEXT" ) ; // JSON [{startTs,endTs}] headwind labels (ground truth for wind estimate)
2026-09-10 15:26:05 +03:00
if ( ! gsCols . includes ( "gap_filled" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN gap_filled INTEGER DEFAULT 0" ) ; // BRouter gap-fill applied (reversible)
2026-09-22 11:12:24 +03:00
if ( ! gsCols . includes ( "rider_weight_kg" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN rider_weight_kg REAL" ) ; // rider weight at ride time; recompute uses it (NULL on old sessions -> latest weigh-in)
Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.
- gps.js: GPS.hrKcalNet; startRecording/stopRecording 'workout' branches;
the HR poll appends [relSec,bpm] samples.
- db.js: gps_sessions.hr_samples column + setSessionHrSamples.
- main.js: 'Зарядка (пульс)' option; renderWorkoutInline (map-less live
screen: big HR, timer, kcal, connect-sensor button); renderWorkoutDetail
(HR chart by zones, load, avg/max).
- tools/hr_log.py: background HR logger (standard 0x180D, no pairing).
Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
if ( ! gsCols . includes ( "hr_samples" ) ) await execute ( "ALTER TABLE gps_sessions ADD COLUMN hr_samples TEXT" ) ; // HR-only "workout" sessions (no GPS): JSON [[relSec,bpm],...]
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
2026-05-27 16:45:33 +03:00
// Add tire_pressure_bar to bikes (for Crr correction by tire pressure)
const bikeCols = ( await select ( "PRAGMA table_info(bikes)" ) ) . map ( r => r . name ) ;
if ( ! bikeCols . includes ( "tire_pressure_bar" ) ) await execute ( "ALTER TABLE bikes ADD COLUMN tire_pressure_bar REAL" ) ;
// --- Bike maintenance log table ---
await execute ( ` CREATE TABLE IF NOT EXISTS bike_maintenance (
id INTEGER PRIMARY KEY ,
bike _id INTEGER NOT NULL REFERENCES bikes ( id ) ON DELETE CASCADE ,
date TEXT NOT NULL ,
description TEXT NOT NULL ,
mileage _km REAL ,
next _service _km REAL ,
notes TEXT
) ` );
Bike profiles, physics-based kcal, session recalculation
Bike profiles:
- bikes table: name, weight, wheel, chainring/cog teeth, surface type
- CRUD in settings screen, active bike indicator
- Default bike: 12kg, 26", 42/18, mixed surface
- bike_id linked to gps_sessions
Physics-based kcal (replaces MET*0.97):
- Per-segment power calculation: gravity + rolling (Crr) + aero (CdA)
- Power > 0 = pedaling: mechanical energy / 25% efficiency = metabolic cost
- Power <= 0 = coasting: basal metabolic rate only
- Walk/run: proper MET table from Compendium of Physical Activities
- Raw altitude for gradient (smoothed was killing real slopes, 460 vs 296 kcal)
- Smoothed altitude kept for displayed elevGain/Loss only
Session recalculation:
- "Recalculate" button on session detail page
- Uses current weight + bike settings, re-reads all points
- Bike selector per session (change bike -> auto recalculate)
Other fixes:
- User weight fallback chain: weight_log -> profile -> 80kg
- Theoretical cadence from gear ratio + wheel circumference
- Crash recovery preserves bike data
2026-05-25 02:09:50 +03:00
// Seed default bike if table is empty
const bikeCnt = await select ( "SELECT COUNT(*) as cnt FROM bikes" ) ;
if ( bikeCnt [ 0 ] . cnt === 0 ) {
await execute (
` INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth,
cog _teeth , surface _type , is _active )
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 ) ` ,
[ "Мой велосипед" , 12 , 26 , 42 , 18 , "mixed" , 1 ]
) ;
console . log ( "[db.js] Seeded default bike" ) ;
}
2026-05-27 17:45:50 +03:00
// --- Goal markers table ---
await execute ( ` CREATE TABLE IF NOT EXISTS goal_markers (
id INTEGER PRIMARY KEY ,
name TEXT NOT NULL ,
lat REAL NOT NULL ,
lon REAL NOT NULL ,
notes TEXT ,
created _at TEXT DEFAULT ( datetime ( 'now' ) )
) ` );
2026-09-12 22:55:51 +03:00
// --- Nogo areas (impassable spots the router avoids) ---
await execute ( ` CREATE TABLE IF NOT EXISTS nogo_areas (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
lat REAL NOT NULL ,
lon REAL NOT NULL ,
radius _m REAL DEFAULT 25 ,
note TEXT ,
created _at TEXT DEFAULT ( datetime ( 'now' ) )
) ` );
2026-07-31 22:11:55 +03:00
// --- Saved routes (hand-drawn reference paths overlaid on the map) ---
await execute ( ` CREATE TABLE IF NOT EXISTS routes (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
name TEXT NOT NULL ,
color TEXT DEFAULT '#e53935' ,
points _json TEXT NOT NULL ,
created _at TEXT DEFAULT ( datetime ( 'now' ) )
) ` );
Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
// Planner-built routes also store their editable waypoints + per-leg mode, so editing
// shows 5-6 points instead of the dense routed geometry. Imported routes leave it null.
let routesCols = ( await select ( "PRAGMA table_info(routes)" ) ) . map ( r => r . name ) ;
if ( ! routesCols . includes ( "waypoints_json" ) ) await execute ( "ALTER TABLE routes ADD COLUMN waypoints_json TEXT" ) ;
2026-07-31 22:11:55 +03:00
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
// --- Supplement columns migration: add missing micronutrient columns ---
const suppCols = ( await select ( "PRAGMA table_info(supplements)" ) ) . map ( r => r . name ) ;
const addSuppCol = async ( col , type ) => { if ( ! suppCols . includes ( col ) ) await execute ( ` ALTER TABLE supplements ADD COLUMN ${ col } ${ type } ` ) ; } ;
await addSuppCol ( "vit_b5_mg" , "REAL" ) ;
await addSuppCol ( "vit_b7_mcg" , "REAL" ) ; // biotin
await addSuppCol ( "chromium_mg" , "REAL" ) ;
await addSuppCol ( "manganese_mg" , "REAL" ) ;
await addSuppCol ( "molybdenum_mcg" , "REAL" ) ;
await addSuppCol ( "iodine_mg" , "REAL" ) ;
await addSuppCol ( "phosphorus_mg" , "REAL" ) ;
await addSuppCol ( "silicon_mg" , "REAL" ) ; // кремний — no RDI
await addSuppCol ( "vanadium_mg" , "REAL" ) ; // ванадий — no RDI
await addSuppCol ( "lutein_mg" , "REAL" ) ; // лютеин — no RDI
await addSuppCol ( "lycopene_mg" , "REAL" ) ; // ликопин — no RDI
await addSuppCol ( "carotene_mcg" , "REAL" ) ; // бета-каротин
Fix carotene missing from food query, add fish oil field, full nutrient SELECT
- getFoodForDate: added p.phosphorus_mg, p.carotene_mcg (were missing)
- searchProducts: added carotene, selenium, zinc, copper, folate, D, K, B6, B12
- Supplement form: "Рыбий жир (мг)" field for total capsule weight
- Omega fields relabeled: "в т.ч." prefix to show they're part of fish oil
Carrot (12000 mcg carotene) was showing 0% because carotene_mcg
was not in the getFoodForDate SELECT.
2026-05-29 18:00:02 +03:00
await addSuppCol ( "fish_oil_mg" , "REAL" ) ; // рыбий жир (общий объём)
2026-05-29 15:21:59 +03:00
await addSuppCol ( "omega3_mg" , "REAL" ) ; // омега-3 общая
await addSuppCol ( "epa_mg" , "REAL" ) ; // EPA (эйкозапентаеновая)
await addSuppCol ( "dha_mg" , "REAL" ) ; // DHA (докозагексаеновая)
Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
2026-05-29 14:47:09 +03:00
await addSuppCol ( "omega6_mg" , "REAL" ) ; // омега-6
await addSuppCol ( "omega9_mg" , "REAL" ) ; // омега-9
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
await addSuppCol ( "ginseng_mg" , "REAL" ) ; // женьшень
await addSuppCol ( "resveratrol_mg" , "REAL" ) ; // ресвератрол
await addSuppCol ( "coq10_mg" , "REAL" ) ; // коэнзим Q10
RDI table, nutrient % in food/recipes, daily deficiency strip
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
2026-05-29 13:38:14 +03:00
2026-06-01 14:51:38 +03:00
// Product dedup handled in Rust import_seed_products (UNIQUE INDEX on name_lower+source+store_id)
Dedup products, fix labels, fix track comparison, seed v9
1. Deduplicate products: 13482 -> 8325 in seed DB. JS migration
deduplicates app DB on launch: reassigns food_log and
recipe_ingredients to kept product before deleting dupes.
2. MICRO_FIELDS: full chemical names + alternatives for all nutrients.
"B3 / PP / ниацин", "B9 / фолиевая", "Кальций / Ca" etc.
3. Track comparison: newer track always compared against older.
Green = newer faster (progress), Red = newer slower (regression).
Removed separate cyan dashed track — only colored segments.
Order of selection doesn't matter — date determines which is newer.
4. Source badges in search: source column was already in SELECT,
badges render via sourceBadge(p.source) in prod card.
5. Gap threshold already cosmetic-only (confirmed, no changes needed).
Seed DB v9, VACUUM'd, indexed.
2026-05-30 18:09:16 +03:00
2026-06-01 16:45:30 +03:00
// 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)" ) ;
}
АУП 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.
2026-06-02 18:39:09 +03:00
// 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)" ) ;
}
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
// Nutrient reserves table — accumulated body stores per day
await execute ( ` CREATE TABLE IF NOT EXISTS nutrient_reserves (
date TEXT PRIMARY KEY ,
reserves TEXT
) ` );
Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
// Road points — deduplicated points from all GPS tracks
await execute ( ` CREATE TABLE IF NOT EXISTS road_points (
id INTEGER PRIMARY KEY ,
lat REAL NOT NULL ,
lon REAL NOT NULL
) ` );
// Spatial index for fast 2m proximity checks
await execute ( ` CREATE INDEX IF NOT EXISTS idx_road_lat_lon ON road_points(lat, lon) ` ) ;
Activity types (squats/jumprope/stretch/swim), road map, tile import, fixes
Activity types:
- New types: squats, jump rope, stretching, swimming pool
- MET-based kcal auto-calculation (6.0/12.3/2.3/8.0)
- Dynamic modal fields per type (sets/reps, distance, minutes)
- Fix: state.latestWeight is object {weight_kg}, not number
Road map:
- road_points table with 2m dedup, activity type (bike/walk)
- Rust rebuild_roads + get_road_points (bypass SQL plugin 28K limit)
- Segment detection by 100m gap, bike=blue walk=brown
Tile system:
- CSP fix: a.tile.openstreetmap.org matches *.tile pattern
- pack_tiles.js tool, import from file/router, ureq body limit bypass
- Tile errors: done(null,tile) prevents crash cascade
Other fixes:
- Manual routes: skip gap segmentation (synthetic timestamps)
- Draw route: own CSS class, not affected by landscape rotation
- Orientation lock button (A/L), hysteresis 65/35 degrees
- GPS placeholder only when state=waiting_gps
- Food amount inputmode=text for formula support (4*30)
- showModal onMount callback
2026-06-26 21:04:21 +03:00
// Activity types: extend activity_log with structured fields
const actCols = ( await select ( "PRAGMA table_info(activity_log)" ) ) . map ( c => c . name ) ;
if ( ! actCols . includes ( "activity_type" ) ) {
await execute ( "ALTER TABLE activity_log ADD COLUMN activity_type TEXT" ) ;
await execute ( "UPDATE activity_log SET activity_type = 'manual' WHERE activity_type IS NULL" ) ;
}
if ( ! actCols . includes ( "sets" ) ) await execute ( "ALTER TABLE activity_log ADD COLUMN sets INTEGER" ) ;
if ( ! actCols . includes ( "reps" ) ) await execute ( "ALTER TABLE activity_log ADD COLUMN reps INTEGER" ) ;
if ( ! actCols . includes ( "distance_m" ) ) await execute ( "ALTER TABLE activity_log ADD COLUMN distance_m REAL" ) ;
OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
reserves strip, GPS sessions in today view, pan on charts, showToast(),
HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
// Water content column for hydration tracking
if ( ! productCols . includes ( "water_pct" ) ) {
await execute ( "ALTER TABLE products ADD COLUMN water_pct REAL" ) ;
// Seed water_pct for known liquid products by name pattern
const waterPatterns = [
[ 100 , [ "% вода %" , "% вода" , "вода %" , "вода питьевая%" , "вода минеральная%" , "вода газированная%" ] ] ,
[ 99 , [ "%чай %" , "%чай" , "чай %" ] ] ,
[ 99 , [ "%кофе %" , "%кофе" , "кофе %" ] ] ,
[ 87 , [ "%молоко %" , "%молоко" , "молоко %" , "%кефир%" , "%ряженка%" , "%простокваша%" , "%ацидофилин%" ] ] ,
[ 88 , [ "%сок %" , "%сок" , "сок %" , "%нектар%" ] ] ,
[ 85 , [ "%компот%" , "%морс%" , "%кисель%" , "%какао%напит%" ] ] ,
[ 90 , [ "%суп %" , "%суп" , "суп %" , "%бульон%" , "%борщ%" , "%щи %" , "%солянка%" , "%рассольник%" , "%у х а %" , "%окрошка%" ] ] ,
[ 89 , [ "%газировка%" , "%кола %" , "%кола" , "%фанта%" , "%спрайт%" , "%лимонад%" , "%тархун%" , "%байкал%" , "%напиток газ%" ] ] ,
[ 92 , [ "%пиво %" , "%пиво" , "пиво %" , "%пивн%" ] ] ,
[ 93 , [ "%квас%" ] ] ,
[ 80 , [ "%смузи%" ] ] ,
] ;
for ( const [ pct , patterns ] of waterPatterns ) {
for ( const p of patterns ) {
await execute ( ` UPDATE products SET water_pct = ?1 WHERE water_pct IS NULL AND name_lower LIKE ?2 ` , [ pct , p ] ) ;
}
}
const waterCount = ( await select ( "SELECT COUNT(*) as cnt FROM products WHERE water_pct IS NOT NULL" ) ) [ 0 ] . cnt ;
console . log ( ` [db.js] Seeded water_pct for ${ waterCount } products ` ) ;
}
АУП 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.
2026-06-02 18:39:09 +03:00
// 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" ) ;
}
2026-05-21 00:06:50 +03:00
console . log ( "[db.js] recipe migrations done, checking seq repair..." ) ;
Seed DB: +5ka (3103) +ION (1078) with micronutrients, GPS fixes, UI improvements
Products:
- 5ka scraper: recursive category BFS, 3103 products imported
- ION scraper (web.ion.ru): fetch-based, 1111 products with full vitamins/minerals
- Seed DB now 13480 products (magnit 8627, 5ka 3103, ion 1078, kdv 672)
- 13 micronutrient columns added: Na, K, Ca, Mg, P, Fe, vit A/B1/B2/B3/C/E, carotene
GPS fixes:
- computeStats sorts points by seq/ts before calculating (fixes wrong elevation/kcal)
- Trail restore on recording screen sorts points
- Old tracks with NULL seq repaired via migration (db.js)
- Activity type saved to localStorage, restored on GPS screen
UI:
- Track points invisible but tappable (popup with time/alt/speed)
- Charts: pointer-events:none on canvas when minimized (page scrolls)
- Navigation history stack (20 entries), back gesture doesn't close app
- Session list sort: added id DESC tiebreaker
2026-05-19 23:53:49 +03:00
// Repair old tracks: assign seq to points where it's NULL, ordered by timestamp.
2026-05-21 00:06:50 +03:00
// Only runs if there are NULL-seq points (should be one-time after migration).
try {
const nullSeqSessions = await select (
"SELECT DISTINCT session_id FROM gps_points WHERE seq IS NULL"
Seed DB: +5ka (3103) +ION (1078) with micronutrients, GPS fixes, UI improvements
Products:
- 5ka scraper: recursive category BFS, 3103 products imported
- ION scraper (web.ion.ru): fetch-based, 1111 products with full vitamins/minerals
- Seed DB now 13480 products (magnit 8627, 5ka 3103, ion 1078, kdv 672)
- 13 micronutrient columns added: Na, K, Ca, Mg, P, Fe, vit A/B1/B2/B3/C/E, carotene
GPS fixes:
- computeStats sorts points by seq/ts before calculating (fixes wrong elevation/kcal)
- Trail restore on recording screen sorts points
- Old tracks with NULL seq repaired via migration (db.js)
- Activity type saved to localStorage, restored on GPS screen
UI:
- Track points invisible but tappable (popup with time/alt/speed)
- Charts: pointer-events:none on canvas when minimized (page scrolls)
- Navigation history stack (20 entries), back gesture doesn't close app
- Session list sort: added id DESC tiebreaker
2026-05-19 23:53:49 +03:00
) ;
2026-05-21 00:06:50 +03:00
if ( nullSeqSessions . length > 0 ) {
console . log ( ` [db.js] Repairing seq for ${ nullSeqSessions . length } sessions... ` ) ;
2026-09-29 12:37:15 +03:00
for ( let row of nullSeqSessions ) {
// Batched CASE update (like saveGapFill/saveSmoothedTrack): one UPDATE per ~300 points
// instead of one per point. The per-point loop was ~15000 awaited IPC round-trips on
// merged/split tracks (seq=NULL by design) — many seconds of startup, EVERY boot, and
// for the big sessions it did not even persist before the next launch re-ran it.
let pts = await select (
"SELECT id FROM gps_points WHERE session_id = ?1 ORDER BY ts ASC, id ASC" ,
2026-05-21 00:06:50 +03:00
[ row . session _id ]
) ;
2026-09-29 12:37:15 +03:00
let SEQCHUNK = 300 ; // 2 params/row (id, seq) → under SQLite's 999 bind-var limit
for ( let i = 0 ; i < pts . length ; i += SEQCHUNK ) {
let chunk = pts . slice ( i , i + SEQCHUNK ) ;
let when = [ ] , ids = [ ] , params = [ ] ;
for ( let k = 0 ; k < chunk . length ; k ++ ) {
params . push ( chunk [ k ] . id ) ; let idi = params . length ;
params . push ( i + k + 1 ) ; let sqi = params . length ;
when . push ( ` WHEN ? ${ idi } THEN ? ${ sqi } ` ) ;
ids . push ( ` ? ${ idi } ` ) ;
}
await execute ( ` UPDATE gps_points SET seq=CASE id ${ when . join ( " " ) } END WHERE id IN ( ${ ids . join ( "," ) } ) ` , params ) ;
2026-05-21 00:06:50 +03:00
}
console . log ( ` [db.js] Repaired seq for session ${ row . session _id } : ${ pts . length } points ` ) ;
}
Seed DB: +5ka (3103) +ION (1078) with micronutrients, GPS fixes, UI improvements
Products:
- 5ka scraper: recursive category BFS, 3103 products imported
- ION scraper (web.ion.ru): fetch-based, 1111 products with full vitamins/minerals
- Seed DB now 13480 products (magnit 8627, 5ka 3103, ion 1078, kdv 672)
- 13 micronutrient columns added: Na, K, Ca, Mg, P, Fe, vit A/B1/B2/B3/C/E, carotene
GPS fixes:
- computeStats sorts points by seq/ts before calculating (fixes wrong elevation/kcal)
- Trail restore on recording screen sorts points
- Old tracks with NULL seq repaired via migration (db.js)
- Activity type saved to localStorage, restored on GPS screen
UI:
- Track points invisible but tappable (popup with time/alt/speed)
- Charts: pointer-events:none on canvas when minimized (page scrolls)
- Navigation history stack (20 entries), back gesture doesn't close app
- Session list sort: added id DESC tiebreaker
2026-05-19 23:53:49 +03:00
}
2026-05-21 00:06:50 +03:00
} catch ( e ) { console . error ( "[db.js] seq repair error:" , e ) ; }
2026-05-14 14:17:57 +03:00
}
window . DB . _migrationsReady = runMigrations ( ) . then ( ( ) => {
console . log ( "[db.js] migrations done" ) ;
} ) . catch ( e => {
console . error ( "[db.js] migration error:" , e ) ;
} ) ;
2026-05-13 10:21:24 +03:00
console . log ( "[db.js] window.DB ready, keys:" , Object . keys ( window . DB ) . length ) ;