
Введение
Последние шесть лет я собирал данные о своих тренировках. Сначала это была обычная тетрадка, потом заметки в телефоне, затем Excel. Со временем таблиц стало так много, что я начал тратить всё больше времени на обслуживание собственной системы учёта.
Вес, питание, тренировки, замеры тела, статистика прогресса — всё существовало отдельно друг от друга. Весной 2026 года я уже почти два месяца сидел без работы и неожиданно получил ресурс, которого обычно не хватает взрослому человеку: свободное время.
22 мая мне пришла простая мысль: а что если объединить все свои данные в одну систему?
На следующий день я узнал, насколько далеко продвинулись инструменты разработки на базе ИИ, купил подписку и решил попробовать.
Раздел 1. Шесть лет подготовки к проекту

Мой путь начался не с программирования, а со спорта. В 2020 году я начал регулярно заниматься, записывая упражнения, подходы и веса сначала в заметки телефона, а затем в бумажную тетрадь.
Примерно тогда же появился первый калькулятор калорий в Mathcad. Позже он переехал в Excel и начал постепенно обрастать новыми функциями.

Следующие несколько лет я фактически занимался проектированием будущего приложения, просто тогда ещё не понимал этого. В Excel постепенно переехали питание, тренировки, вес, процент жира, измерения тела и аналитика.

Годы шли. Количество данных росло. Количество Excel-файлов тоже.
Раздел 2. Первая версия
22 мая я начал делать первую версию проекта. Самым простым вариантом показался Streamlit.
Через несколько часов приложение уже существовало. Через несколько часов стало понятно, что пользоваться им невозможно.

Проблема оказалась простой: у меня накопились тысячи записей тренировок, веса, питания и измерений. При каждом обновлении страницы Streamlit пересчитывал слишком много данных заново. Для маленького прототипа это было терпимо, но для полноценной системы учёта — уже нет.
24 мая было принято решение полностью переписать проект.
Раздел 3. Новая архитектура
Технически новая версия стала не просто «приложением на Electron». Я разделил проект на несколько слоёв.
Electron отвечает за запуск десктопного приложения, управление локальным backend и упаковку в exe.
Frontend отвечает за интерфейс, страницы, графики, формы ввода и клиентский кэш.
FastAPI backend отвечает за REST API, бизнес-логику, импорт данных, синхронизацию и аналитику.
SQLite используется для локального хранения пользовательских данных и справочников.
Yandex Disk API используется как транспортный слой для синхронизации между устройствами.
Polar AccessLink используется для импорта тренировок, пульса и спортивной статистики.
Frontend не ходит напрямую в базу. Все операции проходят через FastAPI, а пользователь определяется через локальный X-User-ID. Это позволило не смешивать UI-логику, расчёты и хранение данных.
В итоге приложение стало похоже на маленькую локальную клиент-серверную систему: Electron запускает оболочку, backend работает локально, база лежит на компьютере пользователя, а внешние API подключаются только для импорта и синхронизации.

