- "Уд." button (red) next to "Ред." on product card
- deleteProduct: nullifies food_log refs, zeros recipe_ingredients refs, deletes product
- List refreshes after deletion
Was: for loop with 3 IPC calls per duplicate group = thousands of
round-trips = 10+ minutes on phone. Now: 3 bulk UPDATE/DELETE
statements that run in SQLite directly. One-time migration,
subsequent launches skip (COUNT=0).
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.
ATTACH UPDATE with correlated subqueries on name_lower was doing
full table scans on 13000 rows. 56 seconds on desktop, 2-5 min
on phone. User saw "загрузка..." for minutes on every launch.
Fix: CREATE INDEX on name_lower in both main and seed DBs before
the UPDATE. Also pre-built index in seed DB file.
Benchmarked: 56.5s -> 0.09s on desktop. Phone should be <1s.
- sourceBadge() helper renders colored badge by source field
- searchProducts SELECT now includes source column
- getFoodForDate SELECT includes p.source as product_source
- Badge shown on product cards in search and food log entries
- CSS: .src-badge with per-source colors (pastel, dark-theme friendly)
Root cause: addProduct INSERT didn't include name_lower column.
Search uses WHERE name_lower LIKE — products with NULL name_lower
never found. User entered 20 min of strawberry data for nothing.
Fixes:
- addProduct: includes name_lower = name.toLowerCase()
- Added phosphorus_mg and carotene_mcg to addProduct INSERT
- PRODUCT_MICRO_FIELDS: subset of MICRO_FIELDS for product forms
(excludes supplement-only fields: omega, EPA/DHA, fish oil etc.)
- showAddProductModal and showEditProductModal use PRODUCT_MICRO_FIELDS
- Rust: removed stale inserted+=1 line
- Seed DB v8: added "Клубника свежая" (alias for "Земляника садовая")
Was: 13000 individual INSERT OR IGNORE in a loop = 2+ minutes.
Now: one INSERT ... SELECT FROM seed.products = seconds.
Flow:
1. ATTACH seed DB
2. INSERT OR IGNORE ... SELECT (bulk, new products)
3. UPDATE store products with KBJU + micronutrients (user_edited=0 only)
4. UPDATE nutrients_guessed flag
5. INSERT OR REPLACE seed_version into profile
6. COMMIT (all or nothing — crash-safe)
7. DETACH
seed_version written INSIDE transaction — if app crashes before COMMIT,
next launch re-does the import. No partial state.
New column: user_edited INTEGER DEFAULT 0 on products table.
- JS migration adds column
- showEditProductModal sets user_edited=1 on save
- Rust seed import: UPDATE store products (source IN magnit/5ka/kdv/ion)
WHERE user_edited IS NULL OR user_edited = 0
- Now updates KBJU + all micronutrients from seed (not just INSERT OR IGNORE)
- User edits preserved: user_edited=1 products never touched by seed import
- Manual/recipe_import products also never touched (source check)
Flow: seed update fixes wrong KBJU (e.g. bread 268→243) automatically,
but if user already fixed it manually — their fix stays.
Product editing:
- "Ред." button on expanded product card
- showEditProductModal: edit name, KBJU, all micronutrients
- Updates product in DB directly, refreshes search
Bread fix:
- "Хлеб Тостовый пшеничный Аютинский 280г" was 268 in Magnit data
- Package says 243 kcal/100g — corrected in seed DB
- Seed version 7
Users can now fix any wrong KBJU directly from the product card
without re-importing or editing the seed DB manually.
Export was broken on Android — blob download (URL.createObjectURL)
doesn't work in WebView. Now uses Tauri save_file command which
triggers SAF file picker (same as GPX export).
Replaced cryptic arrow icons with readable text: "Импорт" / "Экспорт"
on both recipes and supplements screens.
Custom product:
- "+" button next to search on Products tab
- showAddProductModal: name, KBJU, + all MICRO_FIELDS (34 nutrient fields)
- All optional except name
- Creates product with source="manual"
- Refreshes search after save
Button style:
- .add-btn: 0.5px border with primary color, rounded corners (6px)
- Min 40x36px for finger-friendly tapping on retina
- Flexbox centered content
12 mcg beta-carotene = 1 mcg retinol equivalent (vitamin A).
Carotene from food and supplements now adds to vit_a_mcg total.
Removed carotene_mcg from RDI table and NUTRIENT_LABELS —
no longer shows as separate "b-car 0%" chip misleading users.
Carrot 150g: 18000 mcg carotene / 12 = 1500 mcg vit A = 188% RDI.
Supplement field "Бета-каротин" kept in form for data entry.
- 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.
B9 (folate) label said "мкг" but packaging shows "мг" — user entered
0.4 mg, DB stored 0.4 mcg, 0.4/200 = 0%. Fixed: label says "мг",
mult:1000 converts to mcg on save, back to mg on display.
Same for selenium: packaging uses mg, DB uses mcg.
Beta-carotene showing 0% is correct — Daily Men supplement contains
retinol acetate (vit A), not beta-carotene. They are different forms.
Size comparison was unreliable — SQLite reuses free pages internally,
so adding products didn't change file size. New seed never copied.
Now: always overwrite on every launch (~8MB, <1 sec).
VACUUM reduced seed from 8.5MB to 8.2MB.
Was: upper micro-strip (supplements only, scroll) + lower nutrient-strip
(food+supps, expand). Now: ONE strip at top that does both.
Collapsed: horizontal scroll, single row, no wrap.
Tap: expands to full wrap view showing all nutrients.
Tap again: collapses back to scroll row.
Data source: food (product micronutrients on-the-fly) + supplements.
Sorted by % RDI ascending (most deficient first).
Removed: buildMicroStrip() call, .micro-strip element, old scroll handler.
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).
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
When a sub-modal opens over a parent modal, querySelector returns the
FIRST (parent) overlay, not the new one. Compare items, recipe buttons,
goal markers — none of them got onclick bindings because they searched
in the wrong overlay.
All 11 occurrences replaced with [...querySelectorAll(...)].pop() to
always get the topmost (most recently added) modal overlay.
tauri-plugin-sql execute() returns { lastInsertId } but it's unreliable
(returns 0 or undefined for INSERT). New recipe got rid=undefined,
then addRecipeIngredient failed with NOT NULL constraint on recipe_id.
Fix: query actual id after insert with SELECT id ORDER BY id DESC LIMIT 1.
Edit worked because rid came from the existing recipeId parameter.
Added BMR.estimateRealBmr(weights, foodTotals):
- Uses actual weight delta + total food intake over 30 days
- Formula: realExpenditure = (totalIntake + dWeight_kg * 7700) / days
- 7700 kcal per kg of fat, negative dWeight = weight loss
- Requires 3+ days of data and 2+ weight entries
- Sanity check: rejects < 500 kcal/day as bad data
- Overrides formula BMR when enough data available
renderToday now queries recent weights + food and uses real BMR
if calculated, falling back to Mifflin-St Jeor formula.
Two issues:
1. expend was null when BMR=null, even if GPS sessions had kcal.
Now shows activity kcal alone if BMR unavailable.
2. calcBmr returned null if height/birth_year missing, but user
had weight in profile. Now falls back: weight_log -> latestWeight
-> profile.weight_kg. Also returns bmr_formula if height missing
instead of null.
BMR calculation chain:
- bmr_estimated (manual override) takes priority
- Then formula: needs height + birth_year + weight (from any source)
- Then bmr_formula (pre-computed value in profile)
- Last resort: null → expenditure shows activity kcal only
Found 5 places that did overlay.remove() + history.back() directly
instead of going through winStack:
- Session detail recalculate/refresh
- Delete supplement
- Delete bike
- Delete goal marker
- Goal marker add button
- Compare session selection
All converted to: winStack.pop().close() + _skipPops + history.back()
No more direct overlay.remove() outside of winStack close handlers.
Single winStack[] array + single popstate handler replaces:
- Per-modal popHandlers (removed)
- _modalPopHandled flag (removed)
- overlay._pushFullscreen (removed)
- _suspended/resume mechanism (removed)
How it works:
- winPush(type, closeFn): push entry + history.pushState
- winPopTop(): pop top entry, call close(), return true
- closeModal(): pop all entries down to stackBase, history.go(-count)
- _skipPops counter: closeModal's history.go fires popstate — skip N events
Every open (modal, fullscreen map, fullscreen chart) = 1 winPush.
Every back = 1 winPopTop via popstate.
closeModal for buttons = pop N entries + history.go(-N) + skip N pops.
Sub-modals work because stackBase tracks where each modal started.
Ingredient modal pops only its own entry, recipe editor stays.
Root cause: closeModal of sub-modal called history.back(), parent
modal's popHandler was resumed and caught the popstate, closing itself.
Fix: popHandler checks _modalPopHandled flag first. When sub-modal's
closeModal sets the flag before history.back(), parent popHandler sees
it and skips. Global handler also sees it and skips.
Removed suspend/resume mechanism — replaced with simple flag check.
Each popHandler also checks overlay.parentNode for safety.
Problem: ingredient modal's closeModal called history.back(), which
triggered recipe editor's popHandler → closed recipe editor too.
Fix: modal suspend/resume system.
- When sub-modal opens, parent modal's popHandler is suspended
- When sub-modal closes, parent is resumed
- Parent popHandler ignores popstate events while suspended
- closeModal resumes parent before calling history.back()
Also: closeModal now pops exactly 1 history entry (not _historyDepth).
The _historyDepth counter tracks fullscreen entries within the modal,
which are handled by popHandler's decrements. closeModal only needs
to pop the modal's own base entry.
Ingredient modal's onSubmit returned undefined → showModal treated it
as success → closeModal + renderScreen → killed the parent recipe editor.
Fix: ingredient onSubmit returns "norender", showModal checks for it
and skips renderScreen. Modal still closes via closeModal/history.back,
returning to the recipe editor with the new ingredient added.
Every fullscreen (map/chart) inside a modal now pushes its own history
entry via overlay._pushFullscreen(). Back pops it, popHandler collapses.
No more re-pushing, no flag juggling between handlers.
History stack trace:
click track → push {modal}
expand map → push {modalFs}
back → pop {modalFs}, collapse map, stay in modal
back → pop {modal}, close modal, return to track list
back → pop {screen:gps}, return to previous screen
Map toggle: expand pushes via _pushFullscreen, collapse calls history.back()
Chart toggle: same pattern — push on expand, back on X button
closeModal: pops all entries this modal owns (_historyDepth counter)
Fixes: back from fullscreen map/chart no longer closes app,
expand button works correctly after back-collapse
Map fullscreen:
- Removed mapIsFullscreen variable, use CSS class as single source of truth
- popHandler and toggleMapFs now both check mapEl.classList
- After back-gesture collapse, expand button works on first click (not second)
closeModal:
- Sets _modalPopHandled flag before history.back()
- Prevents global popstate handler from double-processing the event
- Fixes: back from modal no longer closes the app
Removed duplicate fsChart block (caused SyntaxError)
Idea 2 — Hill progress markers:
- goal_markers table, CRUD, map-based placement
- Session detail shows closest approach to each marker
- Progress comparison with previous sessions
Idea 6 — 7-day kcal balance widget:
- Rolling average below summary cards on Today screen
- Green = deficit, red = surplus
Idea 7 — Weight vs calorie correlation chart:
- Dual Y-axis: weight (left) + 7-day rolling balance (right)
- Added to stats section in renderStatsCharts
Idea 8 — Food cost of workout:
- Shows session kcal as portions of frequently eaten foods
- "460 kcal = 2.3 x гречка или 1.4 x творог"
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
Navigation:
- _modalPopHandled flag prevents global popstate handler from firing
on the same event that modal popHandler already processed
- Fixes: back from fullscreen map/chart no longer exits the app
Screen wake lock:
- JS: navigator.wakeLock.request("screen") on recording screen
- Kotlin: FLAG_KEEP_SCREEN_ON on GPS start, clearFlags on stop
- Screen stays on while recording is visible in foreground
- Released when navigating away from recording screen
Multi-segment tracks:
- GPS gaps > gapThresholdSec (default 30, configurable) split track
into separate polyline segments on the map
- Each segment has own green start / red end markers
- Shows clearly where GPS signal was lost vs continuous tracking
Settings:
- "Макс. пауза GPS (сек)" — gap threshold for segment splitting
- Physics constants (aero drag, air density, pedal efficiency)
moved from hardcoded to GPS.config + settings UI
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
Root causes fixed:
1. closeModal had replaceState AFTER async history.back() — replaceState
overwrote the current entry before back() fired, causing skip-to-today
2. Chart fullscreen in modals used pushState — each open/close cycle
added extra history entries, exhausting the stack
3. Map collapse in popHandler didn't call invalidateSize/fitBounds
Changes:
- closeModal() simplified: just remove overlay + history.back()
No more replaceState race condition
- Chart/map fullscreen inside modals: NO pushState, CSS-only
popHandler detects .chart-full/.sd-map-full and collapses, re-pushes modal
- Map collapse: uses saved _leafletMap ref and _bounds for proper resize
- Init: renderScreen with fromPopstate=true to avoid navStack duplication
- Modal popHandler searches within overlay only (not global document)
Fixed user-reported bugs:
1. Map collapse keeps zoom → now calls invalidateSize + fitBounds
2. Chart open → back → back = app exit → now back closes chart, back closes modal
3. "<-" goes to Today instead of track list → fixed closeModal race
4. X on chart → back = correct → was already working
5. Chart open → back back = app exit → same fix as #2
Native service (Kotlin):
- maxSpeedMs filter: points with speed > max are not recorded
- Config passed as 4th param: start:minSpeed:maxAcc:minDist:maxSpeed
- Default 60 km/h (16.67 m/s)
Settings:
- "Макс. скорость (км/ч)" field in GPS config section
- Saved to localStorage with other GPS settings
Recalculate fixes:
- Used cleaned sorted array instead of original points (was computing
stats on dirty data with glitches -> hundreds of thousands of kcal)
- parseFloat on weight from profile (string "88" + 12 = "8812" bug)
- seq re-assignment only runs when points were actually deleted
- Glitch points deleted from DB on recalculate (not just skipped)
Track 22 May showed 31km instead of 12.6km because 4 points had
wrong seq order — native service and WebView GPS interleaved.
Sorting by seq placed them wrong, creating 2km+ teleport jumps.
Fixes:
- All sort calls now use timestamp only (seq was unreliable)
- computeStats skips segments > 180 km/h (GPS teleportation)
- After recalculate: 22 May track should show ~12.6 km
Critical kcal fix:
- Profile weight is stored as string ("88"), not number
- "88" + 12 = "8812" (string concat) → mass 8812kg → 40000 kcal
- Fix: parseFloat(weightKg) at top of computeStats
- Also clamp gradient to +-50% to prevent NaN from GPS spikes
Navigation:
- Chart fullscreen on Today screen now has popstate handler
- Global popstate: collapse any visible .chart-full before skipping stale entries
- Both X button and back gesture properly collapse chart
Removed egg hardcode:
- product_units no longer auto-seeded with egg data
- User adds per-product unit mappings manually
- Eliminates slow LIKE query on 13000 products at every startup
Noted bike settings from user: weight 16kg, wheel 24", chainring 40, cog 18, mixed
Migrations:
- Batch PRAGMA table_info per table instead of N separate hasColumn calls
- Reduced IPC calls from ~22 to ~10 per startup
- Egg product_units seed only runs once (checks COUNT first)
- Removed unused hasColumn function
Recalculation:
- recalculateSession now re-orders points by timestamp
- Assigns seq = 1,2,3... in timestamp order
- Only updates if order is wrong (skips if already correct)
- Fixes tracks where points were recorded out of order
Profile:
- Added weight_kg field to profile modal (was only in weight_log)
- Weight fallback chain: weight_log -> profile -> 80kg
Bike modal:
- Active bike toggle: replaced checkbox with Да/Нет chip buttons
Navigation:
- navStack persisted to localStorage, restored on app restart
- Browser history seeded from saved navStack
- Last screen restored after restart (not always "today")
Daily expenditure:
- getActivityTotalsForDate now includes gps_sessions.total_kcal
- getDailyActivityTotals (stats chart) also includes GPS sessions
- GPS bike rides now appear in daily kcal expenditure
Physics kcal fix:
- Use raw altitude (not smoothed) for gradient in power calculation
- Smoothed altitude was killing real slopes (296 vs 460 kcal)
- Smoothed altitude kept only for displayed elevGain/Loss
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