Separate BleBaroService so the HR strap and the barometer each hold their own GATT link and
work at the same time (one adapter, several LE connections). Verified on device against a PC
BlueZ emulator of the sensor: BleHR and BleBaro deliver in parallel, no cross-talk.
- BleBaroService: dedicated baro connection (scan-before-connect + direct-connect fallback,
ESS subscribe, writes ble_baro_data.json). MainActivity routes baro connects to it and
auto-connects both HR (ble_device.json) and baro (baro_device.json) at start.
- Rust: ble_baro_disconnect, ble_save_baro_device.
- Settings: a 'Датчики (BLE)' section - heart-rate and barometer as separate rows with their
own slots, bind/connect/unbind, and a live pressure/temperature/altitude readout.
- Tracking: altitude_baro_m stored per point, preferred in elevation stats (null without baro).
- Record screen: 'Высота' from the barometer (1 Hz, dot marker), temperature from the sensor.
- Fixes: direct-connect on scan timeout; Locale.US JSON (RU decimal comma broke the parse);
parsing routed by characteristic (baro no longer written into the pulse); VR/HR stop returns
to the tracks list instead of into details.
- tools/baro-sensor/emulate_baro.py: PC BlueZ peripheral that emulates the sensor for testing.
Emulator-verified only; real board still pending.
Self-made altitude sensor: ESP32-C3 SuperMini + BMP280 broadcasting pressure and
temperature over standard BLE Environmental Sensing. Read into baldav the same way as
the heart-rate strap (Kotlin BLE service -> file -> Rust -> JS), new device_type 'baro'.
- BleHrService: 'baro' type subscribes to ESS 0x181A (pressure 0x2A6D uint32 0.1 Pa,
temperature 0x2A6E int16 0.01 C), writes ble_baro_data.json + ble_baro_log.jsonl.
Every baro branch is gated by deviceType=='baro', so HR/GPS paths are untouched.
- ble.rs: BaroReading, ESS detection, ble_get_baro command (Android reads the file,
desktop returns None). Registered in lib.rs.
- frontend/js/baro.js: passive window.BARO - hypsometric pressure->altitude using the
sensor temperature, anchored to GPS altitude at ride start. Nothing runs until
BARO.start(), so with no barometer the app is unchanged.
- tools/baro-sensor: firmware (PlatformIO main.cpp + Arduino sketch) that reads BMP280
at max oversampling and advertises the ESS service.
Prep only: no hardware yet, verified by compile (build 0.1.538). Live wiring and
ride-track integration pending the board.
- signal is now a loud TTS 'hydrate now' on the ALARM stream (TTS warmed at
session start), so it is unmistakably BalDav and audible when the phone is not
in view (walking/VR); the old ringtone was indistinguishable from a system alarm.
- sweat need scales by activity instead of a flat 600 ml/h: bike 600, walk 200,
run 450, workout/vr 250 ml/h. Walking no longer nags every 15 min.
- deficit split into baseDeficit (seeded from the food log) + accrued sweat;
logging a drink mid-session recomputes the base and re-seeds via seedSeq, so it
silences the reminder. seedSeq=0 (profile save / init) never zeroes it mid-ride.
- persistent 'БалДав: попей воды (HH:MM)' notification left as a record.
Surface was only paved/unpaved binary - gravel, dirt and sand cost the same. Add a
surfacepenalty into costfactor so a dirt track beats a gravel one of the same tracktype
(asphalt 0 < compacted .2 < ground/dirt/earth .4 < cobblestone/sett .6 < gravel .9 <
clay 1.3 < grass/wood 1.5 < sand/mud 5). Canonical lookup values only. Verified on the
home RouteServer: engages on unpaved, doesn't distort paved routes. Untagged-ford
avoidance by estimated_river_class was tried and dropped - it forced 5x detours (flags
riverside ways, not only crossings); untagged fords stay shown (wetcross) + future marks.
Reverting the SmsManager direct-send path and its runtime SEND_SMS request - the SMS
feature was meant to use the system SMS composer, which needs no permission. Keep only
the prefilled ACTION_SENDTO composer.
- brouter_route gets a format param; routeBrouter returns formatAsGeoJson (WayTags per
segment) when asked; BRouter.routeGeoJson + GPS.parseRouteGeoJson extract geometry +
dangers (ford / grade5|bad-trail / ferry) from the tags
- route editor routes road legs via geojson, marks fords (cyan) and rough tracks
(orange) on the map with a '⚠ брод/грунт' counter, so the rider sees the hell BEFORE
riding it
- 'Броды: избегать/разрешить' toggle picks trekking vs trekking-adventure (allow_adventure)
and re-routes; new trekking-adventure.brf profile (asset + staging)
Parser validated against the home RouteServer on a real ford (47.5387,40.3164).
The ford/grade5/bad-trail penalty sat in the way initialcost and never changed the
route cost (verified on the home RouteServer: avoid and adventure gave identical cost).
Moved it into costfactor next to town/traffic penalties. Verified against a real ford
(47.5387,40.3164): avoid now detours 1.4km->6.2km with fords=0, adventure keeps the
1.4km ford crossing. Node-ford penalty (node initialcost) kept.
Routed through icy fords and abandoned descents 3x. Penalise ford nodes & ways
(ford=yes|stepping_stones), tracktype=grade5 and trail_visibility=bad|horrible|no in
the trekking profile, gated by a new %allow_adventure% param (default false = avoid).
Both profile copies (asset + staging) kept in sync. NOTE: profile syntax must be
verified by routing on device; a bad profile makes routeBrouter error out (safe), not
produce bad routes.
- move the SMS button out of the bottom panel into a left-hand square-button group
(bottom-left, opposite the right control stack), add a LIVE toggle next to it
- LIVE turns location sharing on/off right from the record screen, even if it was
never enabled in the profile; warns if the token is still unset
- LiveShare re-reads its config every tick so the toggle takes effect within ~10s
Feature 1 - opt-in live location:
- set_live_config Rust command writes live_share.json (url/token stay on device)
- LiveShare.kt singleton posts the current fix to /api/live/push every ~10s from the
GPS flush loop, on a background thread; diagnostics in live_share_log.jsonl
- profile toggle + endpoint/token fields, off by default; server side is in botovod
Feature 2 - SMS 'я тут':
- send_sms / pick_contact / get_contact_result Rust commands
- MainActivity: system contact picker (no READ_CONTACTS), direct SmsManager send when
SEND_SMS is granted else the prefilled system composer; SEND_SMS in the manifest
- contact config in profile (manual + picker), 'СМС' button on the record screen with
an in-app confirm; text carries coords + a Yandex Maps pin link
README + session log updated.
Hydration reminder: ON by default (native defaults on when hydration.json is
missing; JS writes the default config at startup; the profile toggle is
checked by default) so it works out of the box on a ride instead of needing a
hidden toggle. Adds hydration_log.jsonl diagnostics and swaps the ToneGenerator
for the real alarm ringtone via MediaPlayer on STREAM_ALARM, with the alarm
volume forced to max for the alert, so it is actually audible.
Calorie bank: cumulative sum of daily balance over the window (+kg) in the day
summary, answering "how much to eat up/under". Balance chart (weight vs balance)
y-axis now floats symmetrically about 0 (±500 floor, no fixed ceiling) so one
hard day no longer flattens the line.
Intake is what you already log (liquids count via water_pct), so there is
nothing to tap mid-ride and no guessing an amount. At session start JS seeds
the native deficit from the food log — need-so-far for the time of day minus
logged intake — via set_hydration_config(start_deficit_ml); the service then
accrues sweat on top. Removes the +вода button, hydration_drink and water_drink.txt.
A shared Hydration singleton (ticked by the GPS ride service and the HR/VR
service, deduped in one process) accrues the body's water need over an active
session and subtracts what you logged via the "+вода" button. It stays silent
while you keep up — can be quiet for hours — and only beeps LOUD on the alarm
stream (cuts through media volume) once the running deficit passes a threshold,
then re-beeps on a cooldown. Not a fixed timer.
Rust set_hydration_config / hydration_drink bridge JS settings and the "+вода"
button to hydration.json / water_drink.txt that the native service reads.
The strap's battery % is coarse/laggy (reads 100% for >24h), so to judge real
battery life we accumulate the time it actually delivered HR plus every battery
step into strap_uptime.json in the foreground service (persistent across
sessions). ble_read_strap_uptime / ble_reset_strap_uptime expose and reset it;
Settings shows hours-since-charge + steps with a Charged reset button.
On an unexpected disconnect of an established connection (connectedOnce, i.e.
services were discovered), reconnect via the scan path up to 3 times, 8s apart,
resetting the budget on each successful reconnect. The scan gives up (stopSelf)
if the strap isn't found, so we never hammer a band that wasn't present at start
or wasn't connected by hand. A user stop, Bluetooth off, or onDestroy sets a
stopping flag that suppresses reconnect.
The JS 2s poll that stored workout HR samples is frozen by Android when the
screen is off, so a sleep-long recording kept only the on-screen stretches
(the strap and live display were fine — the persist path was the gap). The
foreground service now appends every reading to ble_hr_log.jsonl; ble_read_hr_log
exposes it; on stop the workout rebuilds its samples from that log (falling back
to the poll array). ble_clear_hr truncates the log at start.
Single Handler for all postDelayed work, cancelled on teardown; a BT
ACTION_STATE_CHANGED receiver that cleanly stops the service on STATE_OFF;
try/catch around every native scan/connect/GATT call. Prevents the libc
destroyed-mutex abort when the adapter is disabled mid-connection.
User forgets to turn BT/GPS on. Modern Android won't let an app toggle them
silently, so prompt and act: BT via the system enable dialog, location via
the location-settings screen.
- ensureSystemReady(needBt, needGps) runs before startRecording: BT off ->
confirm -> ble_request_enable; location off -> confirm -> open settings.
Gated by two settings (baldav_dontask_bt / _gps); declining proceeds.
- GPS.isLocationEnabled polls a new Kotlin check (check_location writes
location_state.json via LocationManager; read_state_file reads it);
open_location_settings opens ACTION_LOCATION_SOURCE_SETTINGS.
- Settings: 'Не спрашивать про Bluetooth' / '... геолокацию' checkboxes.
- BleHrService reads the standard Battery service (0x180F/0x2A19) 2.5s after
the HR subscribe and every 60s; writes 'bat' into ble_hr_data.json.
- HrReading gains battery: Option<u8> (serde alias 'bat').
- gps.js: the HR poll captures reading.battery; _checkBatteryAlert speaks
(speechSynthesis ru-RU) and beeps once at 10% and 5% (beep is the
guaranteed cue where TTS is absent), resetting above 15%.
- Workout screen shows the strap charge in its status line.
Recording auto-called ble_connect each ride, and the Kotlin service held a
GATT connect to an absent band (radio busy) plus START_STICKY churn -
draining the battery when the band was away or Bluetooth off.
- gps.js _startHrMonitor: drop the auto ble_connect; keep only the cheap HR
poll so a manual connect (now or mid-ride) lands HR in the track.
- Settings: a 'Подключить' button for the paired band, usable any time incl.
during a ride. BT off -> ble_request_enable (system enable dialog); on ->
ble_connect, start the poll if recording, report connected / not found.
- ble.rs: ble_request_enable command (+ handler registration).
- MainActivity: ble_request_enable -> ACTION_REQUEST_ENABLE (BLUETOOTH_CONNECT
already in manifest).
- BleHrService: START_NOT_STICKY; if the 5s scan doesn't find the address,
stopSelf instead of directConnect to an absent device.
Rust export_image(filename,base64,action) + read_file_base64(path) (registered).
Kotlin exportimg: branch — base64-decode, then SAF-save the PNG (CreateDocument
image/png) or share it via FileProvider + ACTION_SEND chooser. Settings gets a
'Фон для картинки' row: pick a photo (pickLocalFile image/*), read it as base64,
downscale to <=1080 and store as a data URL in localStorage for the 'С фото'
variant. Rust compiles in tauri dev; native paths need an APK rebuild to test.
Stage 1 of the nogo/reroute work. Mark impassable circles that BRouter routes
around, end to end:
- DB nogo_areas table + CRUD; drawNogoAreas() red circles (tap to remove).
- BRouter.route(points, profile, nogos): &nogos=lon,lat,r on the RouteServer and
nogos in the embedded-engine request; Rust brouter_route passes it through;
Kotlin builds OsmNodeNamed(radius,isNogo,nogoWeight=NaN) -> rc.nogopoints +
prepareNogoPoints (needs an APK rebuild for offline use).
- Planner: ⛔ button toggles nogo-placement (tap map/line), each leg re-routes
around nogos. Recording map: ⛔ marks the current spot impassable. Detail map
shows the circles.
Verified the RouteServer detours around a nogo (2873m -> 3329m).
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).
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.
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.
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.
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.)
- 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.
- 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.
- 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.
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
GPS altitude: accel-based glitch filter (accel=0 + speed = GPS drift), altitude
jump >100m filter, timestamp sanity (>24h = remove). Per-point Crr from accel
vibration (smooth/mixed/gravel). Gap interpolation on map with dashed lines
when accel confirms movement. Walk/run separate speed limits (configurable).
Adaptive GPS accuracy (auto x2 when signal bad). Dead reckoning points when
GPS silent but accel shows movement. Locale.US fix for sensor JSON.
Sensors: accelerometer + gyroscope in GpsRecorderService (RMS, max, pitch, yaw).
Columns in gps_points. Dynamic addGpsPoint INSERT (safe if columns missing).
Manual routes: draw on map by tapping points, elevation from past tracks +
open-meteo API, realistic timestamps by activity speed. Manual sessions
skip glitch filtering on recalculate. Speed from GPS.config not hardcoded.
Water tracking: water_pct field in product add/edit forms, searchProducts
returns water_pct, "ml" label for liquids, recipe water from ingredients.
BMR: moving average weight (7 days), fixed charts using dailyBase not raw bmr.
BLE: ble_clear_hr on stop, recovery toast, serde alias for ts/timestamp.
Products: 217 Gridnev products (bakery, desserts, bread, ready meals).
Magnit scraper fix (NUXT_DATA name/value pairs, not indices). Supplements
addSupplement with all 41 columns. Nutrient fallback RDI includes omega/EPA/DHA.
Export/import: custom products + recipes + supplements in single JSON bundle.
Export all or single product. Dedup on import by name_lower + source.
OTA: router upload (LAN, always), VPS upload (-u flag). Kotlin checks router
first (2s timeout), then VPS. Update URL from update_info.json.
Session detail: renderToken prevents stale setTimeout callbacks after recalculate.
WAL disabled (PRAGMA journal_mode=DELETE).
Size comparison was unreliable — SQLite reuses free pages internally,
so adding products didn't change file size. New seed never copied.
Now: always overwrite on every launch (~8MB, <1 sec).
VACUUM reduced seed from 8.5MB to 8.2MB.
Navigation:
- _modalPopHandled flag prevents global popstate handler from firing
on the same event that modal popHandler already processed
- Fixes: back from fullscreen map/chart no longer exits the app
Screen wake lock:
- JS: navigator.wakeLock.request("screen") on recording screen
- Kotlin: FLAG_KEEP_SCREEN_ON on GPS start, clearFlags on stop
- Screen stays on while recording is visible in foreground
- Released when navigating away from recording screen
Multi-segment tracks:
- GPS gaps > gapThresholdSec (default 30, configurable) split track
into separate polyline segments on the map
- Each segment has own green start / red end markers
- Shows clearly where GPS signal was lost vs continuous tracking
Settings:
- "Макс. пауза GPS (сек)" — gap threshold for segment splitting
- Physics constants (aero drag, air density, pedal efficiency)
moved from hardcoded to GPS.config + settings UI
Native service (Kotlin):
- maxSpeedMs filter: points with speed > max are not recorded
- Config passed as 4th param: start:minSpeed:maxAcc:minDist:maxSpeed
- Default 60 km/h (16.67 m/s)
Settings:
- "Макс. скорость (км/ч)" field in GPS config section
- Saved to localStorage with other GPS settings
Recalculate fixes:
- Used cleaned sorted array instead of original points (was computing
stats on dirty data with glitches -> hundreds of thousands of kcal)
- parseFloat on weight from profile (string "88" + 12 = "8812" bug)
- seq re-assignment only runs when points were actually deleted
- Glitch points deleted from DB on recalculate (not just skipped)
Old logic: copy only if file doesn't exist. App updates with
newer seed DB were ignored because the old file was already there.
Now compares file size with asset size and overwrites on mismatch.