Commit graph

250 commits

Author SHA1 Message Date
4637846839 Roads overlay: one polyline per segment + viewport culling (fix jank)
The per-pass thickness split fragmented each segment into a run per
weight change, turning a ~1.4k-segment network into ~84k polylines and
janking the map. Draw one polyline per segment at its median-pass width
instead (verified on device: 1430 vs 84083). Add buildRoadSegments
(segment + bbox) and cullRoadsInto (draw only segments meeting the padded
view, redraw on moveend/zoomend); the recording overlay now renders on a
canvas pane instead of SVG, and showRoadsMap culls too. Thickness is now
per-segment (median) rather than per-point — a deliberate simplification.
2026-10-23 01:11:15 +03:00
c7dd588f37 TEF in daily balance + separate HR-only "vr" (hands/VR) workout type
TEF (thermic effect of food): optional profile toggle, 10% of intake,
introduced so maintenance is preserved - base = BMR*actLevel*(1-tef),
then +tef*intake. On a deficit day expenditure is slightly lower (eat
less, digest less), which keeps the existing calibration rather than
inflating the deficit. Day summary shows "расход: база + переваривание
+ активность"; the 7-day trend uses it too.

New activity_type "vr" (Руки/VR): HR-only like "workout" (no GPS, kcal
from HR), tracked separately. GPS.isHrOnly() replaces the hardcoded
"workout" checks so both kinds share the no-GPS record/detail path.
2026-10-22 00:51:31 +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
1cb6efbee3 Track detail: reference calibrated-HR kcal figure (Keytel x personal factor)
HR methods over-read vs physics (ride) and weight (sedentary day); a rough
single factor ~0.65 reconciles both. Add config.hrCalibFactor (0.65) and show a
'ЧСС откалибр. (грубо)' figure next to the Keytel one in the calories block,
clearly labelled as a rough reference. Activity balance still uses physics.
2026-10-21 11:22:46 +03:00
435a0dd27c Tracks: un-merge a merged session back into its original rides (UI button)
A merged session keeps orig_session_id on every point, so split it back:
unmergeGpsSession recreates the original session rows by that id, retags points,
and sets each piece's date/times from its own points. A 'Разделить склейку (N)'
button shows in track detail only when the session is a merge; it un-merges and
recomputes each piece.
2026-10-20 23:49:17 +03:00
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
85d5e47972 Bump version (build.sh output) 2026-10-19 23:16:39 +03:00
10ff635192 Session log and plan: builds for all OS uploaded to the router (build 506) 2026-10-19 23:16:33 +03:00
f2d39459e6 Builds: any mix of targets and flags, Windows icon, router upload per file
icon.ico is required by tauri-build for the Windows resource; made from the
existing icon.png. Docs describe build.sh (gitignored): targets combine freely
under one build number, -da makes only the APK debug (the phone runs
debug-signed builds), and BUILD_NUMBER on the router is written only with the
APK, since the phone's OTA reads it and would otherwise offer the old APK.
2026-10-19 23:05:54 +03:00
1d133d0067 Docs: desktop builds for Linux/Windows/macOS and router upload via build.sh
build.sh itself is gitignored; this records the new targets (linux, windows,
macos, release, upload), the router file names and the osxcross location so
the flow is reproducible.
2026-10-19 22:47:44 +03:00
3f092cced0 Tiles: heal blank tiles online (periodic OSM-reachability check + backfill)
The on-demand loader never retries a tile whose fetch failed, so holes from a
bad signal stayed blank even after the network recovered. Add a healer: every
30s, when OSM is reachable, re-fetch MISSING tiles (current zoom) for the
visible area and along the ridden route, cache them, and redraw. Gentle to OSM
(one-tile probe, 300ms throttle, <=40 fetches/cycle); respects offline mode.
Wired on record/detail/compare/roads maps, stopped on screen change.
2026-10-19 20:07:05 +03:00
d4dda3da0c Dark map: give the roads overlay the same invert/hue/brightness filter as tiles
Road colours were tuned for light tiles and blended into the inverted dark
tiles. Filter the roads pane identically: add the CSS rule for
.leaflet-roadsPane (record screen roads already use that pane), and on the
roads screen draw the polylines through a canvas renderer bound to a new
roadsPane so the same rule applies there too.
2026-10-19 19:26:20 +03:00
802b2cc6e9 Bump version (build.sh output) 2026-10-19 14:12:04 +03:00
eeca9a7d94 HR connect: one BT-enable prompt per start, only on user action
setHrOn gains an 'interactive' flag; it pops the OS enable-Bluetooth dialog
only on a user tap (record-screen toggle). Auto-calls at recording start no
longer re-request enable after the single ensureSystemReady prompt, so a
decline is not immediately followed by the system dialog and is not re-asked
until the next start.
2026-10-19 13:43:55 +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
1df24000a3 Track detail: HR chart as its own collapsible section + 'what if by HR' kcal comparison
- Speed/Elevation/Пульс are now collapsible <details> sections; the HR chart
  moved out of the Elevation block into its own 'Пульс' section (avg/max + chart),
  shown when the track has HR.
