Commit graph

55 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
15a2881c6d Split a track at a point; block merging tracks with different bikes 2026-09-21 12:36:03 +03:00
ea462ad9c4 Repeat analysis: detect a segment ridden twice (minPasses 3 -> 2); count no longer blocks 2-rep sessions 2026-09-21 11:27:24 +03:00
cfadad4690 Merge rides: long-press to multi-select tracks and combine them into one (reversible via orig_session_id) 2026-09-18 00:08:25 +03:00
2ffaf1581d Repeat analysis: project onto the reference line (lateral drift no longer truncates a rep); pass time counts moving seconds only 2026-09-17 23:46:32 +03:00
7717a73129 Repeat/interval analysis: auto-find repeated segments in one ride, table + last/first verdict 2026-09-17 22:11:10 +03:00
6dfe262801 Docs: ridden-roads router, share field selection, segments accordion 2026-09-14 13:44:09 +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
e45c92f09a Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- Editor shows the route with casing + slope/elevation colouring like the detail
  view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
  locate controls added. Root cause of the plain thin line: ele was dropped when
  building leg geometry — now kept from BRouter, so casing/slope work here and in
  showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
  pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
  alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
  big detours (>2x chord) and no-gain fills (road ~ straight).
2026-09-11 01:06:06 +03:00
cfe0212bbc BRouter UX: gap-fill preview+reversible save; per-segment route planner
Gap-fill (track detail): GPS.fillGaps now returns {points,gaps} with per-gap
status; the 🧩 control previews ONLY the filled dropouts (cyan casing; unfilled
= red dashed) with a Save/Cancel panel. Save is a reversible in-place patch
(estimated points added, original untouched; DB.saveGapFill/revertGapFill +
estimated/gap_filled columns), then recalculateSession. Revert removes them.

Route planner: renamed menu 'Нарисовать маршрут' → 'Нарисовать тренировку'
(it makes a workout session, not a route); routes empty-state now points at the
on-phone planner. showRouteEditor rewritten per-leg: each leg road (BRouter) or
straight independently, road by default, tap a leg to toggle it, long-press to
insert, silent straight fallback where no road; the global button sets all legs.
2026-09-10 15:26:05 +03:00
1546e947e7 BRouter: fix routed-line re-route loop; verify planning + gap-fill on desktop
doBrouter() ended with changed(), which nulled routeGeom and re-queued
doBrouter — an endless re-route loop where the road line was never drawn
(editor kept the straight line, distance unchanged). Draw directly instead
(updatePolyline/updateStats).

Verified end-to-end in the UI on a virtual display (Xvfb) against the
RouteServer: editor 'По дорогам' turns a 1.69km straight line into a 2.25km
road-following route; the gap-fill '🧩' overlay draws a shortest-road cyan
dashed line across a track's GPS dropouts. Docs (README/PROJECT_MAP/plan/log)
updated to match; router.js/poi.js now listed in PROJECT_MAP.
2026-09-09 18:18:35 +03:00
542bb5c376 rec screen: rename wind button 'Ветер в лицо' -> 'Ветер'
Shorter label on the recording screen; docs (README, PROJECT_MAP) updated to match.
2026-09-09 17:04:04 +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
1b10aa67de Wind: "headwind in the face" label button on the recording screen
Variant 5 from the findings doc, as GROUND TRUTH for validating the
from-track wind estimate — it does NOT feed kcal/power. Chosen scope
(per user): a toggle button now, binary "in the face" only, voice later.

- rec screen button "Ветер в лицо": tap opens a headwind interval, tap
  closes it; the open state is highlighted and survives a screen re-render
- GPS.toggleWindMark()/_closeOpenWindMark(); recording.windMarks =
  [{startTs,endTs}] ms, persisted to localStorage, written to the DB on
  finish and restored on crash recovery (a dangling interval auto-closes)
- gps_sessions.wind_marks (JSON) + DB.setSessionWindMarks (separate from
  finishGpsSession so recompute paths preserve marks); parsed in getGpsSession
- rec-bottom wraps now so the third button never overflows a narrow screen

Analysis (marks vs estimate) stays in tools/track once enough km accumulate.
2026-09-07 17:10:16 +03:00
dd9ec1bda7 GPS model: accel-based Crr scale, auto walk-the-bike split; document climb/elevation limits
Acted on docs/gps-model-findings.md (independent analysis of 156 rides).

