Commit graph

57 commits

Author SHA1 Message Date
c70cc1e4a4 Record screen: LIVE + SMS as square buttons bottom-left; toggle live on the fly
- 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
2026-10-26 19:13:34 +03:00
738c4e81a1 Profile: render settings checkboxes as toggle switches
The .form-group input rule (width:100% + appearance:none) stretched every
checkbox into an empty full-width box. Add a .toggle class styling a checkbox
as a sliding on/off switch and apply it to the TEF, hydration and live-share
toggles in the profile modal.
2026-10-26 18:25:07 +03:00
d4dda3da0c Dark map: give the roads overlay the same invert/hue/brightness filter as tiles
Road colours were tuned for light tiles and blended into the inverted dark
tiles. Filter the roads pane identically: add the CSS rule for
.leaflet-roadsPane (record screen roads already use that pane), and on the
roads screen draw the polylines through a canvas renderer bound to a new
roadsPane so the same rule applies there too.
2026-10-19 19:26:20 +03:00
87a7fccfcc Record screen: remove the status line + its black bar, tighten button padding, move ⛔/↪ off the zoom controls
- Drop the status line (and any text it showed) from the record screen; keep
  a no-op statusEl sink so existing writes don't crash. Removes the empty
  black bar above the buttons. Tighter .rec-bottom padding.
- The nogo (⛔) and reroute (↪) buttons had no position class, so they piled
  at the map top-left over the zoom controls. Give them stack slots.
2026-10-17 14:43:57 +03:00
69b36be591 Record buttons equal width (border-box + uniform padding); HR strap charge as a background fill on the HR toggle
- Стоп was narrower: buttons had different horizontal padding and weren't
  border-box, so flex-basis:0 didn't equalize them. Force box-sizing + one
  padding on all three .rec-bottom buttons.
- HR strap battery now paints the HR toggle's background bottom-up 0-100%
  (--bat), coloured green/amber/red by level (setHrBatteryFill), instead of
  a number; cleared when HR is toggled off.
2026-10-17 14:35:54 +03:00
e88c2756f7 Record screen: big digits in the top strip (match bottom), drop the black status-bar backdrop band and the extra control offset
Top strip time/temp/clock now use the same 27px bold as the bottom stats. The
old .rec-map-wrap::before status-bar backdrop (and the leaflet-top safe-area
margin) are gone - the top strip already backs the status bar, so that strip
showed as a black band between the bar and the map.
2026-10-17 14:30:42 +03:00
dffadf3cd8 Record screen: regroup stats (top strip time/temp/clock, bottom HR/dist/speed/alt), equal-width buttons; reset stale HR per session
1) Layout: a top strip shows training time | temperature | clock ('external'
   things); the bottom panel row is now HR, distance, speed, altitude (bigger);
   the separate HR bar is gone; the three controls (HR toggle, pause, stop)
   share the width equally instead of clustering right.
2) Clear currentHr/currentBleData at recording start (+ ble_clear_hr) so a
   stale bpm from the previous session no longer hangs on the display or gets
   written onto this session's points; show -- until a fresh reading.