- Калории now also shows the Keytel HR-based kcal as a COMPARISON when HR is
  present ('По ЧСС (сравнение)'). The activity balance keeps the physics/MET
  number unchanged - this is only to see the difference.
2026-10-17 14:53:21 +03:00
87a7fccfcc Record screen: remove the status line + its black bar, tighten button padding, move ⛔/↪ off the zoom controls
- Drop the status line (and any text it showed) from the record screen; keep
  a no-op statusEl sink so existing writes don't crash. Removes the empty
  black bar above the buttons. Tighter .rec-bottom padding.
- The nogo (⛔) and reroute (↪) buttons had no position class, so they piled
  at the map top-left over the zoom controls. Give them stack slots.
2026-10-17 14:43:57 +03:00
69b36be591 Record buttons equal width (border-box + uniform padding); HR strap charge as a background fill on the HR toggle
- Стоп was narrower: buttons had different horizontal padding and weren't
  border-box, so flex-basis:0 didn't equalize them. Force box-sizing + one
  padding on all three .rec-bottom buttons.
- HR strap battery now paints the HR toggle's background bottom-up 0-100%
  (--bat), coloured green/amber/red by level (setHrBatteryFill), instead of
  a number; cleared when HR is toggled off.
2026-10-17 14:35:54 +03:00
e88c2756f7 Record screen: big digits in the top strip (match bottom), drop the black status-bar backdrop band and the extra control offset
Top strip time/temp/clock now use the same 27px bold as the bottom stats. The
old .rec-map-wrap::before status-bar backdrop (and the leaflet-top safe-area
margin) are gone - the top strip already backs the status bar, so that strip
showed as a black band between the bar and the map.
2026-10-17 14:30:42 +03:00
dffadf3cd8 Record screen: regroup stats (top strip time/temp/clock, bottom HR/dist/speed/alt), equal-width buttons; reset stale HR per session
1) Layout: a top strip shows training time | temperature | clock ('external'
   things); the bottom panel row is now HR, distance, speed, altitude (bigger);
   the separate HR bar is gone; the three controls (HR toggle, pause, stop)
   share the width equally instead of clustering right.
2) Clear currentHr/currentBleData at recording start (+ ble_clear_hr) so a
   stale bpm from the previous session no longer hangs on the display or gets
   written onto this session's points; show -- until a fresh reading.
2026-10-17 12:29:14 +03:00
0a4ffcb49d HR toggle: white icon+bpm on solid red when active (contrast)
Active state was red text on faint-red; use white on solid red.
2026-10-17 10:59:31 +03:00
553a4fcf80 Record screen: show the live HR number on the HR toggle button
The HR toggle (ex-wind button) showed only the heart+pulse icon. Now the
current bpm renders on it (onHrUpdate writes #rec-hr-num); it clears when HR
is toggled off, leaving just the grey icon.
2026-10-17 10:57:08 +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
f7ba059628 HR: auto-connect at recording start if a strap is paired; HR on/off toggle replaces the wind button
Earlier I over-corrected: 'don't scan when the strap is absent' became
'never auto-connect'. Restore conditional auto: at recording start (ride or
workout) connect the paired strap and poll; absent strap is safe now
(BleHrService scans briefly and stops - no drain). No paired strap -> HR off.

- gps.js: GPS.setHrOn(on) - connect+poll / disconnect+stop; start auto-ons it.
- Record screen: the inconvenient 'Ветер' button is gone; in its place an
  HR on/off toggle (square, heart+pulse icon; grey off, red on) -> setHrOn.

(Also corrected a stale assumption: the phone DOES have Google services.)
2026-10-17 01:45:02 +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
f704498d11 Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.

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

Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
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
93908e3365 Gap-fill: bridge dropouts over your own ridden roads first, public BRouter only as fallback
fillGaps used the public BRouter shortest only, so a dropout where you rode
a trail/track absent from OSM was left unbridged (detour >2x or noroute),
even though your own track went straight through. Add opts.routeFn: the
detail view now tries rideRoute (the ride-graph over your tracks) first and
falls back to public BRouter only where you've never ridden. A ridden bridge
is trusted as-is (no detour-too-long rejection); public routing keeps that
guard. Clearer 'not bridged' message split by reason (public detour vs no
road data).
2026-09-30 00:23:28 +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
fd178ad97d Track detail: await history.go popstates before re-render after gap-fill revert/save
reopenDetail fired history.go(-count) then re-rendered synchronously, so the
async map/chart build raced the popstate unwind: the pending popstates
desynced the back stack and bumped the detail render token, and the render
bailed at the token check - map and charts came up blank until the next
event repainted them. Make reopenDetail async and await the popstate(s)
before showSessionDetail, the same resolve-on-popstate pattern as closeThen
(no setTimeout/setInterval).
2026-09-29 16:03:52 +03:00
06cc77fdec Maps: trim tile retention, simplify big tracks by zoom, thin POIs when zoomed out
Canvas (prev commit) made the big-track map self-recover but the Chromium
compositor still hit 'tile memory limits exceeded' ~46x and 10k segments
still janked. Three cheap levers:
- tilecache: keepBuffer 2->1 and updateWhenZooming:false - fewer retained
  off-screen tiles in the compositor's raster budget on a high-DPI screen.
- track/roads polylines: smoothFactor 2.5 (Douglas-Peucker in pixels) - a
  10k-point track draws far fewer segments zoomed out, full detail zoomed in.
- POI: cap markers by zoom (z>=16:500, 15:200, 14:80, 13:30) instead of a
  flat 500 - a handful near min-zoom, everything only when zoomed in close.
2026-09-29 15:49:02 +03:00
834204a58a Maps: canvas renderer for heavy-vector maps (SVG blew Chromium tile memory, tiles vanished)
Viewing a big track (10865-pt, 60km) and zooming/panning threw
'cc/tiles/tile_manager tile memory limits exceeded, some content may not
draw' dozens of times - Chromium's compositor tile-memory cap. The track
is drawn by Leaflet's default SVG renderer, multi-layered (casing/core +
per-1%-slope segments), so the giant SVG layer exceeds the raster budget
and the map tiles stop drawing until a force-stop resets the compositor.