Implemented:
- crrFromAccel extended past the old 0.008 ceiling: 7-11 -> 0.008,
  11-15 -> 0.012 (packed dirt), >15 -> 0.020 (grass/rut/gravel). Rough
  surfaces were undercounted ~3x. Applied to session median and per-point Crr.
- Auto walk-the-bike detection (single-speed only, gear_count 1/unset):
  a normal-fix segment slower than the 30-rpm-cadence speed is the rider
  pushing, not riding. Excluded from ride distance/speed/elevation (elevRef
  reset so a pushed climb doesn't merge with the ridden one); effort counted
  as walking, not bike physics. Every sub-threshold point excluded regardless
  of duration; only runs >=30s reported as walked (walkBikeSec/walkBikeMeters).
  gearCount threaded through every computeStats/finish caller.

Documented as known limitations (left as-is by decision, need calibration/DEM):
- kcal max(physics, MET) never sums the biological overhead, so climbs are
  slightly undercounted (comment + README + PROJECT_MAP).
- absolute elevation gain is bounded by the raw GPS altimeter (tens of
  percent; needs a terrain model / wired DEM preload).
2026-09-07 15:50:19 +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
872e20a5b0 tools: steep_segments.js — extract steep bike-track sections to GPX + brouter
Two-pass extractor for the steepest sections of recorded bike tracks:
- pulls gps_points/gps_sessions from baldav.db (adb run-as), bike only;
- refines elevation online (--gpxz Copernicus GLO-30 with the user's key, deduped
  to ~33m cells + cached; falls back to open-elevation / on-device altitude_srtm_m);
- distance-window smoothing, grade over a configurable base (default 120m) with a
  GPS-gap guard, keeps the steepest base-window core per section (so a real steep
  core is not averaged away by a long gentle stretch), spatial de-dup of repeats;
- outputs steep_segments.gpx (named by grade/length/drop) + a brouter lonlats string.
Verified against two ground-truth spots the rider confirmed; open-elevation/SRTM30
flatten short steep features that GLO-30 resolves. Key + data stay gitignored.
2026-08-29 10:05:39 +03:00
a2a2fd4a00 docs: ЧЗ scanner — offline ZXing decoder (no GMS), ЧЗ badge, session auto-push 2026-08-27 04:37:24 +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
fe20a4d590 Recording puck: point direction triangle UP (no jitter)
The direction triangle jumped around in GPS mode: rotation was set to the geo
heading with rotateWithView adding map._bearing, and the two desynced (they
should cancel in course-up but updated at different times). Since the map already
rotates the active heading to the top, travel direction is always screen-up —
so just point the triangle up (no rotateWithView, no per-frame rotation), shown
whenever a heading is known. Compass mode still to be tested.
2026-08-25 20:48:27 +03:00
f7b45d98f0 Product catalog cleanup (no-badge unused) + order-independent search
- searchProducts: order-independent token AND (every whitespace word must appear
  in name, any order), ranked full-substring > prefix > length. Fixes needing to
  type words in the name's order.
- Cleanup of unlabeled junk (health-diet/ashan/fast-food, null source): keep-list
  = magnit,5ka,ion,kdv,manual,recipe_import,gridnev. DB.cleanupUnlabeledProducts
  deletes non-keep AND unused (food_log/recipe_ingredients protected); Settings
  button 'Очистить продукты без магазина' with a counted appConfirm.
- Seed products_seed.db cleaned: 24443 -> 13008 (non-keep removed), seed_meta
  version 10 -> 11, VACUUM (13.5MB -> 8.9MB). So future imports don't re-add junk.
  On-device 11207 unused non-keep will be removed via the button (24 eaten stay).
2026-08-25 18:23:51 +03:00
493c9b92df Recording map: navigation puck (position+direction), z-order panes, compass-only mini-compass
- One direction indicator instead of two geo-arrows: an SVG divIcon L.marker
  (rotateWithView) — a track-colored circle (20% darker) with white + 1px black
  rings, plus a triangle pointing the ACTIVE source heading (compass in compass
  mode, GPS course in GPS mode). Rotation = geo heading; rotateWithView keeps it
  correct as the map turns. Replaces posMarker + compass/course arrows.
- Z-order via panes: roads -> roadsPane(240), route routePane(250), track
  overlay(400), puck markerPane — tiles<roads<route<track<puck (track no longer
  hidden under roads).
- Mini-compass shown only in compass mode; tap-to-calibrate kept.
- darkenHex helper; .rec-puck css (no default div-icon box).
2026-08-24 23:06:26 +03:00
b5275da61e docs: route-editor mid-insert, track color setting, app dialogs, orient-dbg debug-gate 2026-08-22 22:18:44 +03:00
c58ed322ec docs: WebView CDP debugging + build.sh usage notes 2026-08-16 21:16:55 +03:00
bdec4b7bc0 Recompute tracks with an accuracy threshold (non-destructive)
Exclude real GPS fixes coarser than the "Макс. погрешность GPS" setting
(minAccuracyM) from a track's stats and its drawn line. Non-destructive: the
points stay in the DB, so raising the threshold brings them back.

- GPS.filterByAccuracy(points, threshold?): drops points with a measured
  accuracy above the threshold; always keeps points without accuracy (imported
  GPX/FTB, older recordings) and estimated dead-reckoning fills (sentinel >=999).
- Wired into GPS.computeStats (top) so every recompute path funnels through it —
  the "Пересчитать" button, "Пересчитать всё", and the post-de-jitter recompute.
  Also into GPS.filterNoise so the detail-screen line matches the stats.
- Removes mild jitter detours that the spike filter misses (verified: 342 -> 334m
  when a poor-accuracy nudge is dropped). Lower the threshold + Пересчитать to
  apply; raise it to undo.
2026-08-16 20:31:12 +03:00
fd6cf62fc0 Route editor + 1% slope bands + debug-only diagnostics + rotate-map route fix
- Route editor (showRouteEditor): create ("Новый маршрут") or edit an existing
  route's geometry in-app — tap to add, drag a point to move, tap a point to
  delete. Save: new prompts a name (addRoute), edit rewrites points_json
  (updateRoutePoints; drops stale elevations, re-fetch from the preview).
- Slope colouring in discrete 1% bands (green→yellow→red→brown→black, ≥10% black)
  instead of degree buckets; colour-strip legend (slopeLegendHtml).
- Diagnostic logs compiled-in but silenced in release: get_app_info now returns
  debug = cfg!(debug_assertions); JS installs a console.log filter that drops
  lines starting with a diagnostic tag ([poll]/[recover]/[stop]/[GPS]/[HR]/[SD])
  in release only. No more hand-adding/removing logs.
- Fix: saved-route overlay was invisible on the recording (rotating) map — it
  flashed on pinch-zoom then vanished. leaflet-rotate keeps tile/overlay panes
  inside _rotatePane, but the commented-out createPane override meant our custom
  routePane attached to mapPane (outside rotation). Now parent routePane to
  map._rotatePane when present; non-rotating maps unchanged.
2026-08-16 17:15:42 +03:00
f376e0944d Route elevation preview: profile, topo/slope casing, GPXZ + fallback chain
See how hilly a hand-drawn route is BEFORE riding it.

- parseRouteGPX reads <ele>; GPS.routeElevStats computes cumulative distance,
  grade-to-next, ascent/descent (1m hysteresis) and max grade over smoothed
  elevations (filterAndSmoothAlt kills SRTM vertical noise).
- Route preview screen (tap a route name): map with a wide colored casing +
  tappable altitude/grade points, elevation profile chart (steep >8% in red),
  length/ascent/descent/max-grade, fullscreen expand/collapse.
- Casing colored by slope (deg: lime <2, yellow 2-4, red 4-6, black >6) or by
  elevation (green->yellow->brown), toggled with an on-map ∡/⛰ button. Casing
  drawn in a dedicated pane below the overlay pane, so a recorded track stays on
  top; the same toggle is on the track-detail map too.
- fetchElevations tries, in order: GPXZ (Copernicus GLO-30, key from GPS
  settings — stored in localStorage, never in git) -> open-elevation (SRTM) ->
  open-meteo -> OpenTopoData (keyless). Import auto-fetches elevations.
- db.js: updateRoutePoints persists fetched elevations into points_json;
  getRoutes reports hasEle. Route points are now {lat, lon, ele?}.

Native prompt at route import replaced with in-app appPrompt.
2026-08-03 19:29:51 +03:00
bf3c0e2534 Track detail de-jitter: fix overlay stacking, native dialogs, slow save
Fixes to the smoothing/routes UI reported on device:
- Blank detail after save/revert: showSessionDetail was called from inside the
  open detail modal, stacking a second overlay; querySelector("#sd-map") then
  hit the stale, already-initialized Leaflet map and threw, aborting the rest
  of the render (map + speed chart). New reopenDetail() closes the current
  detail (and its fullscreen-map child) and reopens fresh, like recalc/refine.
- Native confirm()/prompt() replaced with in-app appConfirm/appPrompt dialogs
  (winStack + history integrated, hardware back cancels). Used for smoothed
  save, route delete, route rename.
- Slow smoothed save: one UPDATE per point (4000+ IPC round-trips) replaced
  with a chunked CASE update — 18 statements for 4366 points, params kept
  under SQLite's 999 limit. Verified value-for-value on a copy DB.
- Closing the fullscreen map now also closes the smoothing panel (it is a
  fixed element that otherwise lingered over the collapsed map).
- "≈" button active state now reflects panel-open only, not session.smoothed
  (it looked permanently pressed after a save).

Docs: README + PROJECT_MAP updated for saved routes, de-jitter, accuracy_m,
routes table and new gps_points/gps_sessions columns. Version 0.1.393.
2026-07-31 23:51:20 +03:00
cd975b5640 GPS distance: estimate blind-GPS gaps by speed*time; savable gap setting
On stretches where the fix rate drops (dt > gapThresholdSec, incl. dead-
reckoning) the straight chord mis-measures the path, so computeStats now
estimates gap distance by avg(speed)*dt (capped at max speed; a tiny chord
over a long gap means the rider was stopped and the reported speed is stale
-> trust the position). A fast jump WITHOUT a time gap is a teleport glitch
and is dropped. Distance now depends only on ts + speed, not on the
`estimated` flag (which live points carry but DB rows don't), so the live
counter, Stop and recompute finally agree. Verified on a real ride: at
gapThreshold=30 -> 35.7 km / 1024 kcal (map-measured ~35 km); clean tracks
without gaps unchanged.

Settings: read GPS config on input+change (reliable on mobile), add a
"Save GPS settings" button and a "saved" toast so it's clear the value
persisted; changing gapThreshold + recompute now visibly applies.
2026-07-31 12:58:34 +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
84a2b786ca GPS: course-up map rotation in landscape + robust screen-off import
- applyBearing: use h, not h - rotOffset. The .rec-screen (map + panel) is
  CSS-rotated by rotOffset in landscape and setBearing adds another rotate on
  the map pane; subtracting rotOffset cancelled the screen rotation for the
  map only, gluing course to the portrait top edge while the panel turned.
  Using h lets the map turn with the screen -> course points to physical top.
- Poll: guard the requestAnimationFrame yield with !document.hidden. rAF never
  fires while the screen is off, so awaiting it there hung the import loop and
  froze point import mid-walk (regression from the progressive-draw change).
- recoverSession: import missing native points by the SET of existing
  timestamps instead of ts > maxDbTs. Sparse screen-gated points can have a
  late max ts, so the old check imported nothing and left the track full of
  gaps; also pass sensorData so bearing/accel/gyro are stored.
2026-07-08 22:11:17 +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
97539c9110 Gimbal lock compensation, heading EMA smoothing, build.sh install command
Compass:
- Gimbal lock compensation: when phone tilts past horizontal (|beta|>90
  in landscape, |gamma|>90 in portrait), heading flips 180 — compensated
- EMA smoothing for heading (factor 0.3, ~300ms settle) — reduces jitter
  from road bumps while keeping responsive turns
- 3 orient modes: X(off) A(auto) L(lock) — lock freezes layout, compass
  still works with gimbal compensation
- Debug overlay on map shows alpha/beta/gamma/heading/mode

Build:
- ./build.sh install — install last APK without building
- ./build.sh -d install — install debug APK
- install_apk respects -d flag for debug/release selection
2026-07-01 01:12:06 +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
aabcde57da Update docs: activity types, training MET, tile import, road map 2026-06-26 21:05:31 +03:00