Commit graph

65 commits

Author SHA1 Message Date
b4255ef2d5 Block product deletion if used in food log or recipes 2026-05-31 00:48:23 +03:00
bcb4124be0 Delete product button on expanded card, deleteProduct DB function
- "Уд." button (red) next to "Ред." on product card
- deleteProduct: nullifies food_log refs, zeros recipe_ingredients refs, deletes product
- List refreshes after deletion
2026-05-30 21:08:32 +03:00
d365554a7b Fix dedup migration: 3 bulk SQL queries instead of per-dupe loop
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).
2026-05-30 20:55:20 +03:00
0136b8ae0e 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
81125c03fa Remove unused Emitter import and mut inserted warning 2026-05-30 16:32:26 +03:00
44dfa0b88a Add name_lower index: seed import 56s -> 0.09s (600x faster)
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.
2026-05-30 16:24:47 +03:00
8d9b5aaf7e Source badges on products: Магнит (red), 5ка (green), KDV (orange), ИОН (gray), Свой (yellow)
- 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)
2026-05-30 15:59:50 +03:00
2df5908fca Fix product save: add name_lower, product-only micro fields, strawberry
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 "Земляника садовая")
2026-05-30 15:33:59 +03:00
f20d45aa18 Bulk seed import: INSERT SELECT instead of per-row loop
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.
2026-05-29 22:33:12 +03:00
ca45a5acc0 Protect user-edited products from seed overwrites
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.
2026-05-29 22:12:37 +03:00
3789747729 Edit product from search card, fix Ayutinskiy bread kcal
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.
2026-05-29 22:01:52 +03:00
25849b8537 Fix export: use Tauri save_file instead of blob, text button labels
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.
2026-05-29 21:27:31 +03:00
5ecce90744 Export/import recipes and supplements via JSON files
Recipes:
- Export all: downloads baldav-recipes.json with ingredients + product KBJU
- Export one: button in recipe detail downloads {name}.json
- Import: reads JSON file, creates recipes + missing products

Supplements:
- Export all: downloads baldav-supplements.json
- Import: reads JSON, creates supplements (skips id, gets new one)

Buttons: arrow-down (import) and arrow-up (export) next to + on both screens.
Uses downloadFile() helper for browser download, pickFile() for file selection.
Import matches products by name, creates missing ones from exported KBJU.
2026-05-29 21:13:44 +03:00
7b41382ea4 Add-btn border: gray (--border) instead of primary 2026-05-29 20:30:36 +03:00
f871e5bb28 Add custom product with full nutrients, bigger + buttons with border
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
2026-05-29 18:21:49 +03:00
b4b2914d00 Convert beta-carotene to vitamin A, remove from nutrient strip
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.
2026-05-29 18:09:29 +03:00
cbac8d5d7b 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
e6674e2014 Fix B9 and Se showing 0%: unit mismatch mg vs mcg
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.
2026-05-29 17:53:57 +03:00
22a2f6296d Update PROJECT_MAP, README, session logs with current stats 2026-05-29 17:47:58 +03:00
4d97ba607b Always copy seed DB from assets, VACUUM seed to reclaim space
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.
2026-05-29 15:38:10 +03:00
636de9bc25 Add EPA and DHA fields to supplements (omega-3 breakdown) 2026-05-29 15:21:59 +03:00
c986b6af9a Russian names for minerals in supplement form: Кальций, Магний, Железо etc. 2026-05-29 14:59:15 +03:00
818276d60c Merge two nutrient strips into one: scroll collapsed, wrap expanded
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.
2026-05-29 14:55:59 +03:00
de5cfd5c21 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
885ef27f36 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
03358964e2 Fix all querySelector('.modal-overlay') to grab last (topmost) overlay
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.
2026-05-29 10:41:10 +03:00
d5fa37a9fa Fix recipe save: lastInsertId was undefined, ingredients got null recipe_id
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.
2026-05-29 09:25:30 +03:00
1f53b8fcc2 Add try/catch + alert on modal save button to surface hidden errors 2026-05-28 23:56:37 +03:00
45341ddba4 Real BMR from weight change history, not just formula
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.
2026-05-28 23:17:45 +03:00
2b147ef1dc Fix expenditure showing -- when BMR or profile incomplete
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
2026-05-28 23:15:35 +03:00
5f0c21741c Fix remaining overlay.remove bypassing winStack
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.
2026-05-28 22:42:45 +03:00
4960f2a863 Rewrite fullscreen as history entries: each open=push, each back=pop
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.
2026-05-28 22:24:57 +03:00
94ad61f5b9 Fix ingredient modal closing recipe editor — no setTimeout hacks
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.
2026-05-28 14:31:20 +03:00
f395e8ec07 Fix sub-modal (ingredient) closing parent modal (recipe editor)
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.
2026-05-27 22:55:43 +03:00
d59013cc62 Fix ingredient modal closing recipe editor
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.
2026-05-27 20:53:00 +03:00
cb5a916100 Rewrite fullscreen as history entries: each open=push, each back=pop
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
2026-05-27 20:41:33 +03:00
a032af3b00 Fix map fullscreen state sync, closeModal flag, remove duplicate code
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)
2026-05-27 19:36:34 +03:00
9384bb8d2e Goal markers, 7-day kcal balance, weight-calorie correlation, food cost
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 творог"
2026-05-27 17:45:50 +03:00
1d6efcd851 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
7d1e4b3069 Add IDEAS.MD — roadmap ideas for BalDav
8 feature ideas: tire pressure Crr, hill progress markers,
lap comparison, auto climb segments, bike maintenance log,
kcal balance widget, weight-calorie correlation, food cost of climbs
2026-05-27 16:28:05 +03:00
d7527bfd45 Fix back-gesture app exit, screen wake lock, multi-segment tracks
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
2026-05-27 15:29:22 +03:00
7cbfd98e27 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
0701fbd723 Rewrite modal navigation: fix 5 back-gesture bugs
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
2026-05-25 18:18:07 +03:00
30b0ac49f3 Max speed filter in native service, fix recalculate bugs
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)
2026-05-25 15:20:24 +03:00
0dd4a67e7b Sort by timestamp not seq, filter teleportation glitches
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
2026-05-25 09:25:50 +03:00
9b5c9a204c Fix string weight causing 40k kcal, navigation, remove egg hardcode
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
2026-05-25 03:00:47 +03:00
606481103c Faster migrations, recalculate fixes point order, egg seed guard
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
2026-05-25 02:42:52 +03:00
23db25dc8c Delete session returns to track list without history.back race 2026-05-25 02:34:43 +03:00
165e7e7e9c Weight in profile, nav stack persistence, GPS kcal in daily expenditure
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
2026-05-25 02:33:04 +03:00
4740bdbe11 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