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

ARCHITECTURE — общая картина

Детали по каждому сервису — в SERVICES.md. Схема данных и контракты — в DATA_MODEL.md.

Принцип

Два «контура» с разной ответственностью:

  1. Контур ввода тренировокtelegram-bot (единственная точка входа: регистрация, запуск Mini App, OAuth, приём файлов — ADR-0023) + miniapp-frontend (тонкий UI) + wger (движок). Здесь пользователь логирует подходы с реальным временем.
  2. Контур сбора и анализаcore-service (склад правды, своя Postgres) и набор воркеров, которые тянут данные из всех источников и складывают в core. Отсюда идёт /export.

core-service — единственный источник правды для анализа. wger — лишь один из источников (по тренировкам), а не центр системы.

Диаграмма

flowchart TB
    user([Пользователь])

    subgraph tg[Telegram]
        bot[telegram-bot\nPython]
        mini[miniapp-frontend\nтонкий HTML/JS]
    end

    subgraph engine[Движок тренировок]
        wger[(wger\nDjango + REST\nсвоя Postgres)]
    end

    subgraph analytics[Контур анализа]
        core[core-service\nGo + gRPC\nсвоя Postgres]
        wsync[wger-sync-worker\nGo]
        ghsync[google-health-sync\nGo]
        inbody[inbody-parser\nPython]
        ahimport[applehealth-import\nGo]
    end

    google[(Google Health API v4)]
    pdf[InBody PDF]
    axml[Apple Health export.xml]

    user --> bot
    bot -- открывает Mini App --> mini
    mini -- лог подхода (через bot как прокси) --> wger
    bot -- проверка initData, проксирование --> wger

    wger --> wsync --> core
    google --> ghsync --> core
    pdf --> bot --> inbody --> core
    axml --> bot --> ahimport --> core

    bot -- /export --> core
    core -- CSV/JSON --> bot --> user

Поток данных (словами)

  • Тренировка: пользователь жмёт кнопку в боте → открывается Mini App → логирует подход; нажатия «начал/закончил» дают реальные таймстампы → запись уходит в wger (через бот как auth-прокси) → wger-sync-worker периодически забирает новые записи из REST API wger и нормализует в core.
  • Fitbit Air: google-health-sync по расписанию ходит в Google Health API v4 по OAuth, тянет intraday-точки (пульс/шаги/SpO₂/сон/вес) → core.
  • InBody: пользователь кидает PDF в бот → бот передаёт в inbody-parser → распарсенные метрики → core.
  • Apple Health: пользователь кидает export.xmlapplehealth-import стримит и парсит → core.
  • Анализ: /export в боте дёргает gRPC-ручку core → единый CSV/JSON со всех источников, согласованный по времени → пользователь отдаёт его ИИ.

Раскладка репозитория (группа GitLab, отдельные репозитории)

Одна группа GitLab mybit, в ней — отдельный репозиторий на каждый деплоюмый юнит. Причина уйти от монорепо — см. DECISIONS.md (ADR-0012): мердж в main одного сервиса не должен триггерить пересборку/деплой остальных.

gitlab.local/mybit/
├── platform/                  # общие доки, корневой CLAUDE.md, docker-compose.yml
│   ├── CLAUDE.md
│   ├── docs/                  # эта документация (общая память проекта)
│   ├── docker-compose.yml     # локальный запуск всего
│   └── .claude/                # агенты/скиллы Claude Code (см. TOOLING.md)
├── proto/                     # gRPC-контракты — единый источник правды
├── core-service/               # Go + gRPC + Postgres, свой CLAUDE.md
├── telegram-bot/                # Python, свой CLAUDE.md
├── miniapp-frontend/            # тонкий HTML/JS/TS, свой CLAUDE.md
├── wger-sync-worker/            # Go, свой CLAUDE.md
├── google-health-sync/          # Go, свой CLAUDE.md
├── inbody-parser/                # Python, свой CLAUDE.md
├── applehealth-import/           # Go, свой CLAUDE.md
└── wger/                        # форк wger-project/wger, свой remote upstream

Локально всё это — соседние директории под одной рабочей папкой (~/GoLandProjects/mybit/), каждая со своим .git. Рабочая папка сама по себе git-репозиторий mybit/platform (гитигнорит остальные директории, они не его файлы). Такая раскладка позволяет Claude Code открывать один workspace и параллелить работу по субагентам (.claude/agents/), при этом каждый сервис коммитит и деплоится независимо.

wger — отдельный репозиторий и хард-форк (ADR-0020): upstream не отслеживается, мерджей из wger-project/wger не будет. Обновления безопасности переносятся вручную и осознанно — это принятая цена за отсутствие вечного ребейза.

Инфраструктура и сеть

«Мини-ПК» из ранних заметок — это на практике Proxmox 192.168.1.100 в домашней сети, а рантайм mybit — VM 103 pet / 192.168.1.103 (ADR-0015). Инфраструктура управляется отдельным репозиторием ~/infra/homelab-infra (Ansible); все прогоны по mybit — строго с --limit pet, остальные VM не трогаем.

Хост Что держит
proxmox 192.168.1.100 Гипервизор всех VM
gitlab 192.168.1.101 GitLab CE + instance-раннер
pet 192.168.1.103 Рантайм mybit + group-раннер mybit
VPS 144.124.250.219 Caddy и конец туннеля — и больше ничего