2026-10-17 12:29:14 +03:00
0a4ffcb49d HR toggle: white icon+bpm on solid red when active (contrast)
Active state was red text on faint-red; use white on solid red.
2026-10-17 10:59:31 +03:00
553a4fcf80 Record screen: show the live HR number on the HR toggle button
The HR toggle (ex-wind button) showed only the heart+pulse icon. Now the
current bpm renders on it (onHrUpdate writes #rec-hr-num); it clears when HR
is toggled off, leaving just the grey icon.
2026-10-17 10:57:08 +03:00
f7ba059628 HR: auto-connect at recording start if a strap is paired; HR on/off toggle replaces the wind button
Earlier I over-corrected: 'don't scan when the strap is absent' became
'never auto-connect'. Restore conditional auto: at recording start (ride or
workout) connect the paired strap and poll; absent strap is safe now
(BleHrService scans briefly and stops - no drain). No paired strap -> HR off.

- gps.js: GPS.setHrOn(on) - connect+poll / disconnect+stop; start auto-ons it.
- Record screen: the inconvenient 'Ветер' button is gone; in its place an
  HR on/off toggle (square, heart+pulse icon; grey off, red on) -> setHrOn.

(Also corrected a stale assumption: the phone DOES have Google services.)
2026-10-17 01:45:02 +03:00
f704498d11 Workout mode: HR-only recording (зарядка) without GPS
New 'Зарядка' activity type records a session with no GPS - no location
service, no watchPosition, no track points - just the heart-rate poll and a
timer, for indoor exercise. kcal comes from HR (Keytel gross minus BMR, net
like the rest of the app) and counts in the daily balance.

- gps.js: GPS.hrKcalNet; startRecording/stopRecording 'workout' branches;
  the HR poll appends [relSec,bpm] samples.
- db.js: gps_sessions.hr_samples column + setSessionHrSamples.
- main.js: 'Зарядка (пульс)' option; renderWorkoutInline (map-less live
  screen: big HR, timer, kcal, connect-sensor button); renderWorkoutDetail
  (HR chart by zones, load, avg/max).
- tools/hr_log.py: background HR logger (standard 0x180D, no pairing).

Sensor read from the desktop confirmed on a Coospo H808S (F7:72:74:A4:FB:07,
~1 Hz) without pairing.
2026-10-17 00:54:21 +03:00
3e2a6e47f7 Progress on recompute/refine/roads-rebuild + remember map zoom
- .btn-progress fills a button's bg by --pct (setBtnProgress). Elevation
  refine drives it from fetchElevations network batches (0-80%) then save
  (85-100%); recompute from recalculateSession phase milestones; roads
  rebuild from a new Rust roads_progress event (rebuild_roads emits
  [current,total] every ~2000 points - first Rust->JS event in the app,
  adds use tauri::Emitter) so it no longer just hangs silently.
- recalc/refine now reopen the detail via reopenDetail (awaits the
  popstate) instead of the manual pop+history.back+render race.
- Track detail remembers its map view per session (_sdMapView, moveend) and
  restores it on reopen/return instead of re-fitting the whole track; the
  recording map persists its zoom in localStorage across screen switches.
2026-09-29 23:19:37 +03:00
d6fe47a924 Recording map: follow toggle to free-pan while riding
New ⊕ button on the recording screen toggles map auto-follow. When off,
the map stays put so you can look around when unsure where you are (off
the track, or the track leads where there is no road); new GPS points no
longer recentre it. Tapping it back on recentres on the current position
and resumes following. Default on per screen entry (not persisted).

Guards followMode around panTo/setView in onPoint/onBatchUpdate/
onStateChange. Independent of the compass freeze/rotate controls.
2026-09-26 23:44:18 +03:00
15a2881c6d Split a track at a point; block merging tracks with different bikes 2026-09-21 12:36:03 +03:00
cfadad4690 Merge rides: long-press to multi-select tracks and combine them into one (reversible via orig_session_id) 2026-09-18 00:08:25 +03:00
7717a73129 Repeat/interval analysis: auto-find repeated segments in one ride, table + last/first verdict 2026-09-17 22:11:10 +03:00
4f31088af9 Track detail: fold the segment list into an accordion so action buttons stay reachable 2026-09-14 13:27:29 +03:00
d9a3254ffb Share image: pick which fields go on the picture (remembered), add max speed 2026-09-14 13:07:32 +03:00
69f6fdac18 Share image (Похвастаться): render + preview modal + detail button
Stage 1 (pure JS). buildShareImage() draws a 9:16 story PNG: track shape (colored
glowing line), big Distance/Time/Speed + small elevation/kcal/date, on a
transparent or photo background. showShareModal() previews it with a
transparent/photo toggle and Share/Save buttons; a 'Похвастаться' button in the
track detail opens it. Native export (SAF save + share intent) and the settings
background picker come in Stage 2 (needs an APK rebuild); on desktop the export
buttons toast an error.
2026-09-14 10:25:34 +03:00
e45c92f09a Route editor + gap-fill: 6 fixes (casing/slope, controls, tap-insert, waypoints, precise fill)
- 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).
2026-09-11 01:06:06 +03:00
cfe0212bbc BRouter UX: gap-fill preview+reversible save; per-segment route planner
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.
2026-09-10 15:26:05 +03:00
8e720ecd17 BRouter: offline route planning by waypoints + segment download UI
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.
2026-09-09 17:33:11 +03:00
d078db7288 POI overlay on maps: circular contrasting icons + category filter (offline)
Points of interest (cafe, shops, water, toilets, bank/ATM, bus, shelter,
viewpoint, ...) as circular contrasting badges (emoji in a white/dark circle,
readable on light and dark tiles) — not pins. A 📍 control toggles the layer
and opens a per-category filter (checkboxes, remembered). Markers are culled to
the visible bounds and shown from zoom 13 (cap 500/view), on the recording,
track-detail and roads maps.

