ARCHITECTURE — общая картина¶
Детали по каждому сервису — в SERVICES.md. Схема данных и контракты — в DATA_MODEL.md.
Принцип¶
Два «контура» с разной ответственностью:
- Контур ввода тренировок —
telegram-bot(единственная точка входа: регистрация, запуск Mini App, OAuth, приём файлов — ADR-0023) +miniapp-frontend(тонкий UI) +wger(движок). Здесь пользователь логирует подходы с реальным временем. - Контур сбора и анализа —
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.xml→applehealth-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).