Remove the home-city name from docs/comments/UI strings and the external
mirror IP (WAN) from code, docs and the Android cleartext allowlist, replacing
the IP with mirror.invalid (set the real host locally, uncommitted). LAN mirror
and routing keep working; the WAN fallback needs the real host set. Generic
big-city map presets are kept (not identifying).
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.
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 region (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
region with correct bounds/category culling; the live visual could not be
screenshotted (the WebView surface is black under CDP). frontend/data + poi.js.
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.
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.
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).
- 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).
- 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).
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.
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.
- 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.
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.
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.
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.
- 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.
- 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.
- 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).
- 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
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.
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
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
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