Commit graph

44 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
2cc16e0ed6 BRouter trekking: cost by surface (dirt < gravel < sand, approved ranking)
Surface was only paved/unpaved binary - gravel, dirt and sand cost the same. Add a
surfacepenalty into costfactor so a dirt track beats a gravel one of the same tracktype
(asphalt 0 < compacted .2 < ground/dirt/earth .4 < cobblestone/sett .6 < gravel .9 <
clay 1.3 < grass/wood 1.5 < sand/mud 5). Canonical lookup values only. Verified on the
home RouteServer: engages on unpaved, doesn't distort paved routes. Untagged-ford
avoidance by estimated_river_class was tried and dropped - it forced 5x detours (flags
riverside ways, not only crossings); untagged fords stay shown (wetcross) + future marks.
2026-10-27 14:06:22 +03:00
f91281a218 SMS: composer-only (ACTION_SENDTO), drop SEND_SMS permission
Reverting the SmsManager direct-send path and its runtime SEND_SMS request - the SMS
feature was meant to use the system SMS composer, which needs no permission. Keep only
the prefilled ACTION_SENDTO composer.
2026-10-26 22:12:31 +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
640429a209 BRouter trekking: apply adventure penalty in costfactor (was a no-op in initialcost)
The ford/grade5/bad-trail penalty sat in the way initialcost and never changed the
route cost (verified on the home RouteServer: avoid and adventure gave identical cost).
Moved it into costfactor next to town/traffic penalties. Verified against a real ford
(47.5387,40.3164): avoid now detours 1.4km->6.2km with fords=0, adventure keeps the
1.4km ford crossing. Node-ford penalty (node initialcost) kept.
2026-10-26 21:38:17 +03:00
85aa87edb0 BRouter trekking: avoid fords / grade5 tracks / bad-visibility trails by default
Routed through icy fords and abandoned descents 3x. Penalise ford nodes & ways
(ford=yes|stepping_stones), tracktype=grade5 and trail_visibility=bad|horrible|no in
the trekking profile, gated by a new %allow_adventure% param (default false = avoid).
Both profile copies (asset + staging) kept in sync. NOTE: profile syntax must be
verified by routing on device; a bad profile makes routeBrouter error out (safe), not
produce bad routes.
2026-10-26 21:08:22 +03:00
c70cc1e4a4 Record screen: LIVE + SMS as square buttons bottom-left; toggle live on the fly
- move the SMS button out of the bottom panel into a left-hand square-button group
  (bottom-left, opposite the right control stack), add a LIVE toggle next to it
- LIVE turns location sharing on/off right from the record screen, even if it was
  never enabled in the profile; warns if the token is still unset
- LiveShare re-reads its config every tick so the toggle takes effect within ~10s
2026-10-26 19:13:34 +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
762221d186 Hydration on by default + diagnostics + reliable alarm; calorie bank; balance chart scale
Hydration reminder: ON by default (native defaults on when hydration.json is
missing; JS writes the default config at startup; the profile toggle is
checked by default) so it works out of the box on a ride instead of needing a
hidden toggle. Adds hydration_log.jsonl diagnostics and swaps the ToneGenerator
for the real alarm ringtone via MediaPlayer on STREAM_ALARM, with the alarm
volume forced to max for the alert, so it is actually audible.

Calorie bank: cumulative sum of daily balance over the window (+kg) in the day
summary, answering "how much to eat up/under". Balance chart (weight vs balance)
y-axis now floats symmetrically about 0 (±500 floor, no fixed ceiling) so one
hard day no longer flattens the line.
2026-10-26 15:05:31 +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
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
9bc71b19c9 BleHrService: auto-reconnect a dropped strap (bounded, never for an absent one)
On an unexpected disconnect of an established connection (connectedOnce, i.e.
services were discovered), reconnect via the scan path up to 3 times, 8s apart,
resetting the budget on each successful reconnect. The scan gives up (stopSelf)
if the strap isn't found, so we never hammer a band that wasn't present at start
or wasn't connected by hand. A user stop, Bluetooth off, or onDestroy sets a
stopping flag that suppresses reconnect.
2026-10-20 23:25:06 +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
13c61f506a BleHrService: survive Bluetooth being turned off (no native crash)
Single Handler for all postDelayed work, cancelled on teardown; a BT
ACTION_STATE_CHANGED receiver that cleanly stops the service on STATE_OFF;
try/catch around every native scan/connect/GATT call. Prevents the libc
destroyed-mutex abort when the adapter is disabled mid-connection.
2026-10-19 13:43:55 +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
62434d6155 HR strap battery + low-charge voice/beep alerts at 10% and 5%
- BleHrService reads the standard Battery service (0x180F/0x2A19) 2.5s after
  the HR subscribe and every 60s; writes 'bat' into ble_hr_data.json.
