Перейти к содержанию

Досье для разбора советом

Самодостаточное описание проекта на 2026-08-13: что строим, что уже стоит, какие решения приняты и почему, как всё работает и что вызывает сомнения. Читается без остальных доков; ссылки даны для углубления, а не как обязательное чтение.

Предыдущий разбор и его следствия — COUNCIL-2026-08-13.md.


1. Что это

Личная система трекинга тренировок и здоровья. Один пользователь сейчас, но модель данных рассчитана на нескольких (ADR-0021).

Что не даёт ни один готовый продукт и ради чего всё затевается: точные границы каждого подхода — когда он начался и закончился. Проверялись FitNotes, Hevy, Strong, Jefit и интервальные таймеры: логгеры пишут время на уровне тренировки, таймеры не знают про упражнение и вес, гибрида нет (LESSONS-0001).

Из точных границ выводится: - Не зависящее от датчиков: фактический отдых между подходами, время под нагрузкой, плотность сессии, динамика усталости. - Зависящее от датчика (гипотеза): реакция организма на эпизод усилия — пульс на нагрузке и на восстановлении.

Вторая часть — непроверенная гипотеза, и это осознанно (см. §6).

2. Источники данных

Источник Что даёт Как попадает
Тренировки упражнение, вес, повторы, T1/T2 каждого подхода Mini App → бот → wger
Fitbit Air пульс, шаги, SpO₂, сон, вес по точкам во времени Google Health API v4, OAuth через бота
InBody состав тела, посегментный анализ PDF в бота → парсер
Apple Health приём таблеток и прочее export.xml в бота → импортёр

3. Как устроено

Единая точка входа — Telegram-бот (ADR-0023). Регистрация, открытие Mini App, подключение Google-аккаунта, отправка файлов, выгрузки — всё через него. Отдельного веба и ручной настройки учёток нет.

пользователь → Telegram-бот ─┬→ Mini App (свой экран логирования + интерфейс wger)
                             ├→ wger REST (токен подставляет сервер)
                             ├→ OAuth Google (бот принимает callback)
                             ├→ парсеры InBody / Apple Health
                             └→ core-service по gRPC (/export, /stats)

wger ──(опрос по курсору)──→ wger-sync-worker ──gRPC──→ core-service
Google Health ──(опрос)────→ google-health-sync ──gRPC──→ core-service

Два контура. Ввод тренировок (wger + Mini App + бот) и склад правды (core-service + воркеры). У них разные режимы отказа: ввод обязан работать в зале без связи, склад может лежать сутки без потери данных.

core-service — единственный источник правды для анализа. Go + gRPC + своя Postgres. wger — один из источников, не центр (ADR-0001).

Инфраструктура. Всё в одной VM pet (4 ядра, 7 ГБ, Proxmox в домашней сети). Там же GitLab-раннер: CI собирает образ тем же демоном, который его запускает, — поэтому container registry не нужен (ADR-0018). Наружу — обратный SSH-туннель на VPS с Caddy и валидным TLS.

Репозитории. Группа GitLab mybit, 10 отдельных репозиториев: platform (доки, compose), proto (контракты), core-service, telegram-bot, miniapp-frontend, wger-sync-worker, google-health-sync, inbody-parser, applehealth-import, wger. Микросервисы — сознательный выбор, а не следствие раздельного деплоя.

4. Ключевые решения (26 ADR, здесь — несущие)

ADR Решение Суть обоснования
0013 Таймстампы ставит клиент, сервер валидирует В зале связь лагает: время прихода запроса ≠ время подхода. Плюс это открывает офлайн-логирование. Границы правдоподобности защищают от сбитых часов
0019 Единица анализа — эпизод усилия, не подход Пик пульса приходится на первые 5–20 с отдыха; датчик пишет раз в ~5 с и отбрасывает точки при плохом сигнале, который падает при хвате штанги
0020 wger — хард-форк, upstream не отслеживаем Снимает вечный ребейз. Цена: обновления безопасности переносятся вручную
0021 user_id в схему сразу Пока таблицы пусты — почти бесплатно; потом миграция с бэкфиллом и переделка всех натуральных ключей
0022 Брокера нет gRPC + upsert по натуральному ключу + курсор уже дают at-least-once с дедупликацией
0023 Бот — единственная точка входа Решение владельца: вся коммуникация через Telegram
0024 Токены пользователей — в core, не в env Следствие регистрации через бота и мультипользовательской схемы
0025 Публичный HTTPS — предусловие Mini App требует валидного TLS; Google требует HTTPS в redirect URI, а OOB-флоу отключён с 2023
0012/0018 Отдельные репозитории, сборка без registry Независимые пайплайны; один рантайм-хост

5. Что уже сделано, а что нет

Стоит и проверено: 10 репозиториев с зелёными пайплайнами; VM pet с раннером; контракты CoreService с генерацией Go/Python (генерация локальными плагинами, без внешнего сервиса); заготовки всех сервисов собираются и проходят тесты; форк wger импортирован; локальный стек поднимается, grpcurl отвечает SERVING, миграция создаёт все 5 таблиц.

