Gap-fill (track detail): GPS.fillGaps now returns {points,gaps} with per-gap
status; the 🧩 control previews ONLY the filled dropouts (cyan casing; unfilled
= red dashed) with a Save/Cancel panel. Save is a reversible in-place patch
(estimated points added, original untouched; DB.saveGapFill/revertGapFill +
estimated/gap_filled columns), then recalculateSession. Revert removes them.
Route planner: renamed menu 'Нарисовать маршрут' → 'Нарисовать тренировку'
(it makes a workout session, not a route); routes empty-state now points at the
on-phone planner. showRouteEditor rewritten per-leg: each leg road (BRouter) or
straight independently, road by default, tap a leg to toggle it, long-press to
insert, silent straight fallback where no road; the global button sets all legs.
Variant 5 from the findings doc, as GROUND TRUTH for validating the
from-track wind estimate — it does NOT feed kcal/power. Chosen scope
(per user): a toggle button now, binary "in the face" only, voice later.
- rec screen button "Ветер в лицо": tap opens a headwind interval, tap
closes it; the open state is highlighted and survives a screen re-render
- GPS.toggleWindMark()/_closeOpenWindMark(); recording.windMarks =
[{startTs,endTs}] ms, persisted to localStorage, written to the DB on
finish and restored on crash recovery (a dangling interval auto-closes)
- gps_sessions.wind_marks (JSON) + DB.setSessionWindMarks (separate from
finishGpsSession so recompute paths preserve marks); parsed in getGpsSession
- rec-bottom wraps now so the third button never overflows a narrow screen
Analysis (marks vs estimate) stays in tools/track once enough km accumulate.
Manual pause (hand-paused at foot of a climb, walked up, resumed) was
counted as riding: computeStats bridged the GPS gap for distance and
elevRef survived the gap, leaking the on-foot climb into elevation gain.
- gps_sessions.pauses (JSON) now persists manual-pause intervals
- computeStats(opts.pauses): a segment crossing a pause is excluded
entirely (no distance/speed/elevation/kcal), elevRef reset, paused
time subtracted from duration; automatic GPS gaps still interpolated
- altitude glitch guard: an adjacent smoothed step steeper than
maxRealGradePct (100% = 45deg) is not banked and does not move elevRef
(covers Android no-fix 0.0); a smooth change to any value (a real 0
near a river) still passes
- recorder writes "alt":null when hasAltitude() is false (real fixed 0 kept)
- pauses threaded through live-stop, recalculateSession, recovery and bulk recompute
- new-tracks only (old tracks never stored pauses; no manual segment-cut UI)
Also: track-detail header shows a click-to-copy track ID.
- 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).
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.
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.
Routes:
- routes table + CRUD helpers (db.js), GPX rtept/trkpt/wpt parser (gps.js)
- "Saved routes" screen (import/rename/recolor/delete, one active at a time)
- Route overlay on both the recording map and the past-track detail map,
toggled by an "M" button; default ON at Start when a route is selected
- Draw after real map-load via async whenMapReady(), no setTimeout
Road colors:
- per-activity configurable colors (walk/bike/run); fix walk roads (atype "w")
- get_road_points maps run -> "r" (lib.rs)
De-jitter (Savitzky-Golay):
- GPS.smoothTrackSG (quadratic LS per window, iterated, endpoints anchored)
- reversible storage: lat_smoothed/lon_smoothed + session.smoothed flag
- detail-screen tuning panel (window/iters sliders, live preview,
save smoothed / revert original, kcal recomputed on new points)
- accuracy_m column per point for later accuracy-gated recompute
Active map-button state restyled to inverted (dark glyph on translucent
white) for OLED; detail map safe-area handled in fullscreen.
- 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.
- Native: hard-reject fixes coarser than the Min-accuracy setting (drops
coarse cell ~800m that stuck the track); remove low-accuracy and adaptive
relaxation branches so only accurate fixes are recorded, honest gaps otherwise
- Store bearing_deg per point (gps_points migration + addGpsPoint)
- Default Min-accuracy 20 -> 50m (one-time migration, respects user override)
- Fix: http_request was synchronous with no timeout; an unreachable host
(LAN sync on startup) blocked the whole UI ~30s. Now async + spawn_blocking
+ connect(4s)/global(12s) timeouts, fails in background without freezing
- Bundled prior work: course-up map orientation, compass calibration,
online elevation refine (open-elevation), distance gap-gate fix
showSessionDetail rendered its map/charts/buttons inside setTimeout blocks
guarded by renderToken. On WebKitGTK the deferred render was voided, so the
track detail showed no map, charts, or fullscreen button (while the
synchronously-rendered roads view worked).
- Remove all setTimeout/requestAnimationFrame/IIFE from showSessionDetail;
render map, charts and wire buttons synchronously right after the modal
markup exists (like showRoadsMap). Early `return`s become `if` guards;
goals/food-cost keep real `await`. Force layout (offsetHeight) + call
invalidateSize so Leaflet reads the real container size in the modal.
- Fix large i64 session-id corruption from tauri-plugin-sql 2.4.0 (ids come
back off by 1 as JS numbers). Read/pass session ids as text via
CAST(id AS TEXT) AS id_str everywhere they are reused: Trek list, Today
click, compare modal/view, goal-marker prev sessions, map default-center,
and importGpxSession (whose points were orphaned by the corrupted id).
SQLite coerces the string to the INTEGER column. Point ids (max ~135k)
are small and unaffected.
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
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).
Dedup fix:
- Dedup SQL now runs BEFORE seed version check (was inside "if new version" block,
never executed when version matched). 16675→8330 products.
- UNIQUE INDEX on (name_lower, source, COALESCE(store_id,'')) prevents future dupes
- Errors propagated via map_err (no more silent failures)
RDI standards from DB (not hardcoded):
- rdi_standards table: RU, EU, US
- rdi_values table: nutrient_key + value per standard
- 26 nutrients seeded for all 3 standards
- BMR.loadRDI(code) reads from DB, replaces hardcoded RDI object
- Settings: "Стандарт РСП" dropdown (РФ/ЕС/США)
- Profile stores selected standard, loaded at init
- Fallback: hardcoded RU values until DB ready
Dedup products in Rust import (not JS — JS was too slow/hung):
- Key: (name_lower, source, COALESCE(store_id,''))
- Same product from same store = duplicate, deleted
- Different sources (magnit vs ion vs manual) = separate products
- Reassigns food_log + recipe_ingredients refs before deleting
- UNIQUE INDEX prevents future dupes (INSERT OR IGNORE now works)
- Tested: 16675 -> 8330 in 0.15s on device DB copy
Removed JS dedup migration (replaced by Rust).
Order in import_seed_products:
1. Index name_lower
2. Dedup (reassign refs + delete)
3. UNIQUE INDEX
4. INSERT OR IGNORE from seed
5. UPDATE store products
6. seed_version + COMMIT
- "Уд." 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.
- 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 "Земляника садовая")
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.
- 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.
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
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.
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
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
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
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
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
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.