Data is bundled and fully offline: frontend/data/pois.json ([{la,lo,c,n}], 358KB,
5662 POIs) fetched once from Overpass for ~100km around 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.
2026-09-09 16:51:58 +03:00
375fb0a7ab Map: dark backdrop + reliable tile caching
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.
2026-09-09 13:46:22 +03:00
5c23c4dda4 Map: dark-map toggle (invert tiles only), no repacking
A moon button on the recording, track-detail and roads maps inverts ONLY the
tile layer: body.dark-map applies `invert(1) hue-rotate(180deg) brightness(1.5)`
to .leaflet-tile-pane, so the track/route/roads/puck overlays keep their colours.
Works the same on offline (tiles_*.db) and online tiles — no keys, quotas or
tile repacking. The choice persists in localStorage (baldav_dark_map).

Chosen after ruling out dark tile providers (CARTO/Thunderforest/2GIS/Yandex —
keys/quotas/ToS/projection/RF-reachability) and the transport style (inverts
badly in dense city); plain OSM inverted looks good city and countryside.

Verified on device: filter on the tile pane only, overlay pane filter none,
button active and remembered.
2026-09-09 13:33:05 +03:00
34927e3f52 rec screen: status-bar backdrop so the system bar stays visible over the map
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.
2026-09-07 17:15:19 +03:00
1b10aa67de Wind: "headwind in the face" label button on the recording screen
Variant 5 from the findings doc, as GROUND TRUTH for validating the
from-track wind estimate — it does NOT feed kcal/power. Chosen scope
(per user): a toggle button now, binary "in the face" only, voice later.

- rec screen button "Ветер в лицо": tap opens a headwind interval, tap
  closes it; the open state is highlighted and survives a screen re-render
- GPS.toggleWindMark()/_closeOpenWindMark(); recording.windMarks =
  [{startTs,endTs}] ms, persisted to localStorage, written to the DB on
  finish and restored on crash recovery (a dangling interval auto-closes)
- gps_sessions.wind_marks (JSON) + DB.setSessionWindMarks (separate from
  finishGpsSession so recompute paths preserve marks); parsed in getGpsSession
- rec-bottom wraps now so the third button never overflows a narrow screen

Analysis (marks vs estimate) stays in tools/track once enough km accumulate.
2026-09-07 17:10:16 +03:00
3ca2bc00c0 Chestny Znak scanner: bundle offline ZXing decoder, ЧЗ badge, session auto-push
- 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.
2026-08-27 04:31:58 +03:00
493c9b92df Recording map: navigation puck (position+direction), z-order panes, compass-only mini-compass
- One direction indicator instead of two geo-arrows: an SVG divIcon L.marker
  (rotateWithView) — a track-colored circle (20% darker) with white + 1px black
  rings, plus a triangle pointing the ACTIVE source heading (compass in compass
  mode, GPS course in GPS mode). Rotation = geo heading; rotateWithView keeps it
  correct as the map turns. Replaces posMarker + compass/course arrows.
- Z-order via panes: roads -> roadsPane(240), route routePane(250), track
  overlay(400), puck markerPane — tiles<roads<route<track<puck (track no longer
  hidden under roads).
- Mini-compass shown only in compass mode; tap-to-calibrate kept.
- darkenHex helper; .rec-puck css (no default div-icon box).
2026-08-24 23:06:26 +03:00
bf3c0e2534 Track detail de-jitter: fix overlay stacking, native dialogs, slow save
Fixes to the smoothing/routes UI reported on device:
- Blank detail after save/revert: showSessionDetail was called from inside the
  open detail modal, stacking a second overlay; querySelector("#sd-map") then
  hit the stale, already-initialized Leaflet map and threw, aborting the rest
  of the render (map + speed chart). New reopenDetail() closes the current
  detail (and its fullscreen-map child) and reopens fresh, like recalc/refine.
- Native confirm()/prompt() replaced with in-app appConfirm/appPrompt dialogs
  (winStack + history integrated, hardware back cancels). Used for smoothed
  save, route delete, route rename.
- Slow smoothed save: one UPDATE per point (4000+ IPC round-trips) replaced
  with a chunked CASE update — 18 statements for 4366 points, params kept
  under SQLite's 999 limit. Verified value-for-value on a copy DB.
