Commit graph

58 commits

Author SHA1 Message Date
9251c1dd90 ble: barometer on its own connection - simultaneous HR+baro, auto-connect, UI
Separate BleBaroService so the HR strap and the barometer each hold their own GATT link and
work at the same time (one adapter, several LE connections). Verified on device against a PC
BlueZ emulator of the sensor: BleHR and BleBaro deliver in parallel, no cross-talk.

- BleBaroService: dedicated baro connection (scan-before-connect + direct-connect fallback,
  ESS subscribe, writes ble_baro_data.json). MainActivity routes baro connects to it and
  auto-connects both HR (ble_device.json) and baro (baro_device.json) at start.
- Rust: ble_baro_disconnect, ble_save_baro_device.
- Settings: a 'Датчики (BLE)' section - heart-rate and barometer as separate rows with their
  own slots, bind/connect/unbind, and a live pressure/temperature/altitude readout.
- Tracking: altitude_baro_m stored per point, preferred in elevation stats (null without baro).
- Record screen: 'Высота' from the barometer (1 Hz, dot marker), temperature from the sensor.
- Fixes: direct-connect on scan timeout; Locale.US JSON (RU decimal comma broke the parse);
  parsing routed by characteristic (baro no longer written into the pulse); VR/HR stop returns
  to the tracks list instead of into details.
- tools/baro-sensor/emulate_baro.py: PC BlueZ peripheral that emulates the sensor for testing.

Emulator-verified only; real board still pending.
2026-10-30 22:46:26 +03:00
9a8ef04230 Hydration: water intake from the food log, drop the "+вода" button
Intake is what you already log (liquids count via water_pct), so there is
nothing to tap mid-ride and no guessing an amount. At session start JS seeds
the native deficit from the food log — need-so-far for the time of day minus
logged intake — via set_hydration_config(start_deficit_ml); the service then
accrues sweat on top. Removes the +вода button, hydration_drink and water_drink.txt.
2026-10-24 01:29:30 +03:00
6183f0c529 Hydration reminder: level-based drink alerts during a ride or VR
A shared Hydration singleton (ticked by the GPS ride service and the HR/VR
service, deduped in one process) accrues the body's water need over an active
session and subtracts what you logged via the "+вода" button. It stays silent
while you keep up — can be quiet for hours — and only beeps LOUD on the alarm
stream (cuts through media volume) once the running deficit passes a threshold,
then re-beeps on a cooldown. Not a fixed timer.

Rust set_hydration_config / hydration_drink bridge JS settings and the "+вода"
button to hydration.json / water_drink.txt that the native service reads.
2026-10-24 01:13:43 +03:00
76efc9238b Roads overlay: glue repeated passes, thicken line by ride count
rebuild_roads now counts how many times each ~2m cell was ridden
(by cell entry, not per point, so a slow crawl stays one pass) and
stores it in road_points.passes. get_road_points returns it; the
overlay draws each stretch at base width x min(3, 1 + 0.2*(passes-1)),
split into runs of equal thickness. Optional via the "Толщина дорог
по проездам" setting (default on). Both the recording-screen roads
toggle and the Дороги screen honour it.
2026-10-22 00:49:18 +03:00
435a0dd27c Tracks: un-merge a merged session back into its original rides (UI button)
A merged session keeps orig_session_id on every point, so split it back:
unmergeGpsSession recreates the original session rows by that id, retags points,
and sets each piece's date/times from its own points. A 'Разделить склейку (N)'
button shows in track detail only when the session is a merge; it un-merges and
recomputes each piece.
2026-10-20 23:49:17 +03:00
f704498d11 Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.

- gps.js: GPS.hrKcalNet; startRecording/stopRecording 'workout' branches;
  the HR poll appends [relSec,bpm] samples.
- db.js: gps_sessions.hr_samples column + setSessionHrSamples.
- main.js: 'Зарядка (пульс)' option; renderWorkoutInline (map-less live
  screen: big HR, timer, kcal, connect-sensor button); renderWorkoutDetail
  (HR chart by zones, load, avg/max).
