Commit graph

142 commits

Author SHA1 Message Date
7d85773ae3 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
b8fed04f95 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
8e3c9fd4b6 BRouter: offline route planning by waypoints + segment download UI
Stage 3 — planning: showRouteEditor gets a "🚲 По дорогам" toggle. Waypoints
stay editable markers; with it on, the line between them (and the saved
geometry) follows roads via BRouter (brouterMode/routeGeom, 450ms debounce on
edits). BRouter.route (router.js) now: Android -> embedded engine, otherwise
(desktop / at home) -> the PC RouteServer over HTTP (http_request bypasses the
WebView CSP; LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777). Lets planning be
debugged on the desktop.

Stage 2 — segment UI: a "Сегменты" button on the roads screen opens
showBrouterSegments — region tiles (E35_N45/E40_N45), "есть" status, a
Download/Update button with percent progress via BRouter.downloadTileAndWait.

Verified: RouteServer returns 83 trkpt for a 3-waypoint trekking route (3594 m),
parseRouteGPX reads trkpt; both files compile into the apk. End-to-end UI (tap
waypoints -> routed line; in-app segment download) pending desktop/phone run.
2026-09-09 17:33:11 +03:00
2d4a3ed933 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
7554146772 brouter-web: broaden dark theme (sidebar tabs, inputs, dropdowns, elevation chart text/footer)
Cover the elements that stayed light: right vertical sidebar tab strip, search
fields, the profile dropdown and dropdown menus, light-styled buttons, the
elevation chart's SVG axis text (was black on dark) and its background/footer/
stats. Injected as a <style> keyed on body.bw-dark.
2026-09-09 16:51:58 +03:00
456322bf75 POI overlay on maps: circular contrasting icons + category filter (offline)
Points of interest (cafe, shops, water, toilets, bank/ATM, bus, shelter,
viewpoint, ...) as circular contrasting badges (emoji in a white/dark circle,
readable on light and dark tiles) — not pins. A 📍 control toggles the layer
and opens a per-category filter (checkboxes, remembered). Markers are culled to
the visible bounds and shown from zoom 13 (cap 500/view), on the recording,
track-detail and roads maps.

Data is bundled and fully offline: frontend/data/pois.json ([{la,lo,c,n}], 358KB,
5662 POIs) fetched once from Overpass for ~100km around Novocherkassk (the whole
bbox returned in one query; adaptive subdivision was ready if it had truncated).

