baldav/PROJECT_MAP.md
4aiman 7d85773ae3 BRouter: fix routed-line re-route loop; verify planning + gap-fill on desktop
doBrouter() ended with changed(), which nulled routeGeom and re-queued
doBrouter — an endless re-route loop where the road line was never drawn
(editor kept the straight line, distance unchanged). Draw directly instead
(updatePolyline/updateStats).

Verified end-to-end in the UI on a virtual display (Xvfb) against the
RouteServer: editor 'По дорогам' turns a 1.69km straight line into a 2.25km
road-following route; the gap-fill '🧩' overlay draws a shortest-road cyan
dashed line across a track's GPS dropouts. Docs (README/PROJECT_MAP/plan/log)
updated to match; router.js/poi.js now listed in PROJECT_MAP.
2026-09-09 18:18:35 +03:00

265 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PROJECT MAP — BalDav
Трекер здоровья и активности. Pure JS + Tauri 2 + SQLite. Без фреймворков и бандлеров.
## Статистика
| Метрика | Значение |
|---------|----------|
| Строк кода | ~22000 |
| Файлов исходников | 50+ |
| Язык | JS 70%, Rust 6%, Kotlin 12%, CSS 12% |
| Таблиц в БД | 21 |
| Экранов | 7 основных + 15 модалок |
| IPC команд (Tauri) | 14 |
| Библиотеки | Leaflet, Chart.js, Hammer.js, JSZip |
| Платформы | Android (Kotlin), Desktop (Tauri) |
## Структура файлов
### Frontend (`frontend/`)
| Файл | Строк | Назначение |
|------|-------|------------|
| `js/main.js` | ~5900 | Экраны, навигация, модалки, графики, нутриенты, HR UI, компас (EMA + gimbal lock), ландшафт (3 режима), дороги. `appConfirm`/`appPrompt` (in-app диалоги вместо нативных, в стеке окон). Страница «Сохранённые маршруты», оверлей маршрута на записи и просмотре (`whenMapReady` — await готовности карты, не таймаут). Панель де-джиттера на детали трека (ползунки окно/итерации, живое превью, сохранить/откат, `reopenDetail` — переоткрытие детали без наложения оверлеев). `showRouteDetail` — предпросмотр маршрута (карта+профиль высоты+тап-высоты+фуллскрин), `showRouteEditor` — создание/правка маршрута (тап карты=точка в конец, тап линии=вставка в сегмент, тащить=двигать, тап точки=удалить; `makeMarker` возвращает маркер для push/splice). `appConfirm`/`appPrompt`/`appAlert` — in-app диалоги (нативных `alert/prompt/confirm` не осталось; `<select>` оставлены). `orient-dbg` (сенсор-оверлей на карте) — только при `window.__BALDAV_DEBUG`. `drawRoutePolyline`/`routeSlopeColor` (полосы уклона по 1%)/`routeElevColor` — топо/уклон-обводка в панели `routePane` (маршрут ПОД треком; на rotate-карте панель вешается внутрь `_rotatePane`, иначе маршрут не виден при записи). `fetchElevations` — цепочка GPXZ→open-elevation→open-meteo→OpenTopoData. Диагностические `console.log("[тег]...")` глушатся в release по `get_app_info().debug` (cfg!(debug_assertions)) |
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints` — сохранить высоты, `getRoutes.hasEle`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера |
| `js/gps.js` | ~1650 | GPS запись, ккал (нетто-модель + MET-нетто floor, накат=базовый), altitude filter + гистерезис высоты, BLE HR polling, dead reckoning. Источник истины БД при живой записи — нативный файл (`_processIncomingPoint(p, fromNative)`: watchPosition только рисует, поллинг пишет). `recoverFromFiles` — восстановление точек с устройства (матч по id/дате + `_s<id>` в имени, дедуп ±2.5с, `recalculateSession(noDelete)`, чистка импортированных файлов). `recomputeAllStats` — пересчёт статы всех треков без изменения точек. `filterByAccuracy` — отбор по точности (minAccuracyM) внутри `computeStats`+`filterNoise`: неточные фиксы исключаются из расчёта/линии (обратимо, non-destructive; null/≥999 keep), bearing_deg. `smoothTrackSG(pts,W,it)` — де-джиттер Савицкого-Голея (квадратичный МНК по окну, итеративно, концы закреплены; lat/lon→метры→обратно, все поля точки сохраняются). `parseRouteGPX(xml)` — парсер маршрутов (`rtept`/`trkpt`/`wpt`, читает `<ele>`, DOMParser). `routeElevStats(pts)` — профиль маршрута (накопл. дистанция, грейд к след. точке, набор/сброс с гистерезисом, макс-уклон; высоты сглаживаются). `config.gpxzKey` — ключ GPXZ (localStorage) |
| `js/sync.js` | ~300 | Синхронизация между устройствами (LAN, PHP сервер на роутере) |
| `js/chzn.js` | ~330 | Честный знак (CRPT). Скан DataMatrix → карточка Нацкаталога → префилл КБЖУ. `ChZ`: сессия в localStorage (`chzn_session`), `importFromText`, `refresh`/`ensureSid` (sid 48ч, refreshToken ~год), `authedPost/Get` (userId+sid в query, авто-refresh на 401), `check`, `nutritionInfo` (витамины, часто 204), `normalizeCode` (реконструкция GS 0x1D перед AI 93), `parseCard` (группа «Энергетическая ценность…», имена атрибутов разнятся у производителей), `resolve`. `/authorize/anonymous` закрыт Play Integrity — свою сессию не завести, только импорт из приложения (`tools/chzn_export.sh`) |
| `js/tilecache.js` | 178 | Оффлайн кэш тайлов карты (IndexedDB) |
| `js/router.js` | ~80 | Оффлайн-роутинг BRouter. `BRouter.route(points,[profile])`: Android → встроенный движок (`invoke("brouter_route")`), иначе/при ошибке → PC-RouteServer по HTTP (`http_request`, мимо CSP; LAN 192.168.1.18:17777 / WAN 77.66.192.45:17777). Профили `trekking` (планирование) / `shortest` (гэпы). Докачка сегментов `.rd5`: `downloadTile/downloadStatus/listTiles/downloadTileAndWait` |
| `js/poi.js` | ~200 | POI-оверлей на картах: круглые контрастные бейджи (эмодзи в круге, не пины), фильтр категорий (`POI.attach(map)`, данные `data/pois.json`, localStorage `baldav_poi_cats/shown`) |
| `js/bmr.js` | ~70 | BMR (Mifflin-St Jeor), РСП из БД (33 нутриента), полураспад |
| `css/styles.css` | ~2100 | Темная тема, мобильный дизайн, HR зоны, ландшафтный режим |
| `index.html` | 28 | Точка входа, порядок загрузки скриптов |
| `img/gps-placeholder.png` | 63KB | Пиксельарт заглушка при поиске GPS |
| `data/products_seed.db` | ~9MB | 13008 продуктов с меткой источника (magnit 7690, 5ka 3354, ion 1079, kdv 667, gridnev 217, manual 1). Импорт без плашки (HD/ashan/фастфуд) вычищен, seed_meta.version=11. Поиск — `searchProducts` порядко-независимый (AND по токенам). Чистка каталога: `cleanupUnlabeledProducts` (неиспользованные non-keep) |
**Библиотеки** (`lib/`): leaflet.js (1.9.4), leaflet-rotate.js (компас), chart.min.js, chartjs-plugin-zoom.min.js, hammer.min.js, jszip.min.js, zxing.js (@zxing/library UMD — офлайн-декодер DataMatrix для ЧЗ-сканера; нативный `BarcodeDetector` требует Google Play Services и падает "Barcode detection service unavailable" на de-Googled ROM)
### Бэкенд Rust (`src-tauri/`)
| Файл | Строк | Назначение |
|------|-------|------------|
| `src/lib.rs` | ~790 | Миграции v1, IPC команды, seed import, update check, tile import, road map rebuild. `native_root()` — выбор корня приложения по наличию `gps_live_points.jsonl`/`gps_points_*` (на этом Tauri `app_data_dir()` = корень напрямую; старый `.parent().parent()` читал мимо, отсюда потеря фоновых точек). `http_request` (GET/POST, тело) и `chzn_http` (браузерный User-Agent + возврат `{status,body}` для 204/401/427, `http_status_as_error(false)`) — прокси HTTP мимо CORS WebView |
| `src/ble.rs` | ~470 | BLE HR: btleplug (desktop), file IPC (Android), FitPro + Standard HR |
| `src/main.rs` | 5 | Точка входа |
| `Cargo.toml` | — | tauri, tauri-plugin-sql, rusqlite, serde, btleplug, tokio, uuid |
| `tauri.conf.json` | — | Конфиг приложения, плагины, ресурсы |
### Android Kotlin (`src-tauri/gen/android/.../dev/baldav/app/`)
| Файл | Строк | Назначение |
|------|-------|------------|
| `MainActivity.kt` | ~300 | FileObserver IPC, seed DB, safe insets, SAF, BLE commands, OTA check |
| `GpsRecorderService.kt` | ~530 | Foreground service GPS, WakeLock, JSONL (один файл на сессию до Стоп, идемпотентный onStartCommand при START_STICKY-перезапуске ОС; `bindSessionIfPending()` дописывает id сессии в имя файла `_s<id>` по `gps_session.txt`), 50Hz акселерометр/гироскоп, dead reckoning, raw sensor files |
| `BleHrService.kt` | ~340 | Foreground service BLE HR, FitPro protocol, GATT queue, scan/connect |
| `generated/*.kt` | ~1100 | Tauri-сгенерированные: WebView, IPC, permissions |
### Инструменты (`tools/`)
| Скрипт | Назначение |
|--------|------------|
| `5ka_scraper.user.js` | Tampermonkey скрейпер 5ka.ru (BFS категорий, Load JSON) |
| `magnit_scraper.user.js` | Tampermonkey скрейпер magnit.ru (NUXT_DATA парсинг) |
| `ion_scraper.user.js` | Tampermonkey скрейпер web.ion.ru (fetch, без перезагрузок) |
| `import_ion.js` | Импорт ION продуктов в seed DB |
| `import_with_dedup.js` | Импорт с дедупликацией по имени |
| `enrich_nutrients.js` | Обогащение микронутриентами из ION |
| `parse_magnit.js` | Парсер HTML каталога Магнит |
| `parse_kdv.js` | Парсер HTML каталога KDV |
| `download_tiles.js` | Скачивание тайлов карт для оффлайна |
| `pack_tiles.js` | Упаковка PNG тайлов региона в SQLite для импорта |
| `healthdiet_scraper.user.js` | Tampermonkey скрейпер health-diet.ru (КБЖУ + 37 нутриентов) |
| `import_healthdiet.js` | Импорт health-diet.ru JSON в seed DB (авто-бамп версии) |
| `fetch_openfoodfacts.js` | OpenFoodFacts API |
| `fuzzy_dedup.js` | Нечёткая дедупликация |
| `steep_segments.js` | Крутые участки вело-треков → GPX + brouter lonlats. Тянет `baldav.db` (run-as), уточняет высоты online (`--gpxz` Copernicus GLO-30 по ключу, дедуп по ~33м ячейкам + кэш; иначе open-elevation), сглаживание по дистанции, уклон на базе (`120`) с барьером GPS-гэпов, окно-ядро steepest + пространственный дедуп мест. Флаги: `--gpxz/--local/--gps`, `--min= --drop= --dry` |
| `chzn_export.sh` | Экспорт анонимной сессии приложения «Честный знак» с рут-девайса (adb) → `chzn_session.json` для импорта в BalDav. Читает `com.qrbonus.xml`, проверяет живым `refresh` |
### Корень
| Файл | Назначение |
|------|------------|
| `build.sh` | Сборка: android/debug/release, sync frontend, версионирование, -u upload на VPS |
| `BUILD_NUMBER` | Автоинкремент номера сборки |
| `ion_products.json` | 1111 продуктов ION (сырые данные скрейпера) |
| `5ka_products_fresh.json` | 3103 продукта 5ka (сырые данные) |
### Разборы (docs/)
| Файл | Назначение |
|------|------------|
| `docs/gps-model-findings.md` | Находки по расчётной модели GPS (Crr/ккал/высота/ветер) — часть внедрена, см. лог 2026-09-07 |
| `docs/brouter-findings.md` | Разбор BRouter 1.6.3 (оффлайн-роутинг): данные `.rd5`, профили, малый набор высоты, 3 способа вызова + встраивание; под идеи «заполнение GPS-пропусков» и «оффлайн-планирование по точкам». Рекомендация — встраивание движка (jar 356 КБ) в Android-модуль |
## База данных
### Таблицы (Rust v1 миграция + JS миграции)
| Таблица | Назначение | Ключевые поля |
|---------|------------|---------------|
| `profile` | Key-value настройки | key, value |
| `products` | Продукты (13480) | name, kcal/protein/fat/carbs per 100g, витамины, минералы, nutrients_guessed |
| `food_log` | Дневник еды | date, time, product_id/recipe_id, amount_g, unit_id, unit_amount, ккал/БЖУ |
| `recipes` | Рецепты | name, cooking_method_id, servings, notes |
| `recipe_ingredients` | Ингредиенты рецептов | recipe_id, product_id, custom_name, amount_g, cooking_method_id |
| `cooking_methods` | Способы готовки (6 шт) | name, потери витаминов/БЖУ в % |
| `serving_units` | Единицы измерения (5 шт) | name (шт/ст.л./ч.л./стакан/мл), grams_equivalent |
| `product_units` | Привязка единиц к продуктам | product_id, unit_id, grams_per_unit |
| `weight_log` | Лог веса | date, time, weight_kg |
| `sleep_log` | Лог сна | date, bed/asleep/wake time, nap_minutes |
| `activity_log` | Лог активности | date, type, minutes, kcal, activity_type (squats/jumprope/stretch/swim/manual), sets, reps, distance_m |
| `supplements` | Добавки (БАД) | name, daily_dose_g, витамины/минералы |
| `supplement_log` | Приём добавок | date, supplement_id, dose_g |
| `gps_sessions` | GPS тренировки | date, activity_type, distance, kcal, speed, elevation, bike_id, smoothed, smooth_window, smooth_iters (де-джиттер), pauses (JSON ручных пауз), wind_marks (JSON меток «ветер в лицо») |
| `gps_points` | Точки трека | session_id, ts, lat, lon, altitude_gps_m, altitude_srtm_m, speed_ms, seq, bearing_deg, accuracy_m, lat_smoothed, lon_smoothed (обратимый де-джиттер), heart_rate, spo2, bp_sys, bp_dia, accel_rms/max, gyro_pitch/yaw |
| `routes` | Сохранённые маршруты (оверлей навигации) | name, color, points_json (`{lat,lon,ele?}`), created_at |
| `bikes` | Велосипеды | name, weight_kg, wheel, звёзды, surface_type, is_active |
| `rdi_standards` | Стандарты РСП (RU/EU/US) | code, name |
| `rdi_values` | Нормы нутриентов | standard_id, nutrient_key, value, norm_type (rdi/aup) |
| `nutrient_halflife` | Полураспад нутриентов | nutrient_key, halflife_avg_days |
| `nutrient_reserves` | Запасы нутриентов по дням | date, reserves (JSON) |
| `road_points` | Дедуплицированные точки всех треков | lat, lon, atype (bike/walk) |
### Seed DB (products_seed.db)
| Источник | Продуктов | Микронутриенты |
|----------|-----------|----------------|
| magnit | 8627 | нет |
| 5ka | 3103 | нет |
| ION | 1078 | витамины + минералы |
| kdv | 672 | нет |
| **Всего** | **13480** | 1398 обогащены (nutrients_guessed) |
## Экраны и навигация
### Нижний навбар (5 табов)
```
[Сегодня] [Продукты] [Трек] [Добавки] [Настройки]
```
### Экраны
- **Сегодня** — баланс ккал, еда (продукты + блюда), GPS тренировки, активность, вес, сон, добавки, прогресс-бар жидкости, нутриенты (приход + запасы)
- **Продукты** — подтабы [Продукты | Рецепты], поиск, карточки с КБЖУ, добавление в дневник
- **Трек** — GPS тренировки (запись с HR), карта с компасом (leaflet-rotate). Навигационный «пак» позиции (SVG divIcon): круг цвета трека (на 20% темнее) с белой + 1px чёрной обводкой + треугольник, указывающий ВВЕРХ (карта и так доворачивается активным источником — курс/компас — вверх, поэтому без покадрового поворота: не трясёт). Мини-компас только в режиме компаса. Z-order панелями: тайлы < дороги(`roadsPane` 240) < маршрут(`routePane` 250) < трек(overlay) < пак(marker). course-up по GPS-курсу (карта и панель доворачиваются в ландшафт вместе, `bearing = h`), ландшафтный режим (CSS rotation), графики скорости/высоты/ЧСС, импорт GPX/FTB, dead reckoning, placeholder до GPS, карта дорог (дедуп 2м, цвета по вело/пешие/бег), сохранённые маршруты (оверлей-навигация, кнопка-тоггл «М», предпросмотр с профилем высоты, топо/уклон-обводка, тоггл «∡/⛰», высоты по цепочке GPXZ→open-elevation→open-meteo→OpenTopoData), де-джиттер трека (Савицкий-Голей, кнопка «≈» на детали)
- **Добавки** — 41 нутриент (витамины, минералы, жирные кислоты, женьшень, ресвератрол, CoQ10)
- **Настройки** — профиль, образ жизни, нормы ЧСС/давления, BLE пульсометр, велосипеды, GPS конфиг (в т.ч. цвет трека `trackColor`, цвета дорог, GPXZ-ключ), OTA обновления, импорт тайлов, карта дорог
### Модалки
- Еда: showFoodModal, showEditFoodModal, showFoodFromRecipeModal
- Рецепты: showRecipeDetailModal, showRecipeEditorModal, showAddIngredientModal
- Тренировки: showSessionDetail (карта, графики скорости/высоты, пересчёт, оверлей маршрута, панель де-джиттера SG), showRoutesScreen (сохранённые маршруты)
- Прочее: showWeightModal, showSleepModal, showActivityModal, showProfileModal, showBikeModal, showMapDownloadModal, showLogSupplementModal
## IPC команды (Rust → JS)
| Команда | Назначение |
|---------|------------|
| `import_seed_products` | Импорт продуктов из seed DB, прогресс-события |
| `get_safe_insets` | Отступы Android status/nav bar |
| `gps_service_command` | start/stop GPS + BLE команды (ble_scan/connect/disconnect) |
| `gps_service_read_points` | Чтение активного JSONL файла точек (через `native_root`) |
| `gps_set_session_id` | Передать id сессии сервису (пишет `gps_session.txt` → сервис дописывает `_s<id>` в имя файла) |
| `list_points_files` / `read_points_file` / `delete_points_file` | Список/чтение/удаление нативных `gps_points_*` (восстановление + чистка) |
| `get_points_filename` | Имя активного файла точек (по указателю `gps_live_points.jsonl`) |
| `debug_native_paths` | Диагностика путей нативных файлов |
| `save_file` | SAF диалог сохранения (GPX экспорт) |
| `get_app_info` | Версия, git hash, дата сборки, `debug` (cfg!(debug_assertions) — для глушения диагностики в release) |
| `get_update_info` | Проверка OTA обновления (BUILD_NUMBER с VPS) |
| `ble_scan` | Сканирование BLE устройств |
| `ble_scan_results` | Результаты скана (Android) |
| `ble_connect` | Подключение к BLE устройству |
| `ble_disconnect` | Отключение BLE |
| `ble_get_hr` | Последние данные HR/SpO2/BP |
| `ble_is_connected` | Статус подключения |
| `ble_check_bluetooth` | Состояние Bluetooth адаптера |
| `ble_save_device` | Сохранение привязанного устройства |
| `import_tiles` | Чтение тайлового SQLite батчами (base64) |
| `download_file` | Скачивание бинарного файла по URL (без лимита размера) |
| `rebuild_roads` | Перестройка road_points из всех треков (2м дедуп) |
| `get_road_points` | Чтение road_points батчами (обход лимита SQL plugin) |
## Формула ккал (GPS)
### Велосипед — НЕТТО-модель (суточный баланс = intake − BMR*activityLevel − activity, поэтому активность считается СВЕРХ покоя)
```
Power = v * (m*g*sin(gradient) + Crr*m*g*cos(gradient) + 0.5*CdA*rho*v^2)
m = rider_kg + bike_kg; gradient из СГЛАЖЕННОЙ высоты
Crr (ручной surface_type): 0.004 асфальт / 0.006 смешанный / 0.008 грунт
Crr по акселерометру (crrFromAccel, если есть accel_rms >10 точек): <3=0.004, 3-7=0.006,
7-11=0.008, 11-15=0.012 (плотный грунт), >15=0.020 (трава/колея/щебень). Прежний потолок 0.008
занижал грунты ~3x (замер: трава требует 0.020-0.031). Мапится и медиана сессии, и каждая точка (ptCrr).
CdA = 0.6, rho = 1.225, efficiency = 0.25
Педалирование (power>0): kcal = max(физика, MET-нетто), физика = power*dt/(0.25*4184)
MET по скорости: <16км/ч=6.0, <19=6.8, <22=8.0, <25=10.0, <28=12.0, иначе 14.0
MET-нетто = (dt/60)*((MET-1)*3.5*weight)/200 (−1 MET покоя, он уже в BMR)
Floor только при v≥5км/ч (ниже — дрожание GPS, не езда)
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим, нужна калибровка): max() не СКЛАДЫВАЕТ —
на подъёмах физика выигрывает, а накладные (баланс/стабилизация корпуса, без внешней работы,
их ловит MET-этаж) теряются. Направление ошибки известно: подъёмы недосчитаны (по поездке ~1.34x
MET/физика, но равнина 1.85x, подъём 0.41x). Честный фикс = физика/КПД + накладные, отложен.
Накат / freewheel (power≤0, «велик катился бы сам»): kcal = базальный метаболизм
(детектор кручу/накат по IMU-каденсу проверен на 50Hz — сигнал слишком слаб, не используется; хук cadence_rpm — под BLE-датчик шатуна)
Набор высоты: кумулятивный гистерезис (elevRef, порог 1м) — на плотных треках per-point разница <1м даёт 0
ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ (по решению не правим): абсолютный набор по сырому GPS-альтиметру расходится
с независимым пересчётом в ОБЕ стороны (медиана разброса высоты между проездами одной точки ~10м).
Без модели рельефа (DEM) не разрешается. Реальный фикс — предзагрузка DEM по району дома по проводу
(кнопка «уточнить высоты» → GPXZ работает, но требует сети, а РЭБ/РКН глушат в поле). Пока: набор
имеет точность порядка десятков процентов, на расчёт мощности влияет слабо (там уклон по окну, ограничен сверху).
```
### Дистанция по сегментам (computeStats, gps.js ~189-235)
```
segMaxMs = maxSpeedKmh/3.6 (вело) или walkMaxSpeedKmh/3.6 (пеш); gapThr = config.gapThresholdSec (30)
шип (dt<=gapThr И v>segMaxMs) → сегмент не считать (телепорт без разрыва по времени)
разрыв(dt>gapThr) → segDist = min(avg(speed)*dt, segMaxMs*dt); если хорда<20м → берём хорду (стоял, скорость залипла)
норма (иначе) → segDist = хорда (haversine)
distance += segDist; высота/maxSpeed — только на «норме»; ккал в разрыве/estimated = walk-MET по времени
Зависит только от ts+speed (не от флага estimated) → live == «Стоп» == пересчёт. gapThreshold настраивается, применяется при пересчёте.
Ручная пауза (opts.pauses = [{startTs,endTs}] мс): сегмент, пересекающий паузу (prevMs<endTs И pMs>startTs),
исключается ЦЕЛИКОМ — ни дистанции, ни высоты, ни ккал, ни времени (pausedSec вычитается из duration), elevRef=null
(перепад, набранный пешком за паузу, не банкуется на первом сегменте после резюма). Авто-GPS-гэп (без паузы) — считается как раньше.
Глюк-гард высоты: соседний сглаженный перепад с грейдом > maxRealGradePct (1.0 = 100% = 45°) не банкуется и не двигает elevRef
(скачок/ступенька, в т.ч. Android no-fix 0.0). Плавный приход к любому значению (реальный 0 у реки) проходит.
Kotlin пишет "alt":null без фикса высоты (hasAltitude()), честный 0 с фиксом сохраняется. Паузы: колонка gps_sessions.pauses (JSON).
Пеший пуш велосипеда (только односкоростной, gear_count 1 или пусто + есть звезда/трещотка/колесо):
на фиксированной передаче каденс <30 об/мин = велик не едет, его ведут. Порог скорости walkBikeMs = 30*wheelCirc*gearRatio/60.
Сегмент медленнее порога (нормальный фикс, не гэп) исключается из ездовой дистанции/скорости/высоты (elevRef=null — пеший
подъём не склеивается с ездовым), усилие считается как ходьба (walk-MET нетто), не по физике. #7: исключается КАЖДАЯ такая точка
без оглядки на длительность (краткие торможения тоже, безвредно ~0м); в отчёт как «пешком» идут только сплошные пробеги ≥30с.
Возврат computeStats: walkBikeSec (время пуша ≥30с-пробегами), walkBikeMeters (пешая дистанция). opts.gearCount прокинут во все вызовы.
Метки ветра (разметка, НЕ расчёт): кнопка «Ветер» на экране записи — тап открывает интервал встречного ветра,
тап закрывает. GPS.toggleWindMark()/_closeOpenWindMark(); GPS.recording.windMarks=[{startTs,endTs}] мс, персист в localStorage,
запись в БД (DB.setSessionWindMarks) на финише + восстановление в recovery. Колонка gps_sessions.wind_marks (JSON). Это ground
truth для проверки оценки ветра по треку (tools/track/track.py, wind_by_passes) — на distance/kcal/power НЕ влияет. Голос — потом.
```
### Ходьба/бег — MET-таблица
```
<4 км/ч = 2.5 MET, 4-5.5 = 3.5, 5.5-7 = 5.0, 7-9 = 8.0, 9-12 = 10.0, 12-16 = 12.5, 16+ = 15.0
kcal = (dt/60) * (MET * 3.5 * weight_kg) / 200
```
## Сборка
```bash
./build.sh -d -i android # debug APK + установка на телефон (для телефона всегда -d)
./build.sh android # release APK (подпись debug.keystore)
./build.sh -u -d android # debug + install + заливка на роутер/VPS (OTA)
./build.sh install -d # поставить уже собранный APK без пересборки
```
Без таргета `./build.sh` = dev-сервер (десктоп), НЕ сборка APK.
## Отладка WebView (debug-сборки)
Debug-APK инспектируется через Chrome DevTools Protocol — можно смотреть DOM/состояние и выполнять JS прямо на телефоне:
```bash
PID=$(adb shell pidof dev.baldav.app)
adb forward tcp:9222 localabstract:webview_devtools_remote_$PID
curl -s http://127.0.0.1:9222/json/list # webSocketDebuggerUrl страницы
```
`test_recovery/cdp.js` (gitignored) — минимальный CDP-клиент: `node cdp.js "<ws-url>" "<js-expr>"` (Runtime.evaluate, awaitPromise). Так был найден баг зависания `showSessionDetail` на 1-точечном треке (delete не привязывался — исполнение висело на `await whenMapReady` без вида карты).