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.
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.
- 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.
- 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).
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).
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.
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.
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.
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.
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.
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.
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.
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.
- .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.
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).
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.
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.
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).
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.
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.
- 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.
- 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.
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
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).
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.
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).
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.
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
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.
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
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.
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 "Земляника садовая")
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.
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.
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.
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
GPS background recording:
- Batched writes (10s flush) in native service
- Polling lock prevents parallel processing
- Track by timestamp instead of line count
- Recovery idempotent via max DB timestamp
- Seq numbers on live recorded points
GPX export:
- Button in session detail
- SAF file picker on Android (ACTION_CREATE_DOCUMENT)
- GPS config in metadata (accuracy, minSpeed, minDist)
Charts:
- Pinch zoom enabled at creation (Hammer.js registers handlers)
- touch-action: pan-y blocks pinch in normal view (page scrolls)
- Fullscreen: CSS-only expand (no canvas manipulation)
- Downsampled to 500 points for smooth rendering
- Back gesture collapses fullscreen before closing modal
Build: --real flag finds first non-emulator device
GPX export: button in session detail, standard GPX 1.1 format.
Background GPS fixes:
- Native service: batched writes (10s flush) instead of per-point
appendText — eliminates race condition with reader
- JS reader: skip corrupt lines, sort by timestamp, track by ts
instead of line count — handles partial writes and reordering
- Recovery: idempotent import using max DB timestamp
- Live recording: seq numbers on every point
- FileObserver watches correct path (app root, not files/dev.baldav.app)
- Removed ACCESS_BACKGROUND_LOCATION (causes throttling)
Nav: "Трекинг" button on GPS screen when recording active.