В этот момент проект перестал быть экспериментом и начал превращаться в настоящее приложение.
Раздел 4. БД
Еще на этапе стримлита я задумался, о том что хранить всё в одной SQLite-базе неудобно: личные данные, OAuth-токены, историю тренировок и общие справочники лучше разделить.
В итоге desktop-версия использует две SQLite-базы.
workouts.db — личные данные: тренировки, вес, питание, замеры, токены, настройки, sync-состояние.
shared.db — справочники: продукты, упражнения, растяжка, велосипедные коэффициенты.
Backend открывает workouts.db как основную базу и подключает shared.db через ATTACH.
Это дало два плюса.
Первый плюс: личные данные можно не класть в публичный репозиторий.
Второй плюс: справочники можно поставлять вместе с установщиком.
К моменту подготовки публикации схема дошла до версии 79. Последние миграции были уже не просто про добавление колонок, а про наведение порядка в архитектуре данных.
Например, пришлось переносить пользовательские рационы из shared.db в workouts.db, очищать shared.db от пользовательских таблиц, выделять канонический справочник силовых упражнений и разделять упражнения на strength и stretching, чтобы импортированная база растяжки не засоряла список силовых упражнений.
Раздел 5. Оптимизация производительности
После перехода на Electron, FastAPI и SQLite первая большая проблема Streamlit исчезла: приложение больше не пересчитывало весь интерфейс при каждом действии пользователя. Но по мере роста функциональности появились уже другие проблемы производительности.
Самый показательный пример — список кардио-тренировок. В первой реализации backend сначала получал список тренировок, а затем для каждой тренировки отдельно рассчитывал статистику по пульсу. На маленькой истории это было незаметно, но на большом количестве данных такой подход превращался в классический N+1.
Упрощённо старая схема выглядела так:
получить список кардио-тренировок; для каждой тренировки отдельно сходить в таблицу пульса; посчитать avg_hr, max_hr и другие агрегаты; собрать итоговый ответ для frontend.
На практике это означало, что один экран мог порождать десятки дополнительных запросов к SQLite.
После этого я начал выносить тяжёлые расчёты в batch-запросы. Вместо того чтобы считать HR-статистику отдельно для каждой тренировки, backend получает список workout_id и одним запросом забирает агрегаты сразу по всем нужным тренировкам.
Похожая история была с главным экраном. Сначала frontend при открытии приложения делал несколько параллельных запросов: отдельно за весом, тренировками, питанием, синхронизацией и аналитикой. Позже я вынес это в агрегированный endpoint GET /api/dashboard/home. Теперь главный экран получает большую часть данных одним запросом, а тяжёлые блоки загружаются отдельно и только тогда, когда действительно нужны.
Проблемные места были такие:
GET /api/cardio/workouts — N+1 расчёт HR-статистики. GET /api/analytics/ctl — тяжёлый расчёт на большой истории. GET /api/sync/health-connect/hub — тяжёлая агрегация. Главная страница — слишком много параллельных запросов при открытии.
После оптимизации я сделал несколько изменений:
заменил N+1 расчёты на batch-запросы;
добавил индексы по user_id, датам и типам тренировок;
вынес главный экран в GET /api/dashboard/home;
разделил production-данные и debug-панели;
добавил lazy-загрузку тяжёлых блоков;
убрал дублирующиеся запросы на статус синхронизации.
Например, для таблиц с тренировками и пульсом появились индексы по пользователю, дате и workout_id. Это особенно важно для локального приложения, потому что SQLite работает быстро, но только если не заставлять его каждый раз сканировать всю историю тренировок.
В итоге главная страница вместо пачки разрозненных запросов стала открываться через один основной endpoint, а список кардио-тренировок перестал пересчитывать пульс отдельно для каждой строки.
Раздел 6. Polar, аналитика и данные
Для записи силовых тренировок я использую экосистему Polar и нагрудный пульсометр.
Интеграция с Polar оказалась не просто «подключить API». Данные приходят не в том виде, в котором их удобно показывать в интерфейсе.
Схема импорта сейчас выглядит так:
Polar AccessLink Затем pending workout raw JSON. Затем парсинг основных метрик. Затем привязка данных к кардио или силовой тренировке. Затем сохранение avg_hr, max_hr, calories_chest. Затем парсинг samples.data. Затем сохранение временного ряда пульса в workout_heart_rate.
Отдельная проблема — пульсовые сэмплы. Нельзя было просто доверять sample-type, потому что в разных payload данные могли быть размечены по-разному. Поэтому парсер проверяет не только тип, но и содержимое. Если в sample-блоке есть значения, похожие на пульс в диапазоне 25–240 bpm, они обрабатываются как HR-ряд.
Для кардио это даёт график пульса по времени. Для силовых — возможность анализировать блоки тренировки, пики, восстановление между подходами и общую нагрузку.

Позже появилась возможность анализировать велотренировки буквально по каждой точке маршрута.