Verified functionally on device: 124 badges rendered in view at z16 over
Novocherkassk with correct bounds/category culling; the live visual could not be
screenshotted (the WebView surface is black under CDP). frontend/data + poi.js.
2026-09-09 16:51:58 +03:00
d7b77f5bc5 brouter-web: robust dark toggle (floating button + injected style)
The navbar toggle was invisible/zero-width; replaced with a fixed floating
button (bottom-right). Dark mode now injects a <style> keyed on body.bw-dark
covering the tile layer, right sidebar, left toolbar (zoom/draw controls) and
the bottom elevation profile panel (incl. the chart drawn later on route).
2026-09-09 14:57:12 +03:00
74770c1e60 tools/brouter-web: preserve mirror customization (config + dark toggle + default overlays)
Static brouter-web on the Keenetic mirror pointed at the PC RouteServer.
config.js (host/layers/centre), baldav-custom.{css,js} (day/night toggle that
inverts only the tile layer + dark sidebar, one-time default POI overlays),
and a README with the deploy steps. Kept in-repo so the mirror is reproducible.
2026-09-09 13:50:42 +03:00
9798f152d8 Map: dark backdrop + reliable tile caching
Two fixes to the dark map + offline tiles:
- Dark backdrop: body.dark-map also darkens .leaflet-container (was #ddd), so
  the light Leaflet background no longer flashes bright between/behind tiles
  while zooming or where a tile is missing.
- Tile caching: createTile fetched the display <img> from the URL but cached
  via a SEPARATE fetch — on a flaky mobile link that second request often
  failed, so tiles showed but were never cached and re-zooming re-fetched them.
  Now one fetch caches and displays from the same blob, so every shown tile is
  cached. CSP already allows connect-src to the OSM tile host.

Verified on device: container bg rgb(27,31,36) under dark map; IndexedDB tile
cache populated.
2026-09-09 13:46:22 +03:00
a36d9fad9b Map: dark-map toggle (invert tiles only), no repacking
A moon button on the recording, track-detail and roads maps inverts ONLY the
tile layer: body.dark-map applies `invert(1) hue-rotate(180deg) brightness(1.5)`
to .leaflet-tile-pane, so the track/route/roads/puck overlays keep their colours.
Works the same on offline (tiles_*.db) and online tiles — no keys, quotas or
tile repacking. The choice persists in localStorage (baldav_dark_map).

Chosen after ruling out dark tile providers (CARTO/Thunderforest/2GIS/Yandex —
keys/quotas/ToS/projection/RF-reachability) and the transport style (inverts
badly in dense city); plain OSM inverted looks good city and countryside.

Verified on device: filter on the tile pane only, overlay pane filter none,
button active and remembered.
2026-09-09 13:33:05 +03:00
990924a048 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://77.66.192.45: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
542ceb17e3 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
3aa3ffdac1 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
fe9f1de9c8 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->Novocherkassk 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
c3188911c3 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
5b345a1df4 rec screen: status-bar backdrop so the system bar stays visible over the map
The recording map is edge-to-edge and drew behind the system status bar,
leaving its icons unreadable over the tiles (the map controls were already
offset by --safe-area-top, so the design intended a visible bar). Paint a
solid strip the height of the top inset behind the bar — like the header on
every other screen. Disabled in the rotated landscape modes.
2026-09-07 17:15:19 +03:00
2d23f3a475 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
9f8ab0eeb1 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
e9efb4fc6c 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
86a9a1846b 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
4ab8d0a554 build 434 + docs: scanner zoom/torch, OS back-gesture fix 2026-08-27 23:14:54 +03:00
5d042870e2 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
754e462cfa ЧЗ scanner: digital-zoom fallback + fix back-stack desync on scan->add
- Zoom slider: many Android WebViews don't report a `zoom` track capability
  (torch works, zoom doesn't), so the slider never showed. Add a digital-zoom
  fallback (always-visible slider 1-5x): CSS-scale the preview + crop the decode
  ROI tighter to center. Hardware zoom still used when available.
- Back gesture did nothing after scanning: onDecoded closed the scan modal with
  the async history.go(-1) and then synchronously opened the add-product modal,
  so history.go unwound the NEW modal's entry and desynced the winStack/history.
  Add closeThen(): await the close's popstate(s) before opening the next modal.
2026-08-27 18:08:25 +03:00
34941e82dc docs: recognition — zxing-wasm switch, CSP wasm fix, zoom/torch, on-device verify 2026-08-27 11:31:33 +03:00
ac8c0ece56 build 427 2026-08-27 11:30:55 +03:00
05b9df4083 ЧЗ scanner: zoom + torch, looser ROI, CSP wasm-unsafe-eval (wasm verified on device)
- The tight guide box forced the phone closer than it can focus. Drop it: decode
  the whole visible center square (WYSIWYG under object-fit:cover) at up to 768px;
  zxing-wasm locates the code anywhere in it.
- Add a zoom slider and a torch toggle driven by MediaStreamTrack capabilities
  (getCapabilities/applyConstraints), plus continuous focus and a 1920x1080 request
  — the user enlarges a small code optically instead of moving too close.
- CSP: add "script-src ... 'wasm-unsafe-eval'". Tauri's generated hash-based
  script-src dropped unsafe-eval, so WebAssembly.instantiate was blocked
  ("violates CSP") — verified fixed on-device via CDP (zxReady ok, code_33 decodes).
2026-08-27 11:28:00 +03:00
179ddf11da ЧЗ scanner: switch decoder to zxing-wasm (ZXing-C++) for reliable DataMatrix
The pure-JS @zxing/library locator is weak on real ЧЗ codes (decoded 1/4 of the
test photos with correct grayscale input; libdmtx and zxing-wasm read 3/4 — the
4th is physically undecodable). Symptom: scanner takes very long to even find a
code, 2 of 4 never read.

- Bundle zxing-wasm reader as an IIFE (frontend/lib/zxing-wasm.js, global
  ZXingWASM) + zxing_reader.wasm; drop frontend/lib/zxing.js. Init fetches the
  .wasm as an arrayBuffer and hands it over as wasmBinary (no streaming/MIME/CDN).
- ChZ.zxReady()/ChZ.decodeImageData() (chzn.js): decode an ImageData -> raw GS1
  bytes (result.bytes keeps the GS separators) with tryHarder/tryRotate/tryInvert.
- Scanner (main.js) rewritten to a canvas loop: crop the central ROI, upscale to
  540px, decode each frame — full-frame search was the slow/unreliable part.
  Photo path decodes the picked image the same way.

Not yet verified on-device (WebView WASM instantiation); decoder logic validated
in a Node harness against the 4 sample photos.
2026-08-27 09:42:05 +03:00
8d95768b1e build 423 2026-08-27 04:42:14 +03:00
2ac394d840 docs: ЧЗ scanner — offline ZXing decoder (no GMS), ЧЗ badge, session auto-push 2026-08-27 04:37:24 +03:00
fe7845cab9 Chestny Znak scanner: bundle offline ZXing decoder, ЧЗ badge, session auto-push
- Native BarcodeDetector needs Google Play Services (fails "Barcode detection
  service unavailable" on de-Googled ROMs). Replace it with a bundled, fully
  offline ZXing decoder (frontend/lib/zxing.js, @zxing/library UMD): scanner uses
  BrowserDatamatrixCodeReader (TRY_HARDER) for live camera and decodeFromImageUrl
  for the photo fallback. No Google, codes never leave the device.
- Source badge "ЧЗ" (src-chzn) for products added via the scanner.
- tools/chzn_export.sh: adb push the exported session to the device Downloads so
  BalDav's file picker can import it directly.
2026-08-27 04:31:58 +03:00
301138c93a 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
4d6cfbba09 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
8f57c482a8 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
d11402f116 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
7f454cd7e4 docs: route-editor mid-insert, track color setting, app dialogs, orient-dbg debug-gate 2026-08-22 22:18:44 +03:00
42614d9606 Route editor mid-insert (tap line), track color setting, app dialogs, debug-only orient overlay
- Route editor: tap the route line to insert a point at that spot (nearest
  segment, splice); addMarker -> makeMarker so end-append and mid-insert share
  the drag/delete handlers. Add/drag/delete unchanged.
- Настройка 'Цвет текущего трека' (GPS.config.trackColor, default #bb86fc):
  recording line, detail track + gap dashes, manual-draw line/markers use it —
  so the track is distinguishable from the (blue) roads overlay.
- Replace remaining native dialogs with app ones: new appAlert; tile-path prompt
  -> appPrompt; seed/modal/product-in-use alerts -> appAlert. Selects untouched.
- orient-dbg sensor overlay now only renders when window.__BALDAV_DEBUG (debug
  builds); silent in release.
2026-08-22 22:08:58 +03:00
93dc4b0913 Fix: elevation chart empty after 'Уточнить высоты' (read srtm, not just gps)
showSessionDetail's elevation profile, min/max, x-labels and point popup read
only altitude_gps_m, but 'Уточнить высоты (онлайн)' writes altitude_srtm_m
(computeStats already prefers srtm — hence stats updated but the chart stayed
empty). Read altitude_srtm_m ?? altitude_gps_m everywhere the detail shows
elevation, matching computeStats. No regression: no srtm -> falls back to gps.
2026-08-22 14:23:06 +03:00
1007e2e629 build: embed exact/fresh git hash (+ -dirty for uncommitted code)
build.rs now re-runs when the commit changes (cargo:rerun-if-changed on
.git/HEAD + the branch ref) so GIT_HASH never goes stale, and appends -dirty
when tracked code (frontend, src-tauri/src, build.rs) is uncommitted. Lets a
distributed APK be pinned to an exact source commit for regression tracking.
README documents versionName 0.1.NNN + hash traceability. Decision at b4fdc824144.
2026-08-20 19:55:08 +03:00
d23133a1bc docs: WebView CDP debugging + build.sh usage notes 2026-08-16 21:16:55 +03:00
cda91995b9 Fix: single-point track hung showSessionDetail (Delete did nothing)
Diagnosed live over the WebView DevTools protocol: a 0 km track had exactly one
usable point. The map block runs (points.length is truthy), but with a single
coordinate fitBounds is skipped, so the Leaflet map never gets a view and never
fires 'load'. With a saved route active, the overlay code then does
`await whenMapReady(map)`, which waits on that 'load' event forever — hanging the
rest of showSessionDetail, so the Delete/Recalc/Export handlers past it never
bind. No exception, just a silent stall (hence "nothing happens on Delete").

Fix: when there are < 2 coords, setView to the single point so the map loads and
whenMapReady resolves. Verified end-to-end via DevTools: the button now binds and
the 0 km track deletes.

(The prior commit moved the handlers out of the `points.length` guard, which
covers a truly 0-point track; this covers the 1-point-with-route hang.)
2026-08-16 21:06:50 +03:00
f1cb93cfb6 Fix: can't delete a 0-point (0 km) track
The Delete / Recalculate / Refine / Compare handlers were bound inside
`if (mapEl && points.length)` in showSessionDetail, so a track with no points
(started recording, no GPS fix, stopped → 0 km) skipped the whole block and its
"Удалить" button did nothing. Moved the session-action button handlers outside
the map/points guard so they bind regardless. (Also covers a track whose points
all get dropped by the new accuracy filter.)

DB.deleteGpsSession already handled zero points fine (DELETE of 0 rows is a
no-op); the bug was purely the unbound click handler.
2026-08-16 20:35:08 +03:00
f1c04c9bfe 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
5b664c2df9 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
564be2b91d 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
74f26cd2d3 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
0de08bc22c Saved routes overlay + per-activity road colors + reversible track de-jitter
Routes:
- routes table + CRUD helpers (db.js), GPX rtept/trkpt/wpt parser (gps.js)
- "Saved routes" screen (import/rename/recolor/delete, one active at a time)
- Route overlay on both the recording map and the past-track detail map,
  toggled by an "M" button; default ON at Start when a route is selected
- Draw after real map-load via async whenMapReady(), no setTimeout

Road colors:
- per-activity configurable colors (walk/bike/run); fix walk roads (atype "w")
- get_road_points maps run -> "r" (lib.rs)

De-jitter (Savitzky-Golay):
- GPS.smoothTrackSG (quadratic LS per window, iterated, endpoints anchored)
- reversible storage: lat_smoothed/lon_smoothed + session.smoothed flag
- detail-screen tuning panel (window/iters sliders, live preview,
  save smoothed / revert original, kcal recomputed on new points)
- accuracy_m column per point for later accuracy-gated recompute

Active map-button state restyled to inverted (dark glyph on translucent
white) for OLED; detail map safe-area handled in fullscreen.
2026-07-31 22:11:55 +03:00
1812f79514 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
ca45bf27c7 GPS stop: import only the un-polled tail, not a full file re-scan
The old stop path re-added every native point "not already in the DB",
which raw-inserted points the live poll had intentionally dropped
(stationary / auto-pause / wrong state) with scrambled timestamps. The
detail view draws by seq order, so those late-written points with early
timestamps rendered as spikes across the map (distance stayed correct —
computeStats sorts by ts). Now stop imports only points past
_lastProcessedTs (the poll's processed marker): the genuinely un-polled
tail, including a screen-off stretch, without re-adding filtered points.
2026-07-30 00:23:05 +03:00
a9c33d0eab 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
9968ebd594 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