- Closing the fullscreen map now also closes the smoothing panel (it is a
  fixed element that otherwise lingered over the collapsed map).
- "≈" button active state now reflects panel-open only, not session.smoothed
  (it looked permanently pressed after a save).

Docs: README + PROJECT_MAP updated for saved routes, de-jitter, accuracy_m,
routes table and new gps_points/gps_sessions columns. Version 0.1.393.
2026-07-31 23:51:20 +03:00
9d09b4c4a4 Saved routes overlay + per-activity road colors + reversible track de-jitter
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.
2026-07-31 22:11:55 +03:00
2051a604e1 GPS accuracy-gated points + bearing; fix startup UI freeze
- Native: hard-reject fixes coarser than the Min-accuracy setting (drops
  coarse cell ~800m that stuck the track); remove low-accuracy and adaptive
  relaxation branches so only accurate fixes are recorded, honest gaps otherwise
- Store bearing_deg per point (gps_points migration + addGpsPoint)
- Default Min-accuracy 20 -> 50m (one-time migration, respects user override)
- Fix: http_request was synchronous with no timeout; an unreachable host
  (LAN sync on startup) blocked the whole UI ~30s. Now async + spawn_blocking
  + connect(4s)/global(12s) timeouts, fails in background without freezing
- Bundled prior work: course-up map orientation, compass calibration,
  online elevation refine (open-elevation), distance gap-gate fix
2026-07-07 00:28:27 +03:00
50ec5c21f8 Health-diet.ru products (24K), GPS session safety, build.sh upload fix
Products:
- health-diet.ru scraper (Tampermonkey) + import script
- 11469 products with 37 nutrients, Dodo 258, McDonalds 619, KFC 361, Ashan 2000
- Rust import_seed_products: dynamic column list, auto-create missing columns
- Seed version bumped to 10

GPS session safety:
- Separate gps_points_{ts}.jsonl per session, deleted only after DB save
- Toast on DB errors, NaN guard in finishGpsSession

Other:
- build.sh: router/VPS upload only with -u flag
- Orientation buttons bottom-right, weather via Yandex Pogoda
2026-06-28 23:17:51 +03:00
4ed6e6f039 Activity types (squats/jumprope/stretch/swim), road map, tile import, fixes
Activity types:
- New types: squats, jump rope, stretching, swimming pool
- MET-based kcal auto-calculation (6.0/12.3/2.3/8.0)
- Dynamic modal fields per type (sets/reps, distance, minutes)
- Fix: state.latestWeight is object {weight_kg}, not number

Road map:
- road_points table with 2m dedup, activity type (bike/walk)
- Rust rebuild_roads + get_road_points (bypass SQL plugin 28K limit)
- Segment detection by 100m gap, bike=blue walk=brown

Tile system:
- CSP fix: a.tile.openstreetmap.org matches *.tile pattern
- pack_tiles.js tool, import from file/router, ureq body limit bypass
- Tile errors: done(null,tile) prevents crash cascade

Other fixes:
- Manual routes: skip gap segmentation (synthetic timestamps)
- Draw route: own CSS class, not affected by landscape rotation
- Orientation lock button (A/L), hysteresis 65/35 degrees
- GPS placeholder only when state=waiting_gps
- Food amount inputmode=text for formula support (4*30)
- showModal onMount callback
2026-06-26 21:04:21 +03:00
184b86b18c Tile import, road map, orientation lock, CSP fix, tile error handling
- Tile import: pack_tiles.js (PNG→SQLite), Rust import_tiles + download_file
  commands, JS importFromSqlite, UI in settings (file + router download)
- Road map: road_points table with 2m dedup, rebuild_roads + get_road_points
  Rust commands (bypass Tauri SQL plugin 28K row limit), bike=blue walk=brown