- HrReading gains battery: Option<u8> (serde alias 'bat').
- gps.js: the HR poll captures reading.battery; _checkBatteryAlert speaks
  (speechSynthesis ru-RU) and beeps once at 10% and 5% (beep is the
  guaranteed cue where TTS is absent), resetting above 15%.
- Workout screen shows the strap charge in its status line.
2026-10-17 00:58:31 +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
492c54990f DB restore: restart via AlarmManager (startActivity+exit raced, app just closed) 2026-09-21 13:44:54 +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
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
99cd4c8d30 Scrub location/identity: home city name and external mirror IP → placeholders
Remove the home-city name from docs/comments/UI strings and the external
mirror IP (WAN) from code, docs and the Android cleartext allowlist, replacing
the IP with mirror.invalid (set the real host locally, uncommitted). LAN mirror
and routing keep working; the WAN fallback needs the real host set. Generic
big-city map presets are kept (not identifying).
2026-09-10 00:55:45 +03:00
cd98078682 BRouter: gap-fill along roads + release hardening
Stage 4 — gap fill: GPS.fillGaps(points) routes each GPS dropout (dt>gap
threshold, chord>60m) with the shortest road path and splices the routed points
in (flagged estimated), falling back to the chord when the road is >3x the chord
(graph detoured). A 🧩 button on the track detail draws the road-filled path as
a cyan dashed overlay — non-destructive (stats recompute over filled points is a
follow-up). Also fixes doBrouter: parseRouteGPX returns {name,points}, not an
array (planning would have kept the straight line).

Release hardening: network_security_config.xml permits cleartext only to the
mirror/RouteServer hosts (base off, debug-overrides on) — wired via
android:networkSecurityConfig; proguard keeps btools.** so release minification
doesn't strip the embedded engine.
2026-09-09 17:43:56 +03:00
b40775c9d0 BRouter: download segments from own mirror (LAN->WAN->upstream)
Per the user's decision, BalDav pulls .rd5 tiles from their own mirror instead
of brouter.de directly. downloadTile now tries, in order: LAN
http://192.168.1.1:88/brouter/segments4, WAN http://mirror.invalid:8088/... (via
the router port-forward), then brouter.de as a last resort; a base that 4xx's
or fails is skipped. The mirror is the Keenetic's existing uhttpd (:88, docroot
/opt/share/www) serving /opt/share/www/brouter/ off USB — always on.

Verified on device: the phone downloads a tile from the LAN mirror in <2s and
routes on it. Routing stays local (embedded engine); the PC RouteServer +
systemd unit are the user's separate remote endpoint, not used by BalDav.

Debug builds allow cleartext; release needs a network-security-config for the
mirror hosts. WAN port-forwards on the router are outward-facing (added
separately with confirmation).
2026-09-09 11:48:50 +03:00
d807e4ec3b BRouter Stage 2: resilient segment downloader (Range resume), verified on device
In-app download initially stalled at 7922 bytes — not the network: the phone
reaches brouter.de fully (confirmed via Termux). BalDav was not in the device's
split tunnel, so its traffic took the wrong path; fixed device-side. Over the
now-working but bursty link the fixed 30s readTimeout tripped, so the downloader
is rewritten to resume: HTTP Range (append to the .tmp, 206=resume / 200=restart),
readTimeout 60s, up to 30 retries.

Verified on device: E40_N45 (14.1 MB) downloaded by the app (done ~55s), a
cross-tile trekking route on the app-downloaded tile (44.8 km / 316 m, 1.66s)
matches the PC-pushed tile, and listTiles returns both. Remaining Stage 2 work:
the tile-selection UI.
2026-09-09 11:10:24 +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
bc63b34cb9 BRouter Stage 1: verified on device; ship v11 lookups/profiles to match segments
End-to-end offline routing confirmed on the phone: shortest ~5.4 km in Rostov
(3.2s cold), trekking cross-tile Rostov->region 44.8 km / 316 m ascend
(1.2s), 617 ms on the apk-shipped data. JS -> Rust brouter_route -> Kotlin
engine -> GPX works through the whole chain.