- tools/hr_log.py: background HR logger (standard 0x180D, no pairing).

Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
74f3ec4d6e Startup: batch the seq repair (per-point loop made cold start ~15s, every boot)
The startup seq repair (runMigrations) assigned seq to NULL-seq sessions
one UPDATE per point. Merge/split leave seq=NULL by design, so recent
merged tracks (10865 + 3614 points) meant ~15000 sequential IPC writes on
every launch - and for the big sessions the writes did not persist before
the next launch re-ran the whole thing. That was the slow start.

Replace it with the batched CASE re-sequence (chunks of 300, like
saveGapFill/saveSmoothedTrack): ~50 UPDATEs instead of ~15000. Verified on
device: repair of all 3 sessions dropped from ~15s to ~1.1s, seq now
persists so it is one-time (boot 2 does no repair, migrations 357ms), 0
NULL seq left, seq matches ts order (0 inversions).
2026-09-29 12:37:15 +03:00
5f58eabcc5 Gap-fill: re-sequence the session by ts on save (filled points landed at the front on merged/split tracks with NULL seq) 2026-09-22 20:58:06 +03:00
dff9342c0d Balance/trend/charts follow the selected day; kcal weight is per-ride (stored) and settings weight auto-syncs to the latest weigh-in 2026-09-22 11:12:24 +03:00
15a2881c6d Split a track at a point; block merging tracks with different bikes 2026-09-21 12:36:03 +03:00
cfadad4690 Merge rides: long-press to multi-select tracks and combine them into one (reversible via orig_session_id) 2026-09-18 00:08:25 +03:00
c564c419db Walk detection in rides: colour it, keep it out of the ridden roads
Stage B of the ridden-roads work. GPS.detectWalk() flags contiguous low-speed
runs (< walkDetectKmh, or the single-speed 30-rpm push speed, for >= walkMinRunSec)
as walking inside a bike/other ride. Persisted per point (gps_points.walk, via
DB.setWalkFlags in recalculateSession + recomputeAllStats). rebuild_roads maps a
walk point to atype 'walk' so it colours as walking and is not treated as a
rideable road. Track detail colours walk runs and shows 'пешком: N мин / X км'.
Verified detectWalk on real tracks: 98% on a mostly-pushed 4km ride vs 1% on a
normal 16km ride.
2026-09-14 12:14:04 +03:00
1c326ab19c Nogo areas: routing avoids impassable spots (planner + engine + RouteServer)
Stage 1 of the nogo/reroute work. Mark impassable circles that BRouter routes
around, end to end:
- DB nogo_areas table + CRUD; drawNogoAreas() red circles (tap to remove).
- BRouter.route(points, profile, nogos): &nogos=lon,lat,r on the RouteServer and
  nogos in the embedded-engine request; Rust brouter_route passes it through;
  Kotlin builds OsmNodeNamed(radius,isNogo,nogoWeight=NaN) -> rc.nogopoints +
  prepareNogoPoints (needs an APK rebuild for offline use).
- Planner: ⛔ button toggles nogo-placement (tap map/line), each leg re-routes
  around nogos. Recording map: ⛔ marks the current spot impassable. Detail map
  shows the circles.
Verified the RouteServer detours around a nogo (2873m -> 3329m).
2026-09-12 22:55:51 +03:00
aa7022ec6b routes: return a reliable id from addRoute (re-read, not lastInsertId)
Found in desktop testing: saving a built route showed 'Маршрут пуст' and dropped
to the list — addRoute returned the INSERT lastInsertId, an i64 the SQL plugin
corrupts by ±1, so showRouteDetail opened a non-existent id. Re-read the id via
CAST(id AS TEXT) (same guard importGpxSession uses). Session log: desktop
verification of all 6 fixes.
2026-09-11 01:25:28 +03:00
e45c92f09a Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
  view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
  locate controls added. Root cause of the plain thin line: ele was dropped when
  building leg geometry — now kept from BRouter, so casing/slope work here and in
  showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
  pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
  alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
  big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
cfe0212bbc BRouter UX: gap-fill preview+reversible save; per-segment route planner
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.
2026-09-10 15:26:05 +03:00
1b10aa67de Wind: "headwind in the face" label button on the recording screen
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.
2026-09-07 17:10:16 +03:00
ec77f67cab GPS stats: exclude manual pauses; reject unrealistic altitude steps
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.
2026-09-07 13:53:40 +03:00
f7b45d98f0 Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
  in name, any order), ranked full-substring > prefix > length. Fixes needing to
  type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
  = magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
  deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
  button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
  version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
  On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
f376e0944d Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.

- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
  grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
  elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
  tappable altitude/grade points, elevation profile chart (steep >8% in red),
  length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
  elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
  drawn in a dedicated pane below the overlay pane, so a recorded track stays on
  top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
  settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
  open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
  getRoutes reports hasEle. Route points are now {lat, lon, ele?}.

Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
bf3c0e2534 Track detail de-jitter: fix overlay stacking, native dialogs, slow save
Fixes to the smoothing/routes UI reported on device:
- Blank detail after save/revert: showSessionDetail was called from inside the
  open detail modal, stacking a second overlay; querySelector("#sd-map") then
  hit the stale, already-initialized Leaflet map and threw, aborting the rest
  of the render (map + speed chart). New reopenDetail() closes the current
  detail (and its fullscreen-map child) and reopens fresh, like recalc/refine.
- Native confirm()/prompt() replaced with in-app appConfirm/appPrompt dialogs
  (winStack + history integrated, hardware back cancels). Used for smoothed
  save, route delete, route rename.
- Slow smoothed save: one UPDATE per point (4000+ IPC round-trips) replaced
  with a chunked CASE update — 18 statements for 4366 points, params kept
  under SQLite's 999 limit. Verified value-for-value on a copy DB.
- Closing the fullscreen map now also closes the smoothing panel (it is a
  fixed element that otherwise lingered over the collapsed map).
- "≈" button active state now reflects panel-open only, not session.smoothed
  (it looked permanently pressed after a save).

Docs: README + PROJECT_MAP updated for saved routes, de-jitter, accuracy_m,
routes table and new gps_points/gps_sessions columns. Version 0.1.393.
2026-07-31 23:51:20 +03:00
9d09b4c4a4 Saved routes overlay + per-activity road colors + reversible track de-jitter
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.
2026-07-31 22:11:55 +03:00
56c2ff4ec7 GPS: safe on-device recovery, net-kcal model, poll/watchPosition dedup
- native_root(): pick app root by presence of gps_live/gps_points files.
  app_data_dir() is the root directly on this Tauri; the old .parent().parent()
  read the wrong dir, so background (screen-off) points were never imported.
- recoverFromFiles: match file->session by id/date, plus session id stamped
  into the native file name (gps_points_<ms>_s<id>.jsonl) for exact binding
  (gps_set_session_id -> Kotlin bindSessionIfPending). Proximity dedup +-2.5s,
  recalc with noDelete (never runs the destructive glitch/>24h filter that
  once wiped real rides), delete already-imported files. Progress bar UI.
- bike kcal is NET (daily balance already books BMR*activityLevel): pedaling =
  max(physics power/efficiency, MET-net by speed), coasting (power<=0, the bike
  would roll unpowered) = basal only. Elevation via cumulative hysteresis.
  recomputeAllStats + "recompute elevation/kcal" menu refresh stored tracks
  without touching points.
- live recording: the native file is the DB source of truth; watchPosition
  only drives the live marker, so a fix is not stored twice with two timestamps.
- track list pagination, SAF local-file picker, tile regions from router.
2026-07-29 15:35:18 +03:00
2051a604e1 GPS accuracy-gated points + bearing; fix startup UI freeze
- 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
2026-07-07 00:28:27 +03:00
5d6025ea9c Desktop: synchronous track-detail render + text session ids
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.
2026-07-03 22:19:58 +03:00
50ec5c21f8 Health-diet.ru products (24K), GPS session safety, build.sh upload fix
Products:
- health-diet.ru scraper (Tampermonkey) + import script
- 11469 products with 37 nutrients, Dodo 258, McDonalds 619, KFC 361, Ashan 2000
- Rust import_seed_products: dynamic column list, auto-create missing columns
- Seed version bumped to 10

GPS session safety:
- Separate gps_points_{ts}.jsonl per session, deleted only after DB save
- Toast on DB errors, NaN guard in finishGpsSession

Other:
- build.sh: router/VPS upload only with -u flag
- Orientation buttons bottom-right, weather via Yandex Pogoda
2026-06-28 23:17:51 +03:00
4ed6e6f039 Activity types (squats/jumprope/stretch/swim), road map, tile import, fixes
Activity types:
- New types: squats, jump rope, stretching, swimming pool
- MET-based kcal auto-calculation (6.0/12.3/2.3/8.0)
- Dynamic modal fields per type (sets/reps, distance, minutes)
- Fix: state.latestWeight is object {weight_kg}, not number

Road map:
- road_points table with 2m dedup, activity type (bike/walk)
- Rust rebuild_roads + get_road_points (bypass SQL plugin 28K limit)
- Segment detection by 100m gap, bike=blue walk=brown

Tile system:
- CSP fix: a.tile.openstreetmap.org matches *.tile pattern
- pack_tiles.js tool, import from file/router, ureq body limit bypass
- Tile errors: done(null,tile) prevents crash cascade

Other fixes:
- Manual routes: skip gap segmentation (synthetic timestamps)
- Draw route: own CSS class, not affected by landscape rotation
- Orientation lock button (A/L), hysteresis 65/35 degrees
- GPS placeholder only when state=waiting_gps
- Food amount inputmode=text for formula support (4*30)
- showModal onMount callback
2026-06-26 21:04:21 +03:00
184b86b18c Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
  commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
  Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
19282ded78 Sensors, walk limits, GPS fixes, water UI, manual routes, Gridnev products, OTA router
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
  jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
  vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
  when accel confirms movement. Walk/run separate speed limits (configurable).
  Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
  GPS silent but accel shows movement. Locale.US fix for sensor JSON.

Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
  Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).

Manual routes: draw on map by tapping points, elevation from past tracks +
  open-meteo API, realistic timestamps by activity speed. Manual sessions
  skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.

Water tracking: water_pct field in product add/edit forms, searchProducts
  returns water_pct, "ml" label for liquids, recipe water from ingredients.

BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.

BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.

Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
  Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
  addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.

Export/import: custom products + recipes + supplements in single JSON bundle.
  Export all or single product. Dedup on import by name_lower + source.

OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
  first (2s timeout), then VPS. Update URL from update_info.json.

Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
2026-06-18 22:54:45 +03:00
7c93bb75c3 OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
  ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
  Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
  configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
  fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
  mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
  reserves strip, GPS sessions in today view, pan on charts, showToast(),
  HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
0bafb2b317 АУП norms with star, nutrient half-life table, recipe nutrients in daily strip
RDI/АУП:
- rdi_values.norm_type column: 'rdi' or 'aup'
- АУП values seeded: omega3 1000mg, EPA/DHA 250mg, lutein 5mg,
  lycopene 5mg, silicon 30mg, vanadium 0.04mg (all 3 standards)
- Nutrient chips show * after label for АУП norms
- Supplement form placeholders show RDI/АУП values (with * for АУП)

Half-life table:
- nutrient_halflife: nutrient_key, min/max/avg days, source
- 32 nutrients seeded with whole-body biological half-life data
- Sources: PubMed, ICRP, Performance Lab, clinical studies

Recipe nutrients in daily strip:
- recipes.cached_nutrients TEXT column (JSON)
- cacheRecipeNutrients() computes and stores per-serving nutrients
- Called on recipe save in editor
- calcDailyNutrients() now reads cached_nutrients for recipe food entries
- getFoodForDate() JOINs r.cached_nutrients

BMR.loadRDI() now loads norm_type alongside values.
BMR.RDI_TYPES exposed for star rendering.
2026-06-02 18:39:09 +03:00
a0c7dfb5e2 Fix dedup: run BEFORE version check, RDI standards from DB
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
2026-06-01 16:45:30 +03:00
b2b6b6a7ef Dedup products, fix labels, fix track comparison, seed v9
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
2026-06-01 14:51:38 +03:00
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
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
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
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
636de9bc25 Add EPA and DHA fields to supplements (omega-3 breakdown) 2026-05-29 15:21: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
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
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
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
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