Commit graph

181 commits

Author SHA1 Message Date
eb430ffb78 ui: bulk-delete tracks, HR/VR record + detail layout
- Tracks list: the long-press multi-select (used for merge) now also offers Удалить -
  bulk delete of the selected tracks (1+; merge still needs 2+), same selection flow.
- HR/VR record screen: heart rate fills the main area (like the map on a GPS ride),
  Подключить/Стоп moved into a bottom bar, consistent with the GPS screen.
- HR/VR workout detail: 'Удалить зарядку' pinned to the bottom (was floating in the
  scroll flow and stretched).
2026-10-30 23:07:53 +03:00
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
06ffab66a1 trackme share: include POI near the ride (bbox of route+track), shown on the viewer 2026-10-27 14:18:00 +03:00
aae9c55e1a trackme share: round coords to 5 decimals (~1m) to shrink the payload 2026-10-27 01:10:15 +03:00
8a236bae3d Record screen: 'Отд' button — hand route/roads/track to the live viewer
shareRideContext() gathers the active route, ridden roads and track-so-far and POSTs
them to the server's RAM relay (/api/trackme/share); the viewer pulls them into their
own localStorage. One-shot — press again to resend after the track grows.
2026-10-27 00:10:59 +03:00
61cfe7c0ee BRouter danger: detect untagged fords via BRouter river-class heuristic
OSM doesn't tag many real fords (ford=yes). BRouter still flags them with a high
estimated_river_class on the crossing way. Flag a track/path/footway with
estimated_river_class>=5 and no bridge/tunnel as 'wetcross' (возм. брод, light cyan,
shown as a guess vs the solid-blue confirmed ford). Verified against a real untagged
ford on the Rodionovo route (47.4926,39.8610): river_class=5 on a track, now detected.
2026-10-26 22:07:41 +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
ac37f36a06 Reroute (↪): only detour when a nogo is actually ahead on the route
The button built a detour unconditionally and picked the target from the globally
nearest route point + 1500m, which on loops/off-route jumped to the start ('first
waypoint'). Now: find current progress on the route, use the compass bearing to pick
travel direction, and only reroute when a nogo sits on the route ahead within a
lookahead window - else toast 'впереди затыка нет'. Target is the first point clear
of nogos past the blockage.
2026-10-26 20:57:24 +03:00
80ddff9b72 Route grade: compute over a ~30m distance window, not adjacent points
DEM elevations quantise to 30m cells; dense points inside a cell share one value and
the whole rise lands on the single boundary segment -> a false steep spike in slope
colouring. Compute each segment's grade as a central difference over a >=30m window
(config gradeWindowM) so a cell step spreads across the cell and the colour reflects
the real grade. maxGrade uses the windowed value too.
2026-10-26 20:55:11 +03:00
27c60a460a Route slope coloring: colour descents by a cool fan (green->cyan->navy)
Slope was coloured by |grade| - up and down looked identical. Colour by signed
grade in travel-direction order: uphill keeps the warm fan (green->yellow->red->
brown->black), downhill drops through a cool fan (green->cyan->blue->navy). Legend
shows both strips.
2026-10-26 20:55:11 +03:00
a68c298946 SMS: send only the Yandex Maps link so it fits in one message
Drop the 'Я тут: lat,lon' prefix - the Cyrillic forced UCS-2 (70-char SMS) and the
coords were redundant with the pin. The bare ASCII link is GSM-7 and fits in one SMS.
2026-10-26 19:28:08 +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
738c4e81a1 Profile: render settings checkboxes as toggle switches
The .form-group input rule (width:100% + appearance:none) stretched every
checkbox into an empty full-width box. Add a .toggle class styling a checkbox
as a sliding on/off switch and apply it to the TEF, hydration and live-share
toggles in the profile modal.
2026-10-26 18:25:07 +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
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
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
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
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
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
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
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
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
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
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
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