Раздел 7. Калькулятор калорий
Калькулятор калорий я сделал не как статическую формулу, а как систему с калибровкой.
Проблема в том, что браслет и часы считают расход калорий неточно. По моим данным, часы стабильно завышали расход примерно на 30 процентов. Поэтому я не стал перезаписывать исходные данные, а добавил корректирующий коэффициент.
Сначала система считает активные калории с учётом приоритета источников.
Смысл такой:
raw_activity = bracelet_daily_calories - bracelet_workout_calories + Polar_or_chest_workout_calories
Это нужно, чтобы не посчитать одну и ту же тренировку два раза: сначала по браслету, потом по Polar.
Дальше применяется calibration_factor:
corrected_activity = raw_activity * calibration_factor
Сам коэффициент пересчитывается по окну наблюдений.
Система берёт несколько точек веса за последние 14 дней, включая вчера, но не включая сегодняшний день. Потом сравнивает прогнозируемый дефицит с фактическим изменением веса.
predicted_deficit = predicted_expenditure - consumed_calories observed_deficit = -trend_weight_change_kg * 7700 factor = observed_deficit / predicted_deficit
При этом сырые импортированные калории не изменяются. В базу пишется только история калибровок: окно расчёта, коэффициент, confidence и дата.
Так я могу видеть исходные данные, но использовать в расчётах более реалистичный расход.
Раздел 8. Яндекс.Диск вместо сервера
Когда появилась идея мобильного клиента, возник вопрос синхронизации.
Поднимать полноценный сервер не хотелось. Поэтому было принято довольно необычное решение: использовать Яндекс.Диск как транспортный слой между устройствами. Для личного проекта это оказалось неожиданно удобным вариантом.

Яндекс.Диск используется не как способ «скинуть базу целиком в облако», а как транспорт для инкрементальной синхронизации.
Я назвал этот слой FormaSync.
Идея такая:
Устройство A формирует пакет изменений. Пакет сохраняется в JSONL. Затем JSONL упаковывается в ZIP. ZIP загружается на Яндекс.Диск в директорию FormaSync конкретного пользователя. Рядом обновляется manifest.json. Устройство B скачивает manifest, видит новый пакет, загружает его и применяет изменения локально.
В облаке лежит не вся SQLite-база, а пакеты изменений. У каждого пакета есть manifest.
Пример структуры manifest:
schema_version: 1 revision: 1 updated_at: 2026-05-30T12:00:00Z source_device: mobile source_device_id: uuid package: packages/000001-mobile.zip package_sha256: hex entities_summary: food_entries: 3 body_metrics: 1 strength_workouts: 0 cardio_workouts: 0
Внутри ZIP лежат JSONL-файлы по сущностям: питание, вес, тренировки, растяжка, настройки, агрегаты Health Connect.
Такой формат проще отлаживать, чем бинарную SQLite-базу, и он позволяет в будущем решать конфликты на уровне отдельных записей.
Раздел 9. Самый дорогой баг
Любая история разработки обязана содержать факап.
Мой оказался довольно болезненным.
В какой-то момент я решил почистить проект от мёртвого кода и попросил ИИ помочь. Он помог настолько хорошо, что вместе с мусором удалил архив учебных материалов за шесть лет.
Часть вины была и на мне. Папка называлась Data D, что оказалось не лучшей идеей.

После этого я стал гораздо внимательнее относиться к любым автоматическим операциям удаления.
Раздел 10. День 13. Подготовка к публикации
После завершения основной разработки я решил подготовить проект к публикации.
И тут начались сюрпризы.
Размер проекта оказался около 92 ГБ.
Количество файлов — примерно 170 тысяч.