Ресурсы pet: 4 ядра, 7168 МБ с ballooning от 4096, 60 ГБ. Размер посчитан по замеренному потреблению остальных VM (~21.9 ГБ из 31.5 ГБ), гипервизору оставлено ~2.4 ГБ. Стек целиком укладывается примерно в 1.5 ГБ при прижатых shared_buffers, так что запас есть — при условии, что не тащим Grafana/Prometheus/Kafka. concurrent = 1 у раннера остаётся: хост совмещает сборки и рантайм.

Как код попадает на pet

Раннер живёт на том же хосте, что и сервисы, и его job-контейнеры получают /var/run/docker.sock. Поэтому CI собирает образ тем же демоном, который его потом запускает — container registry не нужен вовсе (ADR-0018).

git push в mybit/<service>
   → пайплайн на раннере pet (tags: [pet])
   → lint → test → docker build (демон pet, образ остаётся локальным)
   → deploy (вручную): docker compose up -d <service> в /srv/pet/apps/mybit

Мердж в один репозиторий не задевает пайплайны остальных — ради этого и делались отдельные репозитории (ADR-0012).

Выход наружу — работает

Проверено 2026-08-20 запросами по реальному DNS, с телефонного пути, не в обход.

Наружу выставлены шесть поддоменов зоны arvberezin.online. Пять из них обслуживает Caddy на VPS, проксируя в обратный SSH-туннель до дома:

Домен Порт на VPS Куда ведёт Проверка
mybit 9080 pet:8080 — бот и Mini App 502 — Caddy жив, за портом пусто: бот ещё не задеплоен
gitlab 9083 gitlab:80 302 на форму входа
torrserver 9081 home:8090 200
qbt 9082 home:8080 qBittorrent 200
files 9084 home:8085 WebDAV 401, просит авторизацию
proxmox 9025 proxmox:22только SSH по HTTPS не публикуется, и это осознанно

Сертификаты Let's Encrypt выпущены и обновляются автоматически (cert=0 у всех пяти). Блок VPN владельца — :443, cdn.arvberezin.online с forward_proxy — не тронут и работает как раньше.

502 у mybit — это ожидаемое и правильное состояние: вся цепочка DNS → Caddy → TLS → туннель → pet:8080 собрана и ждёт, когда за портом появится бот. До этого момента публиковать нечего.

proxmox намеренно стоит особняком. Блока в Caddy для него нет и не должно быть: порт 9025 ведёт на proxmox:22, то есть SSH, а не на веб-интерфейс (он на 8006). Админка гипервизора наружу по HTTPS не выставляется — доступ к ней только через SSH-туннель. Имя в DNS заведено ради удобства ssh, а не ради браузера.

Как проверять, что домен жив. Все шесть имён — явные A-записи на 144.124.250.219, wildcard в зоне убран (проверено 2026-08-20). Но проверять доступность всё равно запросом, а не сравнением адресов: LESSONS-0010 описывает, как сравнение dig с ожидаемым IP дважды увело в неверную сторону.

curl -sS -o /dev/null -w "%{http_code} cert=%{ssl_verify_result}\n" \
  https://mybit.arvberezin.online/

Белого IP дома нет и не планируется — он и не нужен. Туннель обратный: pet сам открывает исходящее соединение к VPS, входящих подключений к дому не требуется. Работает за NAT, за серым IP и за CGNAT. Динамический DNS здесь не помог бы: он решает задачу «публичный IP меняется», а не «публичного IP нет».

VPS — только терминация TLS и конец туннеля. Сервисы проекта там не запускаются никогда: у него 1.6 ГБ памяти, и на нём уже крутятся naiveproxy и два SOCKS5 слушателя. Весь рантайм — на pet; на VPS мы добавляем только новый именованный блок Caddy и обратный туннель, не трогая существующие прокси и их порты.

Домен: mybit.arvberezin.online (зона своя, сертификаты Caddy уже работают на соседнем поддомене).

Интернет → Caddy на VPS (TLS, Let's Encrypt по домену)
        → обратный SSH-туннель (autossh)
        → pet (wger / bot)
  • На VPS сейчас один блок — :443, cdn.arvberezin.online с forward_proxy, то есть VPN владельца. Добавляется именованный блок для mybit; правила безопасной правки — ADR-0026 (validate до применения, reload вместо restart, существующий блок не трогаем, откат одной командой).
  • HTTPS обязателен: Telegram не откроет Mini App без валидного TLS.
  • Cloudflare Tunnel не использовать — заблокирован в РФ (LESSONS-0003).
  • Naiveproxy не подходит: это forward-прокси (наружу), а нам нужен reverse (внутрь) — LESSONS-0004.

Поэтапность

Не всё строится сразу. Порядок и зависимости — в ROADMAP.md. Коротко: сначала вход (туннель с TLS, хард-форк wger, регистрация через бота), потом логирование подхода, потом core + wger-sync-worker, потом остальные источники и проверка гипотезы. Инфраструктура доступа идёт первой не по вкусу, а потому что без публичного HTTPS не работают ни Mini App, ни OAuth (ADR-0025).