diff --git a/PROJECT_MAP.md b/PROJECT_MAP.md new file mode 100644 index 0000000..3b64808 --- /dev/null +++ b/PROJECT_MAP.md @@ -0,0 +1,166 @@ +# PROJECT MAP — BalDav + +Трекер здоровья и активности. Pure JS + Tauri 2 + SQLite. Без фреймворков и бандлеров. + +## Статистика + +| Метрика | Значение | +|---------|----------| +| Строк кода | ~15000 | +| Файлов исходников | 44 | +| Язык | JS 71%, Rust 4%, Kotlin 10%, CSS 15% | +| Таблиц в БД | 15 | +| Экранов | 7 основных + 12 модалок | +| IPC команд (Tauri) | 6 | +| Библиотеки | Leaflet, Chart.js, Hammer.js, JSZip | +| Платформы | Android (Kotlin), Desktop (Tauri) | + +## Структура файлов + +### Frontend (`frontend/`) + +| Файл | Строк | Назначение | +|------|-------|------------| +| `js/main.js` | 3264 | Экраны, навигация, модалки, графики | +| `js/db.js` | 972 | SQLite через Tauri IPC. Миграции, CRUD | +| `js/gps.js` | 947 | GPS запись, физика ккал, haversine, импорт/экспорт | +| `js/tilecache.js` | 178 | Оффлайн кэш тайлов карты (IndexedDB) | +| `js/bmr.js` | 14 | Базальный метаболизм (Mifflin-St Jeor) | +| `css/styles.css` | 1599 | Темная тема, мобильный дизайн | +| `index.html` | 26 | Точка входа, порядок загрузки скриптов | +| `data/products_seed.db` | 8.5MB | 13480 продуктов (magnit, 5ka, ION, kdv) | + +**Библиотеки** (`lib/`): leaflet.js, chart.min.js, chartjs-plugin-zoom.min.js, hammer.min.js, jszip.min.js + +### Бэкенд Rust (`src-tauri/`) + +| Файл | Строк | Назначение | +|------|-------|------------| +| `src/lib.rs` | 512 | Миграции v1, IPC команды, seed import | +| `src/main.rs` | 5 | Точка входа | +| `Cargo.toml` | 18 | tauri, tauri-plugin-sql, rusqlite, serde | +| `tauri.conf.json` | — | Конфиг приложения, плагины, ресурсы | + +### Android Kotlin (`src-tauri/gen/android/.../dev/baldav/app/`) + +| Файл | Строк | Назначение | +|------|-------|------------| +| `MainActivity.kt` | 153 | FileObserver для IPC, seed DB копирование, safe insets, SAF | +| `GpsRecorderService.kt` | 205 | Foreground service GPS, WakeLock, JSONL батчевая запись | +| `generated/*.kt` | ~1100 | Tauri-сгенерированные: WebView, IPC, permissions | + +### Инструменты (`tools/`) + +| Скрипт | Назначение | +|--------|------------| +| `5ka_scraper.user.js` | Tampermonkey скрейпер 5ka.ru (рекурсивный BFS категорий) | +| `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 | +| `scrape_5ka.js`, `scrape_5ka_api.js` | CLI скрейперы 5ka | +| `download_tiles.js` | Скачивание тайлов карт для оффлайна | +| `fetch_openfoodfacts.js` | OpenFoodFacts API | +| `fuzzy_dedup.js` | Нечёткая дедупликация | + +### Корень + +| Файл | Назначение | +|------|------------| +| `build.sh` | Сборка: android/debug/release, sync frontend, версионирование | +| `BUILD_NUMBER` | Автоинкремент номера сборки | +| `ion_products.json` | 1111 продуктов ION (сырые данные скрейпера) | +| `5ka_products_fresh.json` | 3103 продукта 5ka (сырые данные) | + +## База данных + +### Таблицы (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 | +| `supplements` | Добавки (БАД) | name, daily_dose_g, витамины/минералы | +| `supplement_log` | Приём добавок | date, supplement_id, dose_g | +| `gps_sessions` | GPS тренировки | date, activity_type, distance, kcal, speed, elevation, bike_id | +| `gps_points` | Точки трека | session_id, ts, lat, lon, altitude, speed, seq | +| `bikes` | Велосипеды | name, weight_kg, wheel, звёзды, surface_type, is_active | + +### Seed DB (products_seed.db) + +| Источник | Продуктов | Микронутриенты | +|----------|-----------|----------------| +| magnit | 8627 | нет | +| 5ka | 3103 | нет | +| ION | 1078 | витамины + минералы | +| kdv | 672 | нет | +| **Всего** | **13480** | 1398 обогащены (nutrients_guessed) | + +## Экраны и навигация + +### Нижний навбар (5 табов) +``` +[Сегодня] [Продукты] [Трек] [Добавки] [Настройки] +``` + +### Экраны +- **Сегодня** — дневной баланс ккал, еда (продукты + блюда), активность, вес, сон, добавки, статистика (графики) +- **Продукты** — подтабы [Продукты | Рецепты], поиск, карточки с КБЖУ, добавление в дневник +- **Трек** — список GPS тренировок, запись, импорт GPX/FTB, скачивание карт +- **Добавки** — список БАД, логирование приёма +- **Настройки** — профиль (вес, рост, BMR), велосипеды, GPS конфиг, оффлайн режим + +### Модалки +- Еда: showFoodModal, showEditFoodModal, showFoodFromRecipeModal +- Рецепты: showRecipeDetailModal, showRecipeEditorModal, showAddIngredientModal +- Тренировки: showSessionDetail (карта, графики скорости/высоты, пересчёт) +- Прочее: 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 сервиса | +| `gps_service_read_points` | Чтение JSONL файла точек | +| `save_file` | SAF диалог сохранения (GPX экспорт) | +| `get_app_info` | Версия, git hash, дата сборки | + +## Формула ккал (GPS) + +### Велосипед — физическая модель +``` +Power = v * (m*g*sin(gradient) + Crr*m*g*cos(gradient) + 0.5*CdA*rho*v^2) + m = rider_kg + bike_kg + Crr = 0.004 (асфальт) / 0.006 (смешанный) / 0.008 (грунт) + CdA = 0.6, rho = 1.225, efficiency = 0.25 + +Power > 0: педалирование → kcal = power*dt / (0.25 * 4184) +Power <= 0: каботаж → kcal = базальный метаболизм +``` + +### Ходьба/бег — 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 android # debug APK +./build.sh android # release APK +./build.sh -i android -d # debug + install +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..ceafba6 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# BalDav + +Персональный трекер здоровья и физической активности. + +## Возможности + +- **Дневник питания** — 13480 продуктов из магазинов (Магнит, 5ка, KDV) + база ИОН с микронутриентами. Рецепты с учётом термообработки (потери витаминов при варке/жарке). Единицы измерения (штуки, ложки, стаканы). +- **GPS трекинг** — запись маршрутов на велосипеде/пешком в фоне. Оффлайн карты (Leaflet + IndexedDB кэш). Импорт GPX/FTB (FitoTrack). Экспорт GPX. +- **Расчёт ккал** — физическая модель для велосипеда (мощность из gradient + Crr + CdA). MET-таблица для ходьбы/бега. Профили велосипедов (вес, звёзды, покрытие). +- **Графики** — скорость и высота тренировки с зумом. Тренд веса. Донат макронутриентов. Баланс ккал (приход/расход). +- **Вес, сон, добавки** — логирование с историей. + +## Стек + +- **Frontend**: чистый JavaScript, HTML, CSS. Без React/Vue/bundler. +- **Backend**: Rust (Tauri 2) — SQLite, IPC, seed import. +- **Android**: Kotlin — foreground GPS service, WakeLock, SAF. +- **Библиотеки**: Leaflet (карты), Chart.js (графики), Hammer.js (жесты), JSZip. + +## Сборка + +```bash +# Зависимости: Rust, Node.js, Android SDK + NDK +./build.sh -d android # debug APK +./build.sh android # release APK +./build.sh -i android -d # debug + установка на устройство +``` + +## Структура + +Подробно: [PROJECT_MAP.md](PROJECT_MAP.md) + +``` +frontend/ + js/main.js — UI, навигация, модалки (3264 строк) + js/db.js — БД, миграции, CRUD (972) + js/gps.js — GPS, физика ккал (947) + js/tilecache.js — оффлайн тайлы (178) + css/styles.css — стили (1599) + data/products_seed.db — 13480 продуктов +src-tauri/ + src/lib.rs — Rust бэкенд (512) + gen/android/ — Kotlin: MainActivity, GpsRecorderService +tools/ + *_scraper.user.js — скрейперы магазинов (Tampermonkey) + import_*.js — импорт в seed DB + enrich_nutrients.js — обогащение микронутриентами +``` + +## Данные продуктов + +| Источник | Продуктов | Витамины/минералы | +|----------|-----------|-------------------| +| Магнит (SSR парсинг) | 8627 | нет | +| 5ка (API скрейпер) | 3103 | нет | +| ИОН (web.ion.ru) | 1078 | да | +| KDV (SSR парсинг) | 672 | нет | +| Обогащено из ИОН | 1398 | да (nutrients_guessed) | diff --git a/frontend/js/db.js b/frontend/js/db.js index 4cd1c2c..4638e5e 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -582,13 +582,14 @@ async function getDailyFoodTotals(days) { } async function getDailyActivityTotals(days) { + const offset = `-${days ?? 30} days`; return select( `SELECT date, SUM(kcal) as kcal FROM ( SELECT date, kcal FROM activity_log WHERE date >= date('now', ?1) UNION ALL - SELECT date, total_kcal as kcal FROM gps_sessions WHERE date >= date('now', ?1) AND total_kcal IS NOT NULL + SELECT date, total_kcal as kcal FROM gps_sessions WHERE date >= date('now', ?2) AND total_kcal IS NOT NULL ) GROUP BY date ORDER BY date ASC`, - [`-${days ?? 30} days`] + [offset, offset] ); } diff --git a/frontend/js/gps.js b/frontend/js/gps.js index b587c24..87dca37 100644 --- a/frontend/js/gps.js +++ b/frontend/js/gps.js @@ -10,6 +10,9 @@ GPS.config = { minAccuracyM: 20, // skip fixes less accurate than this minDistanceM: 1, // skip points closer than this maxSpeedKmh: 60, // above this = GPS glitch, skip segment + airDrag: 0.6, // аэродинамическое сопротивление (CdA, м2). Город=0.6, спорт=0.4 + airDensity: 1.225, // плотность воздуха (кг/м3). Уровень моря=1.225, горы=1.0 + pedalEfficiency: 25, // КПД педалирования (%). Человек ~25% }; // Load saved config @@ -59,11 +62,11 @@ GPS.computeStats = function(points, weightKg, opts) { return cnt > 0 ? sum / cnt : alt; }); - // Physics constants + // Physics constants (configurable in settings) const g = 9.81; - const CdA = 0.6; // typical upright position drag area (m^2) - const rho = 1.225; // air density at sea level (kg/m^3) - const efficiency = 0.25; // human mechanical efficiency (~25%) + const CdA = GPS.config.airDrag ?? 0.6; + const rho = GPS.config.airDensity ?? 1.225; + const efficiency = (GPS.config.pedalEfficiency ?? 25) / 100; const Crr = opts?.surfaceCrr ?? 0.006; // rolling resistance (mixed surface default) const wKg = parseFloat(weightKg) || 80; // ensure numeric (profile stores strings) const mTotal = wKg + (parseFloat(opts?.bikeWeightKg) || 0); @@ -446,7 +449,7 @@ GPS.startRecording = async function(activityType, weightKg) { GPS.recording.activityType = activityType || "bike"; GPS.recording.points = []; GPS.recording.startTime = null; - GPS.recording.weightKg = weightKg ?? 80; + GPS.recording.weightKg = parseFloat(weightKg) || 80; GPS.recording.state = "waiting_gps"; GPS.recording.sessionId = null; GPS.recording.pauseStart = null; @@ -618,7 +621,7 @@ GPS.importGPX = async function(xmlString, weightKg) { const { points, activityType } = GPS.parseGPX(xmlString); if (points.length < 2) throw new Error("GPX has fewer than 2 points"); - const stats = GPS.computeStats(points, weightKg ?? 80); + const stats = GPS.computeStats(points, parseFloat(weightKg) || 80); const first = points[0].ts ? new Date(points[0].ts) : new Date(); const last = points[points.length - 1].ts ? new Date(points[points.length - 1].ts) : new Date(); diff --git a/frontend/js/main.js b/frontend/js/main.js index 07fd1d8..8068bb4 100644 --- a/frontend/js/main.js +++ b/frontend/js/main.js @@ -1246,21 +1246,18 @@ async function showRecipeDetailModal(recipeId) { logBtn.onclick = async () => { await DB.addFoodFromRecipe(state.date, nowTime(), recipeId); - overlay.remove(); - history.back(); + history.back(); // popHandler closes modal renderScreen(state.screen); }; editBtn.onclick = () => { - overlay.remove(); - history.back(); - showRecipeEditorModal(recipeId); + history.back(); // popHandler closes modal + setTimeout(() => showRecipeEditorModal(recipeId), 50); }; delBtn.onclick = async () => { await DB.deleteRecipe(recipeId); - overlay.remove(); - history.back(); + history.back(); // popHandler closes modal renderScreen("recipes"); }; } @@ -2755,6 +2752,27 @@ async function renderSettings(container) { +