SERVICES — как работает каждый сервис¶
Для каждого сервиса: ответственность → внутреннее устройство → интерфейсы
(вход/выход) → заметки по реализации. Контракты данных — в DATA_MODEL.md,
обоснования выбора — в DECISIONS.md.
1. wger (движок тренировок) — форк, Django/DRF/Postgres¶
Ответственность. База упражнений, хранение сессий/подходов, REST API. Это готовый движок, мы используем его как основу, а не пишем своё хранилище тренировок.
Что меняем в форке. Форк хардовый (ADR-0020): upstream не отслеживается, менять
можно свободно. Минимальность больше не требование ради мерджабельности, но остаётся
здравым смыслом — чем меньше правок, тем понятнее граница с чужим кодом.
- Добавляем в модель WorkoutLog два nullable-поля: set_started_at,
set_finished_at (datetime) + миграция + поля в сериализаторе/Viewset API.
Гранулярность уже есть: один WorkoutLog = один подход, и есть поле rest
(фактический отдых). Нам не хватает только реальных T1/T2 — их и добавляем.
- Удаляем nutrition (диета/калории) из INSTALLED_APPS, URL и навигации. Раньше
требовалось прятать, а не удалять, — но только ради мерджей с upstream, которых
больше не будет: форк хардовый (ADR-0020).
- (Опционально) тело-вес в wger можно оставить или скрыть — решаем по ходу, вес и
так приходит из Fitbit/InBody в core.
Интерфейсы:
- Наружу: REST API /api/v2/... (ключевые: workoutsession/, workoutlog/,
routine/, day/, slot/, slot-entry/, exercise*). Есть OpenAPI-схема
(/api/v2/schema/redoc/) — использовать как источник правды по полям.
- Хранилище: своя Postgres (свои миграции, не смешивать с core).
Заметки:
- Фронтенд wger — это React/TypeScript (отдельный репозиторий wger-project/react),
а не Vue. Поэтому свой UI логирования делаем тонким мини-аппом (см. сервис 3),
а не форкаем тяжёлый React-фронт wger. См. DECISIONS.md.
- Систему «рутин/прогрессий» (slots/configs/iterations) для пет-проекта можно почти
не трогать: достаточно ad-hoc сессий + логов + справочника упражнений.
- wger в своей проде сам рекомендует uv и Caddy — это совпадает с нашей инфрой.
2. telegram-bot — Python¶
Ответственность. Единственная точка входа в систему (ADR-0023). Не прокси, а входная система: всё, что пользователь делает с платформой, он делает через бота.
- Регистрация.
/startзаводит пользователя в core-service и создаёт связанный аккаунт в wger. Ручной подготовки учёток нет — ни на сервере, ни в админке wger. - Запуск Mini App. Кнопка
web_appоткрывает и свой экран логирования подхода, и интерфейс самого wger внутри Telegram. - Auth-bridge. Принимает
initData, проверяет HMAC-подпись токеном бота, находит пользователя. Без валидной подписи запрос не идёт дальше — это граница доверия всей системы. - Прокси в wger. Переадресует запросы мини-аппа в REST API wger, подставляя токен пользователя на сервере. Браузер токенов wger не видит никогда.
- Брокер OAuth. Подключение Google-аккаунта: бот выдаёт ссылку авторизации, принимает callback, кладёт токены пользователя в core-service (ADR-0024).
- Приём файлов. InBody PDF и Apple Health
export.xml— передаёт в парсеры. - Выдача данных.
/export(поток чанков из core, собирается в файл) и/stats.
Внутреннее устройство: - Telegram-часть: long polling на старте (вебхук — этап 5). - HTTP-часть: ASGI (FastAPI) — обслуживает мини-апп и принимает OAuth-callback. - gRPC-клиент к core.
Интерфейсы:
- Вход: апдейты Telegram; HTTP от мини-аппа (с initData); OAuth-callback от Google;
файлы.
- Выход: REST к wger; gRPC к core; вызовы парсеров.
Заметки:
- Проверка initData: data_check_string из отсортированных полей, ключ =
HMAC-SHA256(bot_token, "WebAppData"), сравнение constant-time, плюс ограничение
возраста auth_date против повторного использования. Реализовано и покрыто
тестами (src/mybit_bot/initdata.py).
- OAuth-callback требует публичного HTTPS: Google принимает только HTTPS в redirect
URI (исключение — localhost, неприменимо с телефона), а флоу с ручным копированием
кода отключён с 2023 года. Поэтому туннель — предусловие, а не финальный штрих
(ADR-0025).
- Бот — единая точка отказа и главная поверхность безопасности системы. Токены
пользователей хранятся в core, а не в окружении, и не покидают сервер.
3. miniapp-frontend — тонкий HTML/JS(/TS)¶
Ответственность. UI логирования подходов внутри Telegram. Никакой своей БД и бизнес-логики — только экран и вызовы к боту-прокси.
Логика экрана (ядро ценности проекта):
1. отработал подход — телефон в это время не трогаешь;
2. одно нажатие «Подход сделан» → фиксируется marked_at;
3. вес, повторы и упражнение вбиваются когда удобно — задержка записывается
отдельным полем entry_lag_seconds, это данные, а не шум;
4. «Сохранить» → saved_at.
Приложение не утверждает, что знает начало подхода: где он был, восстанавливается из подъёма ЧСС при анализе (ADR-0027). Ввод упражнения свободный — список только подсказывает.
Интерфейсы:
- Вход: справочник упражнений (через бот → wger API).
- Выход: создание WorkoutLog/WorkoutSession (через бот-прокси → wger API).
Заметки:
- Подключить telegram-web-app.js, вызвать Telegram.WebApp.ready()/expand().
- Без сборщика на старте (vanilla/TS-без-бандла) — это пет-проект; усложнять можно
позже. Список упражнений тянуть из wger, не хардкодить.
- Таймстампы ставит клиент (ADR-0013), но границы подхода из них не выводятся
(ADR-0027): клиент даёт только якорь «подход был до этого момента».
- Отсюда обязательный device_clock_offset_ms: всё сопоставление идёт по времени
между телефоном и часами, и сбитые на секунды часы съедают весь бюджет сигнала.
Измеряется раз за сессию, задним числом невосстановим.
4. core-service — Go + gRPC + своя Postgres¶
Ответственность. Единый склад правды для анализа. Нормализованные таблицы по
всем источникам, gRPC-API для записи (воркеры пишут сюда) и для /export/статистики
(бот читает).
Внутреннее устройство:
- gRPC-сервер; схема в proto/.
- Своя Postgres (таблицы workouts, sets, inbody_reports, health_metrics,
medications и т.п. — см. DATA_MODEL.md).
- Слой репозиториев + миграции (например, goose/golang-migrate).
- Метод Export — отдаёт согласованный по времени датасет (CSV/JSON)
потоком, чанками (ADR-0016): год интрадей-пульса не влезает в одно
gRPC-сообщение.
Интерфейсы:
- Вход (gRPC): IngestWorkout, IngestSets, IngestHealth, IngestInbody,
IngestMedications.
- Выход (gRPC): Export (server-streaming), GetStats.
- Все сообщения несут user_id (ADR-0021).
Заметки: - Это сердце; всё остальное дёргает его по gRPC. Браузер в gRPC напрямую не умеет — поэтому фронт ходит через бот. - Для интрадей-рядов (пульс по минутам) хватит обычных индексов по времени; TimescaleDB — опционально и позже, не для MVP. - Идемпотентность приёма: воркеры могут повторно слать одни и те же точки — писать с upsert по натуральному ключу (источник + внешний id + timestamp).
5. wger-sync-worker — Go¶
Ответственность. Мост wger → core. По расписанию забирает новые/изменённые
записи из REST API wger и нормализует в core по gRPC.
Внутреннее устройство:
- Шедулер (cron-тик или тикер).
- HTTP-клиент к wger API: тянет workoutsession/ и workoutlog/ (с нашими
таймстампами), фильтрует по «изменено после последнего курсора».
- Маппинг wger-моделей → нормализованные workouts/sets core.
- Хранит курсор последней синхронизации.
Интерфейсы: вход — REST wger; выход — gRPC core.
Заметки:
- Опрос, а не вебхук: у wger нет нотификаций из коробки.
- Сопоставление делается в core по эпизоду усилия [T1, T2+60s] с раздельными
фазами нагрузки и восстановления, а не по интервалу самого подхода (ADR-0019).
6. google-health-sync — Go¶
Ответственность. Тянет intraday-данные Fitbit Air из Google Health API v4
(health.googleapis.com/v4) по OAuth и льёт в core.
Внутреннее устройство:
- OAuth2 по каждому пользователю: refresh-токены лежат в core-service, а не в
переменных окружения (ADR-0024). Воркер обходит пользователей с подключённым
Google-аккаунтом. Сам OAuth-флоу инициирует бот (ADR-0023) — воркер только
использует уже полученные токены и обновляет access-токен.
- Поллинг dataTypes/{type}/dataPoints с фильтром по времени (пульс, шаги, SpO₂,
сон, вес); постраничный обход; есть reconcile/rollUp для агрегаций.
- Нормализация в health_metrics (тип, значение, timestamp) → gRPC core.
Интерфейсы: вход — Google Health API; выход — gRPC core.
Заметки:
- Старый Fitbit Web API отключается ~сентябрь 2026 — целимся сразу в Google Health
API v4. Intraday там доступен без отдельного «intraday»-разрешения (как было у
Fitbit). Силовые «подход+вес» этот API не отдаёт — они приходят из wger.
- Проверять актуальные эндпоинты/скоупы по докам Google перед реализацией —
фиксировать найденное в LESSONS.md.
7. inbody-parser — Python¶
Ответственность. Парсит PDF-отчёт InBody в структурированный JSON и пишет в core.
Внутреннее устройство:
- Извлечение текстового слоя PDF (в InBody обычно есть текст, не скан) — pdfplumber/
аналог; OCR-фолбэк только если попадётся скан.
- Маппинг в метрики: вес, % жира, скелетные мышцы, висцеральный жир, вода, белок,
минералы, посегментный анализ → inbody_reports.
- gRPC-клиент к core.
Интерфейсы: вход — PDF (от бота); выход — gRPC core.
Заметки:
- Python выбран из-за богатой экосистемы PDF/текста (см. DECISIONS.md).
- Раскладку отчёта зафиксировать на реальных файлах пользователя; вариативность
шаблонов InBody ловить и записывать в LESSONS.md.
8. applehealth-import — Go¶
Ответственность. Парсит export.xml из Apple Health (таблетки и прочее) и пишет
в core.
Внутреннее устройство:
- Стриминговый парсинг encoding/xml (файл большой — не грузить целиком в
память).
- Фильтр нужных record-типов (медикаменты и т.п.) → medications/health_metrics.
- gRPC-клиент к core.
Интерфейсы: вход — export.xml (от бота); выход — gRPC core.
Заметки:
- Go выбран ради потокового парсинга большого XML без раздувания памяти (см.
DECISIONS.md).
- Импорт периодический/ручной (выгрузка из Apple Health не автоматизируется на
стороне Apple).
Сводка стека¶
Сервис (репозиторий mybit/…) |
Язык | Хранилище | Запуск |
|---|---|---|---|
| wger (форк) | Python/Django | своя Postgres | docker |
| telegram-bot | Python (uv) | — (читает core/wger) | docker |
| miniapp-frontend | HTML/JS, без сборщика | — | статика (nginx) |
| core-service | Go (gRPC) | своя Postgres | docker |
| wger-sync-worker | Go | курсор в volume | docker |
| google-health-sync | Go | токены в env | docker |
| inbody-parser | Python (uv) | — | одноразовый запуск |
| applehealth-import | Go | — | одноразовый запуск |
Плюс mybit/proto (контракты) и mybit/platform (доки, compose, тулинг Claude
Code) — см. ARCHITECTURE.md.
Текущее состояние заготовок¶
Во всех репозиториях лежит рабочий каркас: собирается, запускается, конфиг из env, есть тесты и зелёный CI. Реализовано по существу:
core-service— gRPC-сервер, health, reflection, валидация таймингов подходаtelegram-bot— проверкаinitData(HMAC-SHA256, constant-time, защита от повторного использования)inbody-parser— разбор текста отчёта с явным отказом на незнакомой раскладкеminiapp-frontend— цикл логирования подхода с двумя часами (src/timing.js)
Не реализовано и помечено в коде: персистентность и Export в core, обходы API
у воркеров, потоковый разбор export.xml, эндпоинты бота. Порядок — в ROADMAP.md.