- Editor shows the route with casing + slope/elevation colouring like the detail
view (drawRoutePolyline over the routed geometry); day/night, slope/elev and
locate controls added. Root cause of the plain thin line: ele was dropped when
building leg geometry — now kept from BRouter, so casing/slope work here and in
showRouteDetail. Straight legs (no ele) stay a thin line.
- Insert a point is now a TAP on the line (fat invisible hit target,
pointer-events:stroke); long-press toggles road/straight.
- Routes store editable waypoints (routes.waypoints_json {waypoints,legMode})
alongside the dense geometry, so re-editing shows the 5-6 points, not thousands.
- Bottom bar reworked (.re-bottom): hint on top, buttons in a row, cancel below.
- Gap-fill is conservative: only real long dropouts (dt>60s, chord>150m), skips
big detours (>2x chord) and no-gain fills (road ~ straight).
Gap-fill (track detail): GPS.fillGaps now returns {points,gaps} with per-gap
status; the 🧩 control previews ONLY the filled dropouts (cyan casing; unfilled
= red dashed) with a Save/Cancel panel. Save is a reversible in-place patch
(estimated points added, original untouched; DB.saveGapFill/revertGapFill +
estimated/gap_filled columns), then recalculateSession. Revert removes them.
Route planner: renamed menu 'Нарисовать маршрут' → 'Нарисовать тренировку'
(it makes a workout session, not a route); routes empty-state now points at the
on-phone planner. showRouteEditor rewritten per-leg: each leg road (BRouter) or
straight independently, road by default, tap a leg to toggle it, long-press to
insert, silent straight fallback where no road; the global button sets all legs.
Stage 3 — planning: showRouteEditor gets a "🚲 По дорогам" toggle. Waypoints
stay editable markers; with it on, the line between them (and the saved
geometry) follows roads via BRouter (brouterMode/routeGeom, 450ms debounce on
edits). BRouter.route (router.js) now: Android -> embedded engine, otherwise
(desktop / at home) -> the PC RouteServer over HTTP (http_request bypasses the
WebView CSP; LAN 192.168.1.18:17777 / WAN mirror.invalid:17777). Lets planning be
debugged on the desktop.
Stage 2 — segment UI: a "Сегменты" button on the roads screen opens
showBrouterSegments — region tiles (E35_N45/E40_N45), "есть" status, a
Download/Update button with percent progress via BRouter.downloadTileAndWait.
Verified: RouteServer returns 83 trkpt for a 3-waypoint trekking route (3594 m),
parseRouteGPX reads trkpt; both files compile into the apk. End-to-end UI (tap
waypoints -> routed line; in-app segment download) pending desktop/phone run.
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.
Two fixes to the dark map + offline tiles:
- Dark backdrop: body.dark-map also darkens .leaflet-container (was #ddd), so
the light Leaflet background no longer flashes bright between/behind tiles
while zooming or where a tile is missing.
- Tile caching: createTile fetched the display <img> from the URL but cached
via a SEPARATE fetch — on a flaky mobile link that second request often
failed, so tiles showed but were never cached and re-zooming re-fetched them.
Now one fetch caches and displays from the same blob, so every shown tile is
cached. CSP already allows connect-src to the OSM tile host.
Verified on device: container bg rgb(27,31,36) under dark map; IndexedDB tile
cache populated.
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.
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.
- 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.
- 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).
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.
- 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
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
- flex-direction:row in landscape — panel 200px on the right, stats vertical
- CSS rotation compensates phone orientation, user sees element coords directly
- Bottom nav hidden in landscape, restored on exit
- GPS placeholder overlay: separate div from map container to avoid Leaflet conflict
- display:contents removed, clean column layout for stats in landscape
- leaflet-rotate plugin: map rotates by compass heading via deviceorientation
- Landscape detection: CSS viewport rotation when phone held sideways (gamma > 50)
- Bottom nav hidden in landscape mode
- GPS placeholder: pixel art overlay shown until GPS signal arrives
- invalidateSize() after map init to prevent black screen
- sourceBadge() helper renders colored badge by source field
- searchProducts SELECT now includes source column
- getFoodForDate SELECT includes p.source as product_source
- Badge shown on product cards in search and food log entries
- CSS: .src-badge with per-source colors (pastel, dark-theme friendly)
Custom product:
- "+" button next to search on Products tab
- showAddProductModal: name, KBJU, + all MICRO_FIELDS (34 nutrient fields)
- All optional except name
- Creates product with source="manual"
- Refreshes search after save
Button style:
- .add-btn: 0.5px border with primary color, rounded corners (6px)
- Min 40x36px for finger-friendly tapping on retina
- Flexbox centered content
Was: upper micro-strip (supplements only, scroll) + lower nutrient-strip
(food+supps, expand). Now: ONE strip at top that does both.
Collapsed: horizontal scroll, single row, no wrap.
Tap: expands to full wrap view showing all nutrients.
Tap again: collapses back to scroll row.
Data source: food (product micronutrients on-the-fly) + supplements.
Sorted by % RDI ascending (most deficient first).
Removed: buildMicroStrip() call, .micro-strip element, old scroll handler.
1. Manual food entry: added B/Zh/U fields. Creates custom product in DB
with user-provided KBJU, then logs it. Fixed addProduct lastInsertId.
2. Omega 3-6-9: columns in supplements, in supplement totals query,
in MICRO_FIELDS form (34 fields total now).
3. Product card micronutrients: searchProducts now fetches all micro
columns. renderProdDetails shows nutrient chips (sorted by deficiency)
when product is expanded.
4. Supplement form: added B5, B7, chromium, manganese, molybdenum,
iodine, phosphorus, omega-3/6/9, silicon, vanadium, lutein, lycopene.
5. Nutrient strip: tap to expand/collapse. Collapsed = 1 row (36px).
Tap = toggle full view showing all nutrients.
6. Seed DB v6: added "Тесто для чебуреков 500г" (261 kcal, 7.1/3.8/48.2).
RDI (bmr.js):
- 27 nutrients per TR TS 022/2011 Russian daily intake standards
- Exposed as window.BMR.RDI
Supplement columns (db.js):
- 12 new columns: B5, B7(biotin), D, K, Cr, Mn, Mo, I, P, Si, V, lutein, lycopene, carotene
- getSupplementTotalsForDate expanded to sum all new columns
Food nutrient display (main.js):
- Removed asterisk (*) from daily kcal (guessed tint stays)
- getFoodForDate now JOINs all micronutrient columns from products
- foodNutrientLine() shows "C 12%, Ca 5%, Fe 8%..." under each food item
- calculateRecipeNutrition expanded to 19 micronutrients with cooking loss
Daily nutrients section (main.js):
- "Нутриенты" section on Today screen between Добавки and Сон
- calcDailyNutrients() sums food (on-the-fly from products) + supplements
- Sorted by % RDI ascending — most deficient first
- Color coded: red < 30%, orange 30-80%, green >= 80%
Recipe detail:
- "Микронутриенты (на порцию, % РСП)" chips section
Idea 2 — Hill progress markers:
- goal_markers table, CRUD, map-based placement
- Session detail shows closest approach to each marker
- Progress comparison with previous sessions
Idea 6 — 7-day kcal balance widget:
- Rolling average below summary cards on Today screen
- Green = deficit, red = surplus
Idea 7 — Weight vs calorie correlation chart:
- Dual Y-axis: weight (left) + 7-day rolling balance (right)
- Added to stats section in renderStatsCharts
Idea 8 — Food cost of workout:
- Shows session kcal as portions of frequently eaten foods
- "460 kcal = 2.3 x гречка или 1.4 x творог"
Idea 1 — Tire pressure:
- tire_pressure_bar field in bikes table and bike modal
- Crr corrected by sqrt(3.0 / pressure), reference 3.0 bar
- Passed through all opts chains (recording, recovery, recalculate)
Idea 3 — Lap comparison:
- "Сравнить с..." button in session detail
- Shows sessions with same type and similar distance (+-30%)
- Overlay: current track green/red (faster/slower), comparison cyan dashed
- Nearest-neighbor speed matching between tracks
- Summary: time and speed difference
Idea 4 — Auto climb segments:
- computeStats returns segments[] with type (climb/flat/descent)
- Gradient > 2% = climb, < -2% = descent, else flat
- Min 50m per segment, adjacent same-type merged
- Stats per segment: distance, elevation, gradient, duration, speed
- UI section in session detail with colored icons
Idea 5 — Bike maintenance log:
- bike_maintenance table (date, description, mileage, next_service_km)
- Total mileage from GPS sessions per bike
- Overdue indicator when current mileage >= next_service_km
- Add/delete entries in bike modal
- CRUD functions on window.DB
Alternative food units (Phase 3a):
- serving_units table: шт, ст.л., ч.л., стакан, мл
- product_units junction: eggs linked to шт (60g)
- Unit selector chips in food modal after product selection
- Display "2 шт" instead of "120г" in food log when unit is set
- amount_g stays canonical, unit info is display metadata
Analytics (Phase 4):
- Stats section on Today screen with period selector (30d/90d)
- Weight trend chart (line, purple)
- Macros donut (protein/fat/carbs with kcal center text)
- Kcal balance bar chart (intake green vs expenditure red, includes BMR)
- Chart fullscreen + zoom support (reuses GPS chart patterns)
GPS improvements:
- Elevation smoothing: 5-point moving average + 1m dead zone
- Fixes inflated elevation gain (440m → ~70m for a real 70m route)
- Batch polyline update: poll loop suppresses per-point addLatLng,
calls onBatchUpdate to rebuild polyline from sorted points at once
- Eliminates visual jumps when app returns from background
Boot screen: removed verbose migration/GPS status labels,
shows "Загрузка..." only, updates text only during actual seed import
Recipes:
- Cooking methods table + 6 presets (boiling, frying, steaming etc.)
- Recipe editor with live KBJU calculation including cooking loss
- Recipe detail view, log-to-diary, ingredient search
- Split food log into "Products" and "Dishes" sections
- Products screen sub-tabs: Продукты / Рецепты
Nutrient enrichment:
- 1620 store products enriched with ION micronutrients (guessed flag)
- Purple tint on guessed food entries, * on daily kcal total
- Seed DB v4 with nutrients_guessed column
Seed import fixes:
- Rust: always refresh seed DB from bundled assets on launch
- Rust: check app_config_dir for main DB (plugin-sql stores there on desktop)
- Rust: ATTACH-based UPDATE to sync nutrients_guessed for existing products
- Rust: read all 27 columns including micronutrients
- Rust: emit seed_progress events + indeterminate progress bar animation
- JS: boot screen with status labels during migrations/import
- JS: nutrients_guessed column created in JS migration as safety net
Search:
- Results sorted by starts-with-query first, then by name length
- Default limit raised to 200
- Modal search results scroll without max-height cap
Bug fixes:
- product_id=0 for custom recipe ingredients (NOT NULL constraint)
- getRecipe JOIN for cooking_method_name
- calcTotals applies cooking loss from selected method
- Ingredient modal targets last overlay (not parent)
- select elements dark theme (color-scheme: dark)
- renderToday wrapped in try/catch
Products:
- 5ka scraper: recursive category BFS, 3103 products imported
- ION scraper (web.ion.ru): fetch-based, 1111 products with full vitamins/minerals
- Seed DB now 13480 products (magnit 8627, 5ka 3103, ion 1078, kdv 672)
- 13 micronutrient columns added: Na, K, Ca, Mg, P, Fe, vit A/B1/B2/B3/C/E, carotene
GPS fixes:
- computeStats sorts points by seq/ts before calculating (fixes wrong elevation/kcal)
- Trail restore on recording screen sorts points
- Old tracks with NULL seq repaired via migration (db.js)
- Activity type saved to localStorage, restored on GPS screen
UI:
- Track points invisible but tappable (popup with time/alt/speed)
- Charts: pointer-events:none on canvas when minimized (page scrolls)
- Navigation history stack (20 entries), back gesture doesn't close app
- Session list sort: added id DESC tiebreaker
GPS background recording:
- Batched writes (10s flush) in native service
- Polling lock prevents parallel processing
- Track by timestamp instead of line count
- Recovery idempotent via max DB timestamp
- Seq numbers on live recorded points
GPX export:
- Button in session detail
- SAF file picker on Android (ACTION_CREATE_DOCUMENT)
- GPS config in metadata (accuracy, minSpeed, minDist)
Charts:
- Pinch zoom enabled at creation (Hammer.js registers handlers)
- touch-action: pan-y blocks pinch in normal view (page scrolls)
- Fullscreen: CSS-only expand (no canvas manipulation)
- Downsampled to 500 points for smooth rendering
- Back gesture collapses fullscreen before closing modal
Build: --real flag finds first non-emulator device
GPX export: button in session detail, standard GPX 1.1 format.
Background GPS fixes:
- Native service: batched writes (10s flush) instead of per-point
appendText — eliminates race condition with reader
- JS reader: skip corrupt lines, sort by timestamp, track by ts
instead of line count — handles partial writes and reordering
- Recovery: idempotent import using max DB timestamp
- Live recording: seq numbers on every point
- FileObserver watches correct path (app root, not files/dev.baldav.app)
- Removed ACCESS_BACKGROUND_LOCATION (causes throttling)
Nav: "Трекинг" button on GPS screen when recording active.