Commit graph

28 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
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
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
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
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
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
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
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
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
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
ddac7ade70 GPS: full native re-import on stop + visible progressive import
- stopRecording: replace the dead _lastNativeCount backfill (the variable
  is never assigned → guard is always false → missed points were never
  saved) with a full timestamp-based re-import from the native points
  file, then recompute stats over the complete set. Fixes losing ~90% of a
  track when recording with the screen off — the JS import poll is frozen
  by the WebView while the screen is off, so only the native file had all
  points.
- Poll: draw each imported point via a lightweight onImportPoint and yield
  a frame every ~20 points so the track visibly grows as points transfer
  on screen-wake catch-up (restores pre-batch behaviour, kept cheap).
2026-07-07 23:45:11 +03:00
edb1427c3c GPS accuracy-gated points + bearing; fix startup UI freeze
- Native: hard-reject fixes coarser than the Min-accuracy setting (drops
  coarse cell ~800m that stuck the track); remove low-accuracy and adaptive
  relaxation branches so only accurate fixes are recorded, honest gaps otherwise
- Store bearing_deg per point (gps_points migration + addGpsPoint)
- Default Min-accuracy 20 -> 50m (one-time migration, respects user override)
- Fix: http_request was synchronous with no timeout; an unreachable host
  (LAN sync on startup) blocked the whole UI ~30s. Now async + spawn_blocking
  + connect(4s)/global(12s) timeouts, fails in background without freezing
- Bundled prior work: course-up map orientation, compass calibration,
  online elevation refine (open-elevation), distance gap-gate fix
2026-07-07 00:28:27 +03:00
1c95fdfb68 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
7c6b9a870f 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
bc3161c5a2 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
f2d9639476 Update docs: activity types, training MET, tile import, road map 2026-06-26 21:05:31 +03:00
31faa20f16 Update docs: tile import, road map, orientation lock 2026-06-26 16:47:52 +03:00
4098c9bcdc Update docs: compass rotation, landscape mode, sync, dead reckoning, placeholder 2026-06-25 21:02:37 +03:00
4e941c4ecb Update README with manual routes, export/import, OTA router 2026-06-18 22:55:32 +03:00
aa090fb9ac Update README and PROJECT_MAP with BLE HR, water tracking, OTA, nutrients, altitude filter 2026-06-07 01:41:33 +03:00
d1ff631607 Physics settings in UI, recipe modal cleanup, weight parseFloat fixes
Settings:
- Aero drag (CdA), air density, pedal efficiency now in GPS config
- Human-readable names: "Аэросопротивление", "Плотность воздуха", "КПД педалирования"
- Saved to localStorage, used in computeStats

Bug fixes:
- Recipe detail: overlay.remove without popHandler cleanup → now uses history.back()
- weightKg parseFloat in startRecording and importGPX (string "88" bug)
- profile.weight_kg stored as number not string
- getDailyActivityTotals: separate params for UNION ALL subqueries

Docs:
- PROJECT_MAP.md — full project structure, DB schema, screens, formulas
- README.md — overview, build instructions, data sources
2026-05-26 02:14:43 +03:00