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.
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://mirror.invalid: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).
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.
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.
End-to-end offline routing confirmed on the phone: shortest ~5.4 km in Rostov
(3.2s cold), trekking cross-tile Rostov->region 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.
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.
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.
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).
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.
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.
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.)
- 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.
- 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).
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.
- 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.
- 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.
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.
- 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).
- 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.
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.
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.
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.)
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.
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.
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.
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.
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.
- 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
showSessionDetail rendered its map/charts/buttons inside setTimeout blocks
guarded by renderToken. On WebKitGTK the deferred render was voided, so the
track detail showed no map, charts, or fullscreen button (while the
synchronously-rendered roads view worked).
- Remove all setTimeout/requestAnimationFrame/IIFE from showSessionDetail;
render map, charts and wire buttons synchronously right after the modal
markup exists (like showRoadsMap). Early `return`s become `if` guards;
goals/food-cost keep real `await`. Force layout (offsetHeight) + call
invalidateSize so Leaflet reads the real container size in the modal.
- Fix large i64 session-id corruption from tauri-plugin-sql 2.4.0 (ids come
back off by 1 as JS numbers). Read/pass session ids as text via
CAST(id AS TEXT) AS id_str everywhere they are reused: Trek list, Today
click, compare modal/view, goal-marker prev sessions, map default-center,
and importGpxSession (whose points were orphaned by the corrupted id).
SQLite coerces the string to the INTEGER column. Point ids (max ~135k)
are small and unaffected.
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