Set preferCanvas:true on the heavy-vector view maps - showSessionDetail,
showRoadsMap (many road polylines, also reported laggy), showComparisonView,
showRouteDetail. Canvas draws into one viewport-bounded canvas instead of
thousands of SVG paths. Left the recording map on SVG (rotate:true;
leaflet-rotate barely supports Canvas) and the route editor (line clicks).
2026-09-29 13:20:53 +03:00
74f3ec4d6e Startup: batch the seq repair (per-point loop made cold start ~15s, every boot)
The startup seq repair (runMigrations) assigned seq to NULL-seq sessions
one UPDATE per point. Merge/split leave seq=NULL by design, so recent
merged tracks (10865 + 3614 points) meant ~15000 sequential IPC writes on
every launch - and for the big sessions the writes did not persist before
the next launch re-ran the whole thing. That was the slow start.

Replace it with the batched CASE re-sequence (chunks of 300, like
saveGapFill/saveSmoothedTrack): ~50 UPDATEs instead of ~15000. Verified on
device: repair of all 3 sessions dropped from ~15s to ~1.1s, seq now
persists so it is one-time (boot 2 does no repair, migrations 357ms), 0
NULL seq left, seq matches ts order (0 inversions).
2026-09-29 12:37:15 +03:00
2d91c06290 gitignore build.sh (holds deploy secret) and BUILD_NUMBER (local state) 2026-09-29 12:19:15 +03:00
dbb6c2ea9f Build: auto-clean stale Android bundles per build + clean target
Each android build now removes the regenerable AAB bundles
(intermediary_bundle, outputs/bundle) and gen APK copies once the final
APK is in binaries/; a manual 'clean' target does the same on demand.
Frees ~0.5G/cycle without touching the Rust incremental cache or
binaries/. (build.sh stays untracked - it holds a deploy secret; this
documents the change.)
2026-09-27 00:04:52 +03:00
0123d3562b Bump version (build.sh output) 2026-09-27 00:04:52 +03:00
d6fe47a924 Recording map: follow toggle to free-pan while riding
New ⊕ button on the recording screen toggles map auto-follow. When off,
the map stays put so you can look around when unsure where you are (off
the track, or the track leads where there is no road); new GPS points no
longer recentre it. Tapping it back on recentres on the current position
and resumes following. Default on per screen entry (not persisted).

Guards followMode around panTo/setView in onPoint/onBatchUpdate/
onStateChange. Independent of the compass freeze/rotate controls.
2026-09-26 23:44:18 +03:00
5da02c598a Docs: versions table to build 474 (deployed to router + VPS) 2026-09-22 22:11:38 +03:00
749064ce59 Bump version (build.sh output) 2026-09-22 21:09:13 +03:00
5f58eabcc5 Gap-fill: re-sequence the session by ts on save (filled points landed at the front on merged/split tracks with NULL seq) 2026-09-22 20:58:06 +03:00
514ff4edb3 Docs: versions table to build 473 (deployed to router + VPS) 2026-09-22 11:49:48 +03:00
862b1ffc96 Bump version (build.sh output) 2026-09-22 11:32:19 +03:00
5d24f8c3a3 Keep scroll position across same-screen re-renders (day nav, add food) instead of jumping to top 2026-09-22 11:31:37 +03:00
bb9f03131d Bump version (build.sh output) 2026-09-22 11:19:37 +03:00
dff9342c0d Balance/trend/charts follow the selected day; kcal weight is per-ride (stored) and settings weight auto-syncs to the latest weigh-in 2026-09-22 11:12:24 +03:00
6534d6601c Docs: bump versions table to build 471 (deployed to router + VPS) 2026-09-21 20:40:49 +03:00
3ba8751664 Bump version (build.sh output) 2026-09-21 20:31:34 +03:00
685151b76b Repeat analysis: sweep the hot-cell threshold and keep the most passes (out-and-back reps were lost when a twice-ridden approach inflated the reference) 2026-09-21 20:30:38 +03:00