The 1.6.3 distribution ships lookups.dat v10, but the live brouter.de/segments4
tiles are v11 ("lookup version mismatch"). Replaced the vendored lookups.dat +
trekking/shortest profiles with the v11 set from abrensch/brouter master
(misc/profiles2); the 1.6.3 engine accepts them. Profiles and lookups MUST match
the segments' version.
2026-09-09 01:07:24 +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
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
296d23a008 Android: fix OS back gesture for SPA modals/screens (drive JS history.back)
WryActivity's generated back handler calls WebView.goBack(), which does NOT
dispatch popstate for same-document history.pushState() entries — so the OS back
gesture never reached the app's popstate handler and couldn't dismiss modals
(new product, add-to-eaten, ЧЗ scanner) or navigate screens; only the on-screen
back arrow worked. WryActivity.kt is auto-regenerated each build, so override in
the app-owned MainActivity instead: set handleBackNavigation=false and register
our own OnBackPressedCallback that runs window.history.back() via evaluateJavascript,
which fires popstate. Verified on-device: swipe-back now closes modals and the ЧЗ
scanner. (adb `input keyevent BACK` does NOT go through OnBackPressedDispatcher on
Android 13+, so it can't be used to test this — only the real gesture does.)
2026-08-27 23:12:34 +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
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
d4ef9a1f9f Fix: background GPS points lost on OS service restart
Regression from per-session points file change (1feb2ef): the service
returns START_STICKY, so when the OS kills it during screen-off, it
restarts with a null intent and onStartCommand minted a NEW points file,
repointing gps_live_points.jsonl and orphaning accumulated background
points. Evidence: many tiny orphaned gps_points_*.jsonl fragments.

Fix: onStartCommand creates a new session file only on a genuine start
from JS (intent with extras). A START_STICKY restart (null intent) keeps
writing to the existing session file from the pointer — one file per
session until Stop.

Verified on device: am kill (= START_STICKY restart) no longer creates
a new file, pointer unchanged, trail resumes on return. Background
writing confirmed: 49 points in 70s with screen off (Dozing) at 1Hz.
2026-07-02 22:29:29 +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
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
4d97ba607b Always copy seed DB from assets, VACUUM seed to reclaim space
Size comparison was unreliable — SQLite reuses free pages internally,
so adding products didn't change file size. New seed never copied.
Now: always overwrite on every launch (~8MB, <1 sec).
VACUUM reduced seed from 8.5MB to 8.2MB.
2026-05-29 15:38:10 +03:00
d7527bfd45 Fix back-gesture app exit, screen wake lock, multi-segment tracks
Navigation:
- _modalPopHandled flag prevents global popstate handler from firing
  on the same event that modal popHandler already processed
- Fixes: back from fullscreen map/chart no longer exits the app

Screen wake lock:
- JS: navigator.wakeLock.request("screen") on recording screen
- Kotlin: FLAG_KEEP_SCREEN_ON on GPS start, clearFlags on stop
- Screen stays on while recording is visible in foreground
- Released when navigating away from recording screen

Multi-segment tracks:
- GPS gaps > gapThresholdSec (default 30, configurable) split track
  into separate polyline segments on the map
- Each segment has own green start / red end markers
- Shows clearly where GPS signal was lost vs continuous tracking

Settings:
- "Макс. пауза GPS (сек)" — gap threshold for segment splitting
- Physics constants (aero drag, air density, pedal efficiency)
  moved from hardcoded to GPS.config + settings UI
2026-05-27 15:29:22 +03:00
30b0ac49f3 Max speed filter in native service, fix recalculate bugs
Native service (Kotlin):
- maxSpeedMs filter: points with speed > max are not recorded
- Config passed as 4th param: start:minSpeed:maxAcc:minDist:maxSpeed
- Default 60 km/h (16.67 m/s)

Settings:
- "Макс. скорость (км/ч)" field in GPS config section
- Saved to localStorage with other GPS settings

Recalculate fixes:
- Used cleaned sorted array instead of original points (was computing
  stats on dirty data with glitches -> hundreds of thousands of kcal)
- parseFloat on weight from profile (string "88" + 12 = "8812" bug)
- seq re-assignment only runs when points were actually deleted
- Glitch points deleted from DB on recalculate (not just skipped)
2026-05-25 15:20:24 +03:00
c5bf6da79e Android: always refresh seed DB from assets when size differs
Old logic: copy only if file doesn't exist. App updates with
newer seed DB were ignored because the old file was already there.
Now compares file size with asset size and overwrites on mismatch.
2026-05-21 01:10:15 +03:00