LESSONS — журнал ошибок и выводов¶
Append-only. Пишем правило на будущее, а не пересказ инцидента. Новые записи
снизу. Когда дописывать — см. корневой CLAUDE.md: (а) пользователь поправил по
сути, (б) наткнулись на неочевидный подводный камень, (в) решение оказалось не
таким, как ожидалось.
Шаблон:
## LESSONS-NNNN: Заголовок — правило
ДАТА: YYYY-MM-DD
ЧТО СЛУЧИЛОСЬ: коротко.
ПОЧЕМУ: корневая причина.
ПРАВИЛО: что делать в следующий раз.
LESSONS-0001: Готовые логгеры тренировок не дают времени подхода¶
ДАТА: 2026-06-30 ЧТО СЛУЧИЛОСЬ: Проверены FitNotes, Hevy, Strong, Jefit и интервальные таймеры в надежде не писать логирование самим. ПОЧЕМУ: Логгеры пишут время на уровне всей тренировки; таймеры не знают про «упражнение/вес/повторы». Гибрида с экспортом нет. ПРАВИЛО: Не искать снова готовое решение для логирования подходов — его нет. Точное время подхода фиксируем сами, это ядро ценности проекта (ADR-0003).
LESSONS-0002: Фронтенд wger — React, а не Vue¶
ДАТА: 2026-06-30
ЧТО СЛУЧИЛОСЬ: Планировали форкнуть фронт wger, считая его лёгким Vue-приложением.
ПОЧЕМУ: Фронт живёт в отдельном репозитории wger-project/react и написан на
React/TypeScript; переделывать его под сценарий «начал/закончил подход» дорого и
хрупко.
ПРАВИЛО: Перед оценкой «форкнем UI» проверять, где реально лежит фронт и на чём
он написан. Для mybit UI логирования — свой тонкий мини-апп (ADR-0005).
LESSONS-0003: Cloudflare Tunnel заблокирован в РФ¶
ДАТА: 2026-06-30 ЧТО СЛУЧИЛОСЬ: Очевидный способ выставить мини-апп наружу — Cloudflare Tunnel — не заработал. ПОЧЕМУ: Сервис заблокирован в РФ. ПРАВИЛО: Не предлагать Cloudflare Tunnel для этого проекта. Наружу выходим через обратный SSH-туннель + Caddy на VPS (ADR-0008).
LESSONS-0004: Naiveproxy — forward-прокси, для входящего трафика не годится¶
ДАТА: 2026-06-30 ЧТО СЛУЧИЛОСЬ: На VPS уже стоял naiveproxy, и возникла идея пустить через него трафик к мини-аппу. ПОЧЕМУ: Naiveproxy — forward-прокси (наружу из сети), а нужен reverse (внутрь, к нашему сервису). Разные задачи. ПРАВИЛО: Для входящего трафика использовать Caddy (его серверная часть уже стоит на VPS — добавляется сайт-блок), а не переиспользовать forward-прокси.
LESSONS-0005: Удалённые плагины buf требуют аккаунта BSR¶
ДАТА: 2026-08-13
ЧТО СЛУЧИЛОСЬ: buf generate с плагинами buf.build/protocolbuffers/go работал
локально, но в CI падал с permission_denied: 403 Forbidden.
ПОЧЕМУ: Выполнение удалённых плагинов идёт через Buf Schema Registry и требует
аутентификации; локально помогал кэш.
ПРАВИЛО: В proto использовать локальные плагины (go install
protoc-gen-go, protoc-gen-go-grpc; Python — через grpc_tools.protoc).
Генерация не должна зависеть от внешнего сервиса и аккаунта.
LESSONS-0006: Проверка «код не устарел» молча врёт без git в образе¶
ДАТА: 2026-08-13
ЧТО СЛУЧИЛОСЬ: Джоба сверки сгенерированного кода в python:3.12-slim сообщала
«gen/python is stale», хотя код был актуален.
ПОЧЕМУ: В slim-образе нет git, git diff падал, и if ! git diff уходил в
ветку «устарело». Ошибка выглядела содержательной, но была ложной.
ПРАВИЛО: Проверка вида if ! <команда> должна отличать «различия есть» от
«команда не отработала». Для CI-джоб со сверкой брать образ, где git есть.
LESSONS-0007: CI job token по умолчанию не ходит в соседний проект¶
ДАТА: 2026-08-13
ЧТО СЛУЧИЛОСЬ: Пайплайн core-service падал на клонировании submodule proto:
Authentication by CI/CD job token not allowed from core-service to project #4.
ПОЧЕМУ: В GitLab у проекта есть inbound-allowlist для CI job token; проекты одной
группы туда автоматически не попадают.
ПРАВИЛО: Заводя нового потребителя mybit/proto, добавлять его в allowlist:
POST /projects/4/job_token_scope/allowlist с target_project_id. Иначе
submodule не склонируется в CI.
LESSONS-0008: Контейнеры не резолвят mDNS-имена (*.local)¶
ДАТА: 2026-08-13
ЧТО СЛУЧИЛОСЬ: Регистрация GitLab Runner на pet падала:
lookup gitlab.local on 127.0.0.53:53: no such host.
ПОЧЕМУ: gitlab.local раздаётся через Avahi/mDNS. Хост его резолвит, а
контейнер — нет: у него свой resolv.conf и mDNS ему недоступен.
ПРАВИЛО: Любому контейнеру, который ходит на *.local, прописывать
extra_hosts / --add-host (для раннера — и самому контейнеру, и его
job-контейнерам через --docker-extra-hosts).
LESSONS-0009: url.ParseRequestURI пропускает host:port без схемы¶
ДАТА: 2026-08-13
ЧТО СЛУЧИЛОСЬ: Валидация WGER_BASE_URL принимала wger:8000, хотя должна была
требовать абсолютный URL. Поймал собственный тест.
ПОЧЕМУ: wger:8000 разбирается как схема wger с opaque-частью 8000 — формально
это валидный URI, ошибки нет.
ПРАВИЛО: Проверять Scheme (http/https) и непустой Host явно, а не полагаться
на отсутствие ошибки парсинга. И писать тест на негативный кейс — он тут и сработал.
LESSONS-0010: Диагноз «домен ведёт не туда» ставится запросом, а не сравнением адресов¶
ДАТА: 2026-08-20 (переписан в тот же день — дважды)
ЧТО СЛУЧИЛОСЬ: Три состояния подряд, и два вывода из них оказались неверными.
1. dig mybit.arvberezin.online вернул 198.20.0.66, VPS — 144.124.250.219,
плюс в зоне отвечал wildcard. Вывод «записи нет, домен смотрит на парковку»
— неверный: запросы через эти адреса возвращали 200 с валидным
сертификатом Let's Encrypt, то есть доходили.
2. Вывод «значит, между DNS и VPS стоит прокси» — тоже неверный, или по
меньшей мере недоказуемый: через несколько часов wildcard исчез, появились
явные A-записи, и всё стало резолвиться прямо в 144.124.250.219.
3. Что происходило в промежутке, установить уже нельзя. Вероятнее всего записи
правились прямо во время проверок, и часть ответов приходила из кэша.
ПОЧЕМУ: Сравнение адресов отвечает не на тот вопрос. «Резолвится не в тот IP» не
значит «трафик не доходит», а «резолвится в нужный IP» не значит, что сервер за
ним отвечает. Оба раза ошибка была одна: вывод делался по dig, а проверялось
запросом уже потом.
ПРАВИЛО: Проверять запросом, и только им:
curl -sS -o /dev/null -w "%{http_code} ip=%{remote_ip} cert=%{ssl_verify_result}\n" \
https://имя.домена/
cert=0 и осмысленный код означают, что путь рабочий, каким бы ни был адрес.
Отдельно: TLS-ошибка при обращении к домену чаще означает «нет блока под этот
SNI», чем «не тот сервер» — сначала смотреть конфиг веб-сервера, а не DNS.
И вывод об инфраструктуре, которую правят параллельно, стоит помечать датой:
он живёт часы, а не месяцы.
LESSONS-0011: Тест, умеющий пропускаться, делает джобу зелёной впустую¶
ДАТА: 2026-08-20
ЧТО СЛУЧИЛОСЬ: Тесты core-service против Postgres написаны так, чтобы
пропускаться без БД (CORE_TEST_DATABASE_URL не задан) — иначе make test
требовал бы докера. В CI база подавалась сервисом, но тесты всё равно
пропускались, а джоба была бы зелёной, ничего не проверив. Поймала это
отдельная проверка, поставленная рядом именно на этот случай.
ПОЧЕМУ: Две причины подряд.
1. services: - name: postgres:16-alpine / alias: postgres — GitLab сам
выводит алиас postgres из имени образа, поэтому явный алиас конфликтовал:
Skipping alias "postgres" ... already in use, и наш алиас отбрасывался.
2. Раннер работает через хостовый docker.sock, сервисный контейнер — сосед, а не
потомок. Без FF_NETWORK_PER_BUILD: "true" он не резолвится по имени.
ПРАВИЛО: Если тест умеет пропускаться, в CI обязана стоять проверка, что он не
пропустился. И она должна различать три исхода, а не два: прошёл, упал, не
запускался. Оговорка про текущую реализацию: проверка в core-service покрывает
один тест из четырёх — остальные три пропустятся незаметно, если их условие
когда-нибудь разойдётся. Сегодня условие у всех одно, поэтому и одного хватает;
разойдётся — проверку надо расширять. Первая версия печатала «не запускался» на любое отсутствие --- PASS
и увела в неверную сторону — на самом деле тест запускался и падал, что было видно
только по времени: 17 секунд вместо мгновенного пропуска.
Для сервисов в GitLab CI на хостовом docker.sock: FF_NETWORK_PER_BUILD: "true" и
не задавать алиас, совпадающий с именем образа.