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

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" и не задавать алиас, совпадающий с именем образа.