Реализовано по существу — три куска логики: - проверка initData в боте (constant-time, защита от повторного использования, 15 тестов); - валидация таймингов подхода в core; - разбор отчёта InBody с явным отказом на незнакомой раскладке; - клиентский таймер подхода на двух часах (Date.now() для отправки, performance.now() для отображения).

Не реализовано: персистентность и Export в core, обходы API у воркеров, потоковый разбор export.xml, все эндпоинты бота, регистрация, OAuth, туннель. Домен для HTTPS не выбран.

Порядок работ — ROADMAP.md: вход → логирование → склад → источники и проверка гипотезы. Пять спринтов, 45 задач на доске.

Первое, что предстоит: туннель pet → VPS с валидным TLS на mybit.arvberezin.online. Белого IP дома нет и не будет — туннель обратный, pet сам открывает исходящее соединение, поэтому NAT и серый IP не мешают.

5а. Сосуществование с VPN на VPS

VPS существует не ради этого проекта. Его основная работа — naiveproxy (VPN) и SOCKS5 (danted), то есть повседневный доступ в интернет. Проект въезжает туда гостем, и цена ошибки несимметрична: сломав конфиг Caddy, мы уроним не проект, а VPN.

Отсюда четыре жёстких правила (ADR-0026): существующий блок Caddy не редактируется вообще, добавляется только именованный; caddy validate до применения и reload вместо restart; ключ туннеля ограничен одним портом (restrict,port-forwarding,permitlisten), чтобы компрометация pet не давала шелл на сервере с личным трафиком; порт 9080 только на 127.0.0.1.

Открытый вопрос, который кодом не решается (T-00): стоит ли вообще делить один IP между VPN и публичным именем проекта. Это общая судьба при блокировке и лишняя поверхность для фингерпринтинга naiveproxy. Альтернатива — отдельный VPS за 300–400 ₽/мес.

Заодно вскрылось состояние сервера: диск был забит на 100% (32 ГБ в одном syslog.1), потому что logrotate никогда не ротировал syslog — молча пропускал из-за прав на /var/log. Починено 18.08, рост логов упал с ~32 ГБ/сутки до ~14 МБ.

5б. Как разделён труд

45 задач размечены по исполнителю (do::):

Метка Задач Что это
do::claude-code 32 код
do::claude-infra 6 Ansible, VPS, туннели, CI
do::user-manual 3 DNS, регистрация OAuth-приложения, решение по IP
do::user-test 2 зал, телефон вне домашней сети
do::claude-test 2 Playwright, проверка TLS
do::user-verify 1 подтвердить, что VPN и прокси живы

Смысл разделения: видно, что блокирует работу и ждёт владельца, а что Claude может делать сам. Задача может нести две метки — например, OAuth требует и ручной регистрации приложения, и кода.

6. Что сомнительно — на что смотреть в первую очередь

  1. Гипотеза про пульс не проверена. Разрешение 5 с подтверждено из независимого источника, ошибка при силовой работе 10–20 ударов. На подход в 40 с приходится максимум 8 точек, реально меньше, и выборка смещена к спокойным моментам. Критерий проверки — остаточная дисперсия сверх упражнения, веса, повторов и предыдущего отдыха, а не совпадение с нагрудным ремнём. Проверка стоит в этапе 4.
  2. Бот — единая точка отказа и вся поверхность безопасности. Компрометация его токена открывает доступ ко всему. Токены пользователей будут лежать в БД — шифрование надо решить до первого живого refresh-токена.
  3. Соотношение машинерии и логики. Около 330 строк доменной логики против 10 репозиториев, 26 ADR и 10 пайплайнов. Прошлый совет назвал это главной претензией; владелец ответил, что микросервисы — осознанный выбор. Вопрос закрыт как решение, но цена остаётся.
  4. Смещение часов телефона относительно часов нигде не измеряется, а 5 секунд съедают весь бюджет сигнала.
  5. Общая судьба с VPN. Публичное имя проекта встаёт на тот же IP, что и naiveproxy. Блокировка адреса уносит и то, и другое (см. §5а).
  6. Инфраструктура впереди ценности. Первый этап целиком про доступ (туннель, регистрация, форк) — видимой функции в нём нет. Это следствие ADR-0025, но риск потери темпа в личном проекте реален.
  7. Не названо нигде: часовые пояса (подходы в локальном времени, точки пульса в UTC); идентичность тренажёра (обещана в целях, в схеме только имя упражнения); видимость сбоя синхронизации; бэкфилл истории; порядковый номер подхода внутри упражнения.

7. Вопросы, на которые нет ответа

  • Какое решение в понедельник меняет этот датасет? Нигде не записано.
  • «Отдать выгрузку ИИ» — это анализ или отсрочка анализа? Если решение звучит как «отдыхать, пока пульс не упадёт ниже X», то нужен реалтайм на телефоне, а архитектура имеет форму пакетного экспорта.
  • Стоит ли вообще держать wger, если единственный незаменимый его актив — справочник упражнений, а интерфейс всё равно открывается через Mini App?