- Orientation lock button (A/L) on recording map, hysteresis 65/35 degrees
- CSP fix: tile URL a.tile.openstreetmap.org (matches *.tile CSP pattern)
- Tile errors: done(null, tile) instead of Error to prevent crash cascade
- GPS placeholder: only show when state=waiting_gps (not on screen re-enter)
- ureq body size: use into_reader() to bypass 10MB limit for tile downloads
2026-06-26 16:41:14 +03:00
704c0892ec Landscape panel on the right side, nav hidden, GPS placeholder fix
- 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
2026-06-25 18:23:58 +03:00
fda820568e Compass map rotation, landscape layout, GPS placeholder
- 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
2026-06-25 16:54:45 +03:00
7c93bb75c3 OTA updates, BLE HR, water tracking, GPS altitude filter, BMR fix, nutrient units
OTA: VPS nginx endpoint, build.sh -u upload flag, Kotlin check + browser download
BLE HR: FitPro protocol (PAIR/CCCD/HR_SWITCH/REALTIME/MEASURE), GATT queue,
  ACK responses, auto-detect device type, auto-start with GPS, re-trigger 25s
  Kotlin BleHrService, btleplug for desktop, file IPC for Android
Water: products.water_pct column, getDailyWaterMl(), progress bar on today screen
GPS altitude: outlier spike filter (>4m), stuck run interpolation (5+ identical),
  configurable smoothing (altSmoothHalf setting), filtered raw for kcal calc
BMR: removed broken estimateRealBmr, added activity level dropdown (1.2-1.725),
  fixed 7-day trend using dailyBase instead of raw bmr
Nutrients: ginseng/resveratrol/coq10 columns, fixed folate/selenium units (removed
  mult:1000), iodine/chromium input in mcg, addSupplement now inserts all 41 columns
UI: parseAmount() for 4*30 expressions, recipe portion amount field, nutrient
  reserves strip, GPS sessions in today view, pan on charts, showToast(),
  HR norms in settings, HR on recording screen, HR chart in track details
Scrapers: 5ka Load JSON button, magnit_scraper.user.js (NUXT_DATA parsing)
2026-06-07 01:36:38 +03:00
45d3736a99 Chart zoom 2x faster, chart padding, clearer food cost format
Charts:
- Zoom speed: 0.02 -> 0.04 (all 5 chart instances)
- stats-chart-block: added padding 0 14px to match content margins

Food cost of workout:
- Was: "1.8 x Хлеб (102г, 273 кк)" — ambiguous "1.8 x"
- Now: "1.8 порц. Хлеб (по 102г, 273 кк/порц.)" — clear units
- Header: "485 ккал — это как:" instead of "485 ккал ="
- Math: 485/273 = 1.78 portions of 102g each

Duplicates: seed DB has none (verified). Device dupes from pre-dedup
imports — JS migration handles them on next launch.
2026-06-01 13:51:47 +03:00
8d9b5aaf7e Source badges on products: Магнит (red), 5ка (green), KDV (orange), ИОН (gray), Свой (yellow)
- 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)
2026-05-30 15:59:50 +03:00
7b41382ea4 Add-btn border: gray (--border) instead of primary 2026-05-29 20:30:36 +03:00
f871e5bb28 Add custom product with full nutrients, bigger + buttons with border
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
2026-05-29 18:21:49 +03:00
818276d60c Merge two nutrient strips into one: scroll collapsed, wrap expanded
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.
2026-05-29 14:55:59 +03:00
de5cfd5c21 Manual KBJU input, omega-3/6/9, product micronutrients, nutrient strip expand
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).
2026-05-29 14:47:09 +03:00
885ef27f36 RDI table, nutrient % in food/recipes, daily deficiency strip
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
2026-05-29 13:38:14 +03:00
9384bb8d2e Goal markers, 7-day kcal balance, weight-calorie correlation, food cost
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 творог"
2026-05-27 17:45:50 +03:00
1d6efcd851 Tire pressure Crr, lap comparison, auto climb segments, bike maintenance
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
2026-05-27 16:45:33 +03:00
e4f3d3e3ca Alternative units, analytics charts, elevation smoothing, batch GPS update
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
2026-05-23 17:12:19 +03:00
049f4d6546 Unified product search dropdown, UI fixes
- Reusable createProductSearch() component used in food and ingredient modals
- Search dropdown: position:absolute, z-index:100, shadow, scrollable
- Products tab: list scrolls independently (max-height: calc(100vh - 209px - safe areas))
- prod-card flex-shrink:0 prevents collapse in scroll container
- Products tab reverted to normal list (not dropdown) with expanded cards
- Search limit raised to 200, sorted by starts-with-query first
- Ingredient modal: uses createProductSearch instead of manual wiring
- Food modal: uses createProductSearch instead of manual wiring
2026-05-21 00:48:27 +03:00
3e5333cec6 Recipes, nutrients_guessed, seed import fixes, search improvements
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
2026-05-21 00:06:50 +03:00