При подготовке к публикации выяснилось, что «выложить проект на GitHub» и «выложить проект безопасно» — разные задачи.
В проекте были личная база тренировок, OAuth-токены, .env с секретами, кэши, сборки, временные файлы, большие SQLite-базы и установочные артефакты.
Поэтому пришлось разделить публичную и приватную конфигурацию.
В установщик можно класть публичные OAuth client id, redirect URI, пустую или очищенную seed-базу и справочники без пользовательских данных.
В установщик и публичный репозиторий нельзя класть YANDEX_CLIENT_SECRET, GOOGLE_CLIENT_SECRET, POLAR_CLIENT_SECRET, access tokens, refresh tokens, мой workouts.db и dev-версии баз со следами пользовательских таблиц.
Для desktop-сборки я добавил отдельную проверку, которая валит сборку, если в публичные файлы попали запрещённые ключи или seed-базы слишком большие.
Это оказалось отдельным этапом разработки. До этого приложение просто работало у меня на компьютере. После этого оно стало пригодно для публикации.
Попытка посчитать объём проекта через cloc сначала показала миллионы строк кода. Позже выяснилось, что большая часть объёма приходилась на базы данных, кэши, сборки и временные файлы.

Следующий день ушёл на создание чистовой версии проекта. Пришлось отделять пользовательские данные, проверять OAuth, искать токены, тестировать установщик и запускать приложение на новой учётной записи Windows.

Именно тогда я впервые убедился, что приложение действительно работает не только на моём компьютере.
Раздел 11. ИИ
Отдельно стоит сказать про масштаб работы с ИИ.
Это не был сценарий «написал один промпт — получил приложение». Скорее это был непрерывный цикл:
Идея. План. Реализация. Запуск. Ошибка. Лог. Исправление. Новая ошибка.
За время разработки я израсходовал примерно:
Первый аккаунт Pro — 450 млн токенов.
Второй аккаунт Pro — 530 млн токенов.
Pro+ — 1,05 млрд токенов.
Cursor grant — 25 долларов гранта.
Итого получилось около 2,03 млрд токенов.
Большая часть ушла не на генерацию «новых фич», а на итерации: поиск багов, чтение логов, миграции БД, рефакторинг, документацию, подготовку к публикации, проверку секретов и попытки не сломать уже работающие части.
При этом ИИ не отменил необходимость понимать, что происходит в проекте. Наоборот, чем больше становился код, тем чаще приходилось вручную формулировать ограничения: какие файлы нельзя трогать, какие таблицы нельзя удалять, какие данные должны остаться приватными, какие endpoint нельзя ломать из-за уже существующего frontend
Раздел 12. Итоги
К моменту остановки разработки проект представлял собой полноценное десктопное приложение на Electron.
Оно умеет:
вести журнал тренировок;
учитывать питание;
хранить историю веса;
анализировать данные Polar;
рассчитывать энергозатраты;
синхронизировать данные между устройствами.
Немного цифр:
12 дней активной разработки.
Около 2,03 млрд токенов в ИИ-инструментах.
2 аккаунта Pro и 1 аккаунт Pro+.
25 долларов Cursor-гранта.
74+ миграции базы данных.
Schema version desktop-ветки дошла до 79.
Более 40 документов по архитектуре, API, импорту, синхронизации и упаковке.
Около 500 силовых тренировок в истории.
199 пробежек.
82 велотренировки.
28 плаваний.
Ориентировочно 100–130 тысяч строк осмысленного кода после очистки от кэшей, сборок и баз данных.
Силовые программа считает не по тренировкам, а подходам

Исходный код проекта доступен на GitHub:
Раздел 13. Что дальше
Сейчас разработка поставлена на паузу из-за бюджета.
Следующие цели:
мобильный клиент;
интеграция Health Connect;
анализ сна;
HRV;
пульс в покое;
аналитика восстановления.

Главный вывод всей этой истории оказался не техническим.
Шесть лет накопленных данных дали понимание проблемы. Современные инструменты разработки позволили превратить это понимание в работающее приложение.
12 дней назад его не существовало.
Сегодня оно хранит результаты нескольких лет моего спортивного пути и уже пригодно для повседневного использования.
P.S. Если интересна не только техническая часть, но и вся история создания проекта, записал отдельное видео.
