Commit graph

53 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
82f3dc9d4a ble: read DIY barometer (BaroBalDav) over BLE + firmware
Self-made altitude sensor: ESP32-C3 SuperMini + BMP280 broadcasting pressure and
temperature over standard BLE Environmental Sensing. Read into baldav the same way as
the heart-rate strap (Kotlin BLE service -> file -> Rust -> JS), new device_type 'baro'.

- BleHrService: 'baro' type subscribes to ESS 0x181A (pressure 0x2A6D uint32 0.1 Pa,
  temperature 0x2A6E int16 0.01 C), writes ble_baro_data.json + ble_baro_log.jsonl.
  Every baro branch is gated by deviceType=='baro', so HR/GPS paths are untouched.
- ble.rs: BaroReading, ESS detection, ble_get_baro command (Android reads the file,
  desktop returns None). Registered in lib.rs.
- frontend/js/baro.js: passive window.BARO - hypsometric pressure->altitude using the
  sensor temperature, anchored to GPS altitude at ride start. Nothing runs until
  BARO.start(), so with no barometer the app is unchanged.
- tools/baro-sensor: firmware (PlatformIO main.cpp + Arduino sketch) that reads BMP280
  at max oversampling and advertises the ESS service.

Prep only: no hardware yet, verified by compile (build 0.1.538). Live wiring and
ride-track integration pending the board.
2026-10-30 00:38:12 +03:00
1101016ef4 hydration: spoken 'hydrate now' alert, per-activity sweat, drink reconciliation
- signal is now a loud TTS 'hydrate now' on the ALARM stream (TTS warmed at
  session start), so it is unmistakably BalDav and audible when the phone is not
  in view (walking/VR); the old ringtone was indistinguishable from a system alarm.
- sweat need scales by activity instead of a flat 600 ml/h: bike 600, walk 200,
  run 450, workout/vr 250 ml/h. Walking no longer nags every 15 min.
- deficit split into baseDeficit (seeded from the food log) + accrued sweat;
  logging a drink mid-session recomputes the base and re-seeds via seedSeq, so it
  silences the reminder. seedSeq=0 (profile save / init) never zeroes it mid-ride.
- persistent 'БалДав: попей воды (HH:MM)' notification left as a record.
2026-10-27 21:47:10 +03:00
d25cb0de49 BRouter Part B: show fords/rough tracks on the route + adventure opt-in
- brouter_route gets a format param; routeBrouter returns formatAsGeoJson (WayTags per
  segment) when asked; BRouter.routeGeoJson + GPS.parseRouteGeoJson extract geometry +
  dangers (ford / grade5|bad-trail / ferry) from the tags
- route editor routes road legs via geojson, marks fords (cyan) and rough tracks
  (orange) on the map with a '⚠ брод/грунт' counter, so the rider sees the hell BEFORE
  riding it
- 'Броды: избегать/разрешить' toggle picks trekking vs trekking-adventure (allow_adventure)
  and re-routes; new trekking-adventure.brf profile (asset + staging)
Parser validated against the home RouteServer on a real ford (47.5387,40.3164).
2026-10-26 21:46:56 +03:00
0d5a5986ac Elevation correction from ridden tracks (SRTM base + median offset)
get_elev_correction (Rust): per ~20m cell, the median (altitude_gps_m − altitude_srtm_m)
offset over >= N distinct-session passes. SRTM stays the base; this is only the up/down
nudge where you've actually ridden. JS applies a smoothed per-cell delta to a route's
SRTM elevations in the preview (non-destructive; no coverage -> unchanged).
2026-10-26 21:08:22 +03:00
1410062d85 Live location sharing (botovod) + SMS 'я тут' with Yandex Maps link
Feature 1 - opt-in live location:
- set_live_config Rust command writes live_share.json (url/token stay on device)
- LiveShare.kt singleton posts the current fix to /api/live/push every ~10s from the
  GPS flush loop, on a background thread; diagnostics in live_share_log.jsonl
- profile toggle + endpoint/token fields, off by default; server side is in botovod

Feature 2 - SMS 'я тут':
- send_sms / pick_contact / get_contact_result Rust commands
- MainActivity: system contact picker (no READ_CONTACTS), direct SmsManager send when
  SEND_SMS is granted else the prefilled system composer; SEND_SMS in the manifest
- contact config in profile (manual + picker), 'СМС' button on the record screen with
  an in-app confirm; text carries coords + a Yandex Maps pin link

README + session log updated.
2026-10-26 16:12:11 +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
abc4e3cef0 Strap usage tracking: accrue real HR-delivering time + battery steps natively
The strap's battery % is coarse/laggy (reads 100% for >24h), so to judge real
battery life we accumulate the time it actually delivered HR plus every battery
step into strap_uptime.json in the foreground service (persistent across
sessions). ble_read_strap_uptime / ble_reset_strap_uptime expose and reset it;
Settings shows hours-since-charge + steps with a Charged reset button.
2026-10-20 23:49:17 +03:00
ee47af4479 Workout HR: log samples natively so recording survives screen-off
The JS 2s poll that stored workout HR samples is frozen by Android when the
screen is off, so a sleep-long recording kept only the on-screen stretches
(the strap and live display were fine — the persist path was the gap). The
foreground service now appends every reading to ble_hr_log.jsonl; ble_read_hr_log
exposes it; on stop the workout rebuilds its samples from that log (falling back
to the poll array). ble_clear_hr truncates the log at start.
2026-10-20 23:09:50 +03:00
2d0e51bd8d Prompt to enable Bluetooth/location at recording start + two 'don't ask' settings
User forgets to turn BT/GPS on. Modern Android won't let an app toggle them
silently, so prompt and act: BT via the system enable dialog, location via
the location-settings screen.

- ensureSystemReady(needBt, needGps) runs before startRecording: BT off ->
  confirm -> ble_request_enable; location off -> confirm -> open settings.
  Gated by two settings (baldav_dontask_bt / _gps); declining proceeds.
- GPS.isLocationEnabled polls a new Kotlin check (check_location writes
  location_state.json via LocationManager; read_state_file reads it);
  open_location_settings opens ACTION_LOCATION_SOURCE_SETTINGS.
- Settings: 'Не спрашивать про Bluetooth' / '... геолокацию' checkboxes.
2026-10-17 02:07:44 +03:00
dd1642b6e9 BLE HR: connect manually only, no auto-scan on every ride (battery)
Recording auto-called ble_connect each ride, and the Kotlin service held a
GATT connect to an absent band (radio busy) plus START_STICKY churn -
draining the battery when the band was away or Bluetooth off.

- gps.js _startHrMonitor: drop the auto ble_connect; keep only the cheap HR
  poll so a manual connect (now or mid-ride) lands HR in the track.
- Settings: a 'Подключить' button for the paired band, usable any time incl.
  during a ride. BT off -> ble_request_enable (system enable dialog); on ->
  ble_connect, start the poll if recording, report connected / not found.
- ble.rs: ble_request_enable command (+ handler registration).
- MainActivity: ble_request_enable -> ACTION_REQUEST_ENABLE (BLUETOOTH_CONNECT
  already in manifest).
- BleHrService: START_NOT_STICKY; if the 5s scan doesn't find the address,
  stopSelf instead of directConnect to an absent device.
2026-10-02 14:03:16 +03:00
3e2a6e47f7 Progress on recompute/refine/roads-rebuild + remember map zoom
- .btn-progress fills a button's bg by --pct (setBtnProgress). Elevation
  refine drives it from fetchElevations network batches (0-80%) then save
  (85-100%); recompute from recalculateSession phase milestones; roads
  rebuild from a new Rust roads_progress event (rebuild_roads emits
  [current,total] every ~2000 points - first Rust->JS event in the app,
  adds use tauri::Emitter) so it no longer just hangs silently.
- recalc/refine now reopen the detail via reopenDetail (awaits the
  popstate) instead of the manual pop+history.back+render race.
- Track detail remembers its map view per session (_sdMapView, moveend) and
  restores it on reopen/return instead of re-fitting the whole track; the
  recording map persists its zoom in localStorage across screen switches.
2026-09-29 23:19:37 +03:00
9e0910a35f DB backup: drop data_dir writes, use native_root only (match gps_service_read_points pattern) 2026-09-21 13:36:17 +03:00
d0abb72aba DB backup: use native_root() for the app root (not app_data_dir parent chain) - the fix we already made for point files 2026-09-21 13:22:05 +03:00
bb8a739b7e DB backup: write export/staging next to baldav.db (app_data_dir can be the app root; ../.. hit unwritable /data/user) 2026-09-21 13:19:18 +03:00
682bee6e9a Full DB backup: export all data to a file and restore it (swap DB on next launch) 2026-09-21 13:03:32 +03:00
a30c742bc6 Ridden-roads-preferred router: plan over your own tracks first
Planner now tries a route over roads you have actually ridden before
falling back to BRouter public data:
- Rust get_ride_graph(min_speed): nodes snapped to ~20m grid (walk=1
  points excluded), edges between consecutive same-session points with
  weight = node-to-node distance (consecutive-point distance loses
  intra-cell travel and makes Dijkstra underestimate).
- JS loadRideGraph cache, grid-indexed nearest-node, Dijkstra, rideRoute
  (snap <=60m, drop if >2.5x straight line).
- routeDirty: rideRoute first, BRouter(+nogos) in the gaps. Ridden leg
  has no elevation -> thin line; BRouter -> casing (visually distinct).
- formatDistance now rounds metres (was 966.145...m).
2026-09-14 12:35:39 +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
5fe30399ab Share image Stage 2: native save/share + settings background picker
Rust export_image(filename,base64,action) + read_file_base64(path) (registered).
Kotlin exportimg: branch — base64-decode, then SAF-save the PNG (CreateDocument
image/png) or share it via FileProvider + ACTION_SEND chooser. Settings gets a
'Фон для картинки' row: pick a photo (pickLocalFile image/*), read it as base64,
downscale to <=1080 and store as a data URL in localStorage for the 'С фото'
variant. Rust compiles in tauri dev; native paths need an APK rebuild to test.
2026-09-14 10:38:29 +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
b4046841be BRouter Stage 2 (primitive): download .rd5 segments inside the app
Kotlin owns the download so tiles land in the exact dir the engine reads
(files/brouter/segments4) — no Rust path guessing. processBrouterCommand now
dispatches: dltile:<tile> streams brouter.de/brouter/segments4/<tile>.rd5 to a
.tmp then renames, writing progress/final state to brouter_dl_<tile>.json;
listtiles writes brouter_tiles.json. Tile names are sanitized ([A-Za-z0-9_]).

Rust relays: brouter_download_tile / brouter_download_status / brouter_list_tiles.
JS: BRouter.downloadTile/downloadStatus/listTiles/downloadTileAndWait.

Compiles into the apk. Not yet exercised on device (phone offline) — the tile
selection UI and the end-to-end download->offline-route test are the remaining
Stage 2 work. IMPLEMENTATION_PLAN updated.
2026-09-09 01:11:20 +03:00
a52e998fb6 BRouter: analysis + embed the offline routing engine (Stage 1)
Analysis in docs/brouter-findings.md: how BRouter knows roads (.rd5
segments), routes and reaches low elevation gain (profile uphill/downhill
cost + kinematics), the three call surfaces, and an evaluation of two ideas
— fill GPS gaps along roads, and offline waypoint route planning. License is
MIT; the engine has no desktop-only deps, so it embeds on Android ART.

Stage 1 (embed + bridge, compiles into the apk; on-device route not yet run):
- vendored brouter-1.6.3-all.jar into the Android module (dexes cleanly) and
  bundled the trekking/shortest profiles + lookups.dat as assets, copied to
  files/brouter/profiles2 on launch
- MainActivity: a brouter_command.txt handler on the existing FileObserver
  runs btools.router.RoutingEngine off the UI thread (recipe taken via javap
  from ServerHandler) and returns the track as GPX
- Rust brouter_route command: same file bridge as GPS (request + command,
  poll for the result), registered in the invoke handler
- frontend/js/router.js: BRouter.route(points, profile) -> GPX (parseRouteGPX)
- LICENSE-brouter (MIT notice) kept alongside the vendored engine

Segments (.rd5, ~13-23 MB per 5x5 tile) are delivered later (Stage 2, wired
download inside BalDav). IMPLEMENTATION_PLAN.md tracks the 5 stages.
2026-09-09 00:56:13 +03:00
c3ec590da5 Chestny Znak scanner: DataMatrix -> National Catalog КБЖУ prefill
- ChZ module (frontend/js/chzn.js): import an anonymous session (exported from
  the CRPT app with tools/chzn_export.sh) and keep it alive via /authorize/refresh
  — /authorize/anonymous is Play Integrity-gated so BalDav can't mint its own.
  GS1 code normalization (reconstruct the GS separator before AI 93), check ->
  catalogData good_attrs КБЖУ parser (match by attr group + name prefix, since
  producers name the fields differently), optional nutrition/info vitamins.
- chzn_http Rust command: browser UA + returns {status,body} so 204/401/427 drive
  the session/refresh logic.
- Scanner modal (BarcodeDetector camera + photo/manual fallbacks) -> prefill the
  new-product form; CAMERA permission; settings section to import/refresh session.
- tools/chzn_export.sh: pull + validate the anonymous session from a rooted device.
2026-08-27 03:27:15 +03:00
fd6cf62fc0 Route editor + 1% slope bands + debug-only diagnostics + rotate-map route fix
- Route editor (showRouteEditor): create ("Новый маршрут") or edit an existing
  route's geometry in-app — tap to add, drag a point to move, tap a point to
  delete. Save: new prompts a name (addRoute), edit rewrites points_json
  (updateRoutePoints; drops stale elevations, re-fetch from the preview).
- Slope colouring in discrete 1% bands (green→yellow→red→brown→black, ≥10% black)
  instead of degree buckets; colour-strip legend (slopeLegendHtml).
- Diagnostic logs compiled-in but silenced in release: get_app_info now returns
  debug = cfg!(debug_assertions); JS installs a console.log filter that drops
  lines starting with a diagnostic tag ([poll]/[recover]/[stop]/[GPS]/[HR]/[SD])
  in release only. No more hand-adding/removing logs.
- Fix: saved-route overlay was invisible on the recording (rotating) map — it
  flashed on pinch-zoom then vanished. leaflet-rotate keeps tile/overlay panes
  inside _rotatePane, but the commented-out createPane override meant our custom
  routePane attached to mapPane (outside rotation). Now parent routePane to
  map._rotatePane when present; non-rotating maps unchanged.
2026-08-16 17:15:42 +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
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
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
c72a22a965 Fix dedup FK: delete product_units refs before products, remove PRAGMA hack
FOREIGN KEY constraint failed because product_units table has FK to
products(id) and wasn't cleaned before DELETE. Now: delete orphaned
product_units rows first, then delete dupe products. No PRAGMA needed.

Verified on desktop: 16675 → 8330 with FK enforcement ON.
2026-06-01 22:43:02 +03:00
46ad9022c2 Fix dedup: PRAGMA foreign_keys=OFF before delete, alert on seed error
FOREIGN KEY constraint failed on desktop (rusqlite enables FK by default).
Android was silently failing too. Fix: disable FK during dedup, re-enable after.

Tested: desktop 16675 → 8330 products, idx_products_dedup created.
Also: alert() instead of boot-label for seed errors (visible to user).
2026-06-01 21:25:23 +03:00
ed20d4a9c8 Fix DB path: 3 levels up from app_data_dir, not 2 (files/dev.baldav.app/ → root) 2026-06-01 17:33:59 +03:00
9a4d62dd03 Fix dedup not running: Rust opened wrong DB path on Android
Root cause: app_data_dir() = files/dev.baldav.app/ but tauri-plugin-sql
stores baldav.db in app root /data/user/0/pkg/baldav.db (two levels up).
Rust created empty DB at wrong path, dedup ran on empty DB = no-op.

Fix: added p_root = app_data_dir/../../../baldav.db check.
Also: show seed error on boot label instead of swallowing silently.
2026-06-01 17:21:36 +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
01427941d4 Fix dedup: use execute() not execute_batch(), propagate errors
execute_batch() silently failed on multi-statement dedup SQL.
Split into 4 separate execute() calls with map_err — errors
now propagate to JS and show to user instead of being swallowed.
2026-06-01 14:58:15 +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
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
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
62cdd7740f Fix back navigation after modal fullscreen, fix SQL operator precedence
Navigation:
- Stale {modal:true}/{chartFull:true} history entries after app restart
  caused back gesture to do nothing (no screen to render, no modal open).
  Now global popstate handler skips these stale entries with history.back().

Rust seed import:
- Fixed SQL operator precedence in nutrients_guessed UPDATE:
  WHERE (guessed IS NULL OR guessed = 0) AND name_lower IN (...)
  was previously parsed as: guessed IS NULL OR (guessed = 0 AND ...)
  which matched ALL products with NULL guessed regardless of name.
2026-05-23 18:39:28 +03:00
3e5333cec6 Recipes, nutrients_guessed, seed import fixes, search improvements
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
2026-05-21 00:06:50 +03:00
b26264538b GPS point ordering + GPX export + chart zoom fixes
GPS background recording:
- Batched writes (10s flush) in native service
- Polling lock prevents parallel processing
- Track by timestamp instead of line count
- Recovery idempotent via max DB timestamp
- Seq numbers on live recorded points

GPX export:
- Button in session detail
- SAF file picker on Android (ACTION_CREATE_DOCUMENT)
- GPS config in metadata (accuracy, minSpeed, minDist)

Charts:
- Pinch zoom enabled at creation (Hammer.js registers handlers)
- touch-action: pan-y blocks pinch in normal view (page scrolls)
- Fullscreen: CSS-only expand (no canvas manipulation)
- Downsampled to 500 points for smooth rendering
- Back gesture collapses fullscreen before closing modal

Build: --real flag finds first non-emulator device
2026-05-16 21:21:13 +03:00
daf71333ff GPX export + GPS background recording fixes
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.
2026-05-16 10:49:55 +03:00
f717204ac8 Seed products DB + Settings screen + build fixes
GPS recording overhaul: inline screen (not modal), floating bar
on other screens, manual pause/resume, crash recovery via
localStorage + JSONL file, _processIncomingPoint extracted.

Settings: profile as button, GPS config, permissions, app info.
Products seed DB: 9299 products bundled, imported on first launch.
Build: version/hash in build.rs, signing, install command.
2026-05-15 14:36:22 +03:00