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
Navigation:
- Stale {modal:true}/{chartFull:true} history entries after app restart
caused back gesture to do nothing (no screen to render, no modal open).
Now global popstate handler skips these stale entries with history.back().
Rust seed import:
- Fixed SQL operator precedence in nutrients_guessed UPDATE:
WHERE (guessed IS NULL OR guessed = 0) AND name_lower IN (...)
was previously parsed as: guessed IS NULL OR (guessed = 0 AND ...)
which matched ALL products with NULL guessed regardless of name.
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
Old logic: copy only if file doesn't exist. App updates with
newer seed DB were ignored because the old file was already there.
Now compares file size with asset size and overwrites on mismatch.
Recipes:
- Cooking methods table + 6 presets (boiling, frying, steaming etc.)
- Recipe editor with live KBJU calculation including cooking loss
- Recipe detail view, log-to-diary, ingredient search
- Split food log into "Products" and "Dishes" sections
- Products screen sub-tabs: Продукты / Рецепты
Nutrient enrichment:
- 1620 store products enriched with ION micronutrients (guessed flag)
- Purple tint on guessed food entries, * on daily kcal total
- Seed DB v4 with nutrients_guessed column
Seed import fixes:
- Rust: always refresh seed DB from bundled assets on launch
- Rust: check app_config_dir for main DB (plugin-sql stores there on desktop)
- Rust: ATTACH-based UPDATE to sync nutrients_guessed for existing products
- Rust: read all 27 columns including micronutrients
- Rust: emit seed_progress events + indeterminate progress bar animation
- JS: boot screen with status labels during migrations/import
- JS: nutrients_guessed column created in JS migration as safety net
Search:
- Results sorted by starts-with-query first, then by name length
- Default limit raised to 200
- Modal search results scroll without max-height cap
Bug fixes:
- product_id=0 for custom recipe ingredients (NOT NULL constraint)
- getRecipe JOIN for cooking_method_name
- calcTotals applies cooking loss from selected method
- Ingredient modal targets last overlay (not parent)
- select elements dark theme (color-scheme: dark)
- renderToday wrapped in try/catch
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
GPS background recording:
- Batched writes (10s flush) in native service
- Polling lock prevents parallel processing
- Track by timestamp instead of line count
- Recovery idempotent via max DB timestamp
- Seq numbers on live recorded points
GPX export:
- Button in session detail
- SAF file picker on Android (ACTION_CREATE_DOCUMENT)
- GPS config in metadata (accuracy, minSpeed, minDist)
Charts:
- Pinch zoom enabled at creation (Hammer.js registers handlers)
- touch-action: pan-y blocks pinch in normal view (page scrolls)
- Fullscreen: CSS-only expand (no canvas manipulation)
- Downsampled to 500 points for smooth rendering
- Back gesture collapses fullscreen before closing modal
Build: --real flag finds first non-emulator device
GPX export: button in session detail, standard GPX 1.1 format.
Background GPS fixes:
- Native service: batched writes (10s flush) instead of per-point
appendText — eliminates race condition with reader
- JS reader: skip corrupt lines, sort by timestamp, track by ts
instead of line count — handles partial writes and reordering
- Recovery: idempotent import using max DB timestamp
- Live recording: seq numbers on every point
- FileObserver watches correct path (app root, not files/dev.baldav.app)
- Removed ACCESS_BACKGROUND_LOCATION (causes throttling)
Nav: "Трекинг" button on GPS screen when recording active.
Products seed: 9299 products bundled in products_seed.db, Kotlin
copies from assets to filesDir on Android, Rust imports into main
DB on first launch with version check for future updates.
Settings: profile button, offline toggle, GPS permission, app info
with git hash and build date from build.rs.
Build: version synced to Cargo.toml, unified import (GPX/FTB by
extension), chart zoom Ctrl+scroll only, scroll wheel zoom disabled
on detail map.