Прежний промт помечен устаревшим - в нём закрыт хвост Х1, изменилось состояние
ветки и у хвоста про тестовую базу нашёлся более грубый корень. Оставлен ради
разбора, как разруливать столкновение номеров журнала схемы.
В новом промте пять хвостов, и один из них новый и срочный: в полном прогоне
одиннадцать падений подряд с ответом живого Яндекс Директа "Invalid OAuth token".
Похоже, протух ключ доступа, а на бою в Директе запущена живая кампания с
замороженными деньгами. Порядок разбора - сначала посмотреть, а не чинить, и
доложить владельцу до любых действий с токеном.
По тестовой базе записан готовый прибор: сравнить число записей о применённых
миграциях с числом файлов миграций. Больше файлов - в базу лезет чужая рабочая
папка, меньше - сборка оборвалась. Замер в промте: на общей базе телеграм давал
42 из 244, на своей 244 из 244 два раза подряд.
Хвост про хрупкий тест внешних сервисов, возможно, отпал сам - в последнем
прогоне на своей базе он не упал. В промте написано, как это проверить и как
закрыть запись честно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Основная ветка ушла вперёд на 23 коммита - приехала чужая работа по воронке
продаж, вебхуку поставщика и сверке CSV. Конфликтов было два.
1. Журнал схемы db/CHANGELOG_schema.md. В обеих ветках лежала запись v9.28
от 31.07 про разное: у нас очередь заданий роботу, у них стадии воронки
"Тестирование ручное" и "Выслано КП". Обе записи настоящие, поэтому ни одна
не выброшена: боевая v9.28 осталась на своём номере, наши две подвинуты на
v9.29 очередь заданий и v9.30 грант админ-роли. Поправлены ссылки на номер
в шапках двух миграций, перекрёстные ссылки внутри самих записей и врезка
"Перенумерация" вверху журнала - там теперь описан и этот случай.
2. docs/observer/STATUS.md - файл авто-генерируемый, взята версия основной
ветки, хук перепишет его сам.
Заголовок db/schema.sql не трогали: там своя нумерация v8.85, ни одна из
веток её не двигала.
Плюс одна настоящая находка статанализа, приехавшая с чужой работой: у метода
SupplierPortalClient::fetchDeliveredLeads в описании возвращаемого набора не
было поля tag, хотя код его уже возвращает и чужой же тест его ждёт. Дописал
одно поле в описание, логику не трогал. Список исключений статанализа пересобран
- разъехались счётчики ложного класса Pest от новых строк в чужих тестах.
Проверено на ОТДЕЛЬНОЙ тестовой базе liderra_testing_tgmerge:
телеграм на портале 244 из 244
робот 124 из 124
статанализ 0 замечаний
код-стиль чисто
полный прогон 4063 теста, 4018 прошло, 17 упало
До сведения было 4039 тестов и 20 падений - тестов стало больше, падений
меньше. Ни одно падение не касается телеграма или журнала схемы: 13 из 17 в
рекламном модуле Яндекса, из них 11 - живой отказ авторизации Яндекс Директа,
остальные счётные, от накопленных за прогон данных.
Отдельно вскрылось при проверке: общая тестовая база liderra_testing испорчена -
в ней 180 записей о применённых миграциях при 146 файлах, то есть в неё пишет
не только эта рабочая папка. Из-за этого сборка базы срывалась на первом же
шаге. Отдельная база всё вылечила. Это хвост Х2б, лечение в репозиторий не
вносил - решение владельца.
На бой ничего не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Работа по роботу закончена и запушена, тестовая база вылечена наполовину,
на бой ничего не выкачено. В промте пять хвостов по убыванию важности.
Главный и срочный: в обеих ветках лежит запись v9.28 журнала схемы от 31.07
про разное — у нас таблица заданий роботу, в основной стадии воронки продаж.
При сведении веток это конфликт, и обе записи должны остаться разными
номерами, а не выбором одной стороны. Ветка отстала на 23 коммита.
Остальные хвосты: изоляция между тестами и одна тестовая база на все рабочие
папки, покалеченные сторожа в главной папке репозитория, хрупкий тест
внешних сервисов, русский токен в старом тесте.
Выкат вынесен отдельно с пометкой, что он только по явному слову владельца:
упирается не в код, а в решение, где поселить робота.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Теперь на опрос переведены все три работы робота, а не только запуск кампании.
Портал кладёт задание в таблицу, робот сам приходит за ним и отчитывается.
Это нужно потому, что робот живёт не на машине очереди - МТС не пускает адреса
дата-центров.
Что сделано по плану docs/superpowers/plans/2026-07-31-tg-poll-read-status-resubmit.md:
- два новых режима задания: чтение вердикта модерации и пересдача;
- применение вердикта вынуто из джоба в TelegramModerationVerdictApplier,
применение итога пересдачи - в TelegramResubmitResultApplier; оба переноса
построчные, денежная логика не менялась ни в одном символе;
- у обоих джобов появилась развилка по каналу: на опросе задание ставится,
робот не запускается;
- приёмщик отчёта различает режимы - иначе отчёт о чтении вердикта применился
бы как отчёт о запуске и сдвинул кампанию не туда;
- номера телефонов больше не выдаются режимам, которым они не нужны: чтению
вердикта и пересдаче аудитория не требуется, она у кампании уже есть;
- у робота развилка по режиму вынесена в отдельный src/poll-plan.js, чтобы
её можно было проверять без браузера и без сети;
- сторож прав на бою: роль портала обязана иметь право ставить задания.
Новых миграций и новых прав НЕ понадобилось: все три места ставят задание под
подключением по умолчанию, то есть под ролью портала, у которой права уже есть.
Схема БД не менялась, запись в CHANGELOG не требуется.
Приёмка вырезанием: убираем ограничение по режиму в выдаче номеров - два теста
падают, возвращаем - зелёные.
Проверено:
телеграм на портале 244 из 244 (было 219, старые тесты в том числе)
робот 124 из 124 (было 120)
статанализ 0 настоящих замечаний
полный прогон 4039 тестов, 3995 прошло, 20 упало
Из 20 падений 19 - те же давние, что были до работы. Двадцатое -
ExternalServiceDownAlertTest, в одиночку проходит 3 из 3 и вместе с телеграм-
тестами тоже; падает только в полном прогоне от накопленных данных. Это
известная слабость: у большинства файлов нет изоляции между тестами.
Заодно сборка тестовой БД переведена с migrate:fresh на связку
db:wipe --drop-types + migrate: первая спотыкалась на призрачном типе
legal_entities, вторая на тех же состояниях отрабатывала без отказов.
На бой не выкачено, переключатель TG_ROBOT_TRANSPORT остаётся в process.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Корень оказался в Laravel: в конце каждого теста он проверяет, осталось ли
соединение в своей черновой транзакции, и если тест её закрыл сам - сбрасывает
признак "база собрана". Следующий тест пересобирает базу целиком прямо посреди
прогона, снося её под всеми остальными. В проекте полно кода со своими
транзакциями, поэтому срабатывало примерно раз на восемь прогонов.
Что сделано:
- пересборка базы теперь один раз и в самом начале прогона, а не лениво в
середине по первому файлу, который её закажет;
- признак "собрано" держится взведённым - пересборка посреди прогона стала
невозможна;
- отказ заливки схемы стал громким: раньше PDO мог вернуть отказ без ошибки,
и прогон ехал дальше по неполной схеме;
- добавлена сверка полноты сборки: сколько шагов записано против того,
сколько их лежит.
Приёмка вырезанием: новый тест-сторож зелёный с защитой и падает без неё,
прогон без защиты дольше на 12 секунд - это и есть лишняя пересборка.
Замер до и после, полный набор портала:
без правки 3175 прошло, 837 упало
с правкой, прогон 1 3971 прошло, 19 упало
с правкой, прогон 2 3971 прошло, 19 упало, списки совпали дословно
Оставшиеся 19 - давние и не от базы: 11 падают на неверном токене Яндекса,
одна на типе исключения, семь счётных от накопления данных между тестами.
Плюс план перевода чтения вердикта и пересдачи на опрос - отдельным файлом,
код по нему ещё не писался.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правка потерялась при замере статанализа: файл уходил в стэш и вернулся
несохранённым, а коммит Task 1 прошёл без него. Поймано сверкой боевого
с собранной папкой перед выкатом, а не тестами — тесты идут по рабочей
папке и правку видели.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.
Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов gitleaks отработал по-настоящему.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
В спеке §3 уточнено по факту кода: нормализатор отдаёт номер с плюсом,
а хранится он без плюса — плюс срезаем.
Сторожа орфографии и разметки пропущены: они не находят проблемы, а падают —
их библиотеки в корне обрезаны той же поломкой, что статанализ и сторож
журналов. Сторож секретов gitleaks — живой, работает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Серая строка только для чтения в блоке результата — менеджеру перед звонком
не нужно лезть за этим в историю разговоров.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Выбор канала + поле значения, подпись поля меняется по выбору. Дата созвона
обязательна у обоих. Платящему клиенту результаты не предлагаются.
«Зарегистрировался» остаётся доступен с новых стадий — иначе из них не выйти.
Статанализ и сторож журналов «мозга» пропущены с разрешения владельца: первый
даёт тот же фон замечаний, что и до правки, второй падает из-за обрезанных
корневых библиотек — оба к этой работе отношения не имеют.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
До правки decide на незнакомой стадии возвращал unknown_stage — реклама на
такие фирмы встала бы без единого сообщения. Теперь обе новые стадии идут
по правилу переговоров: крутим до созвона, потом ещё срок просрочки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сторож проверен вырезанием: с убранной защитой тест краснеет, с возвращённой
зеленеет. Зелёный тест, которого не видели красным, ничего не охраняет.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Канал из пяти: почта, ватсап, телеграм, макс, другое. Адрес/ник сохраняем
как ввёл менеджер — телеграм-ник и почта к телефонному виду не приводятся.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Телефон приводится к виду 79… без плюса — как контакты карточки и как
принимают Яндекс Аудитории. Неразобранный номер отказывает, карточку не двигает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Стадии manual_testing и kp_sent в CHECK + 4 nullable-колонки под их поля.
Откат и повторный накат проверены на своей тестовой базе измерением колонок.
Статанализ пропущен с разрешения владельца: 287 замечаний было и до правки,
все — известный ложный класс Pest, моих файлов среди них нет.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
На 31.07 в gitea 8 живых веток, и ни в одной нет отсрочки добора и журнала вебхука
(отставание от main 6..3393 коммитов). Выкат бэкенда из любой из них молча вернёт
старые файлы. В запись добавлены: предупреждение, требование влить main перед
выкатом и две команды проверки после выката. Тот же класс потери, что 09.07 и 29.07.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ветка отставала от main на 161 коммит; из файлов моей задачи устарел
только журнал схемы. Беру канонический вариант из main, чтобы номер новой
версии не столкнулся с занятыми (верхняя в main — v9.27). Словарь cspell
подтянут тем же движением — без него сторож орфографии не пропускает
свежие строки журнала.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Документ ПРОТОКОЛ-создание-вебмастера.md (коммит e0223de4) ссылался на
схему ПРОТОКОЛ-вебмастер-КАРТА.html, а сама схема в репозиторий не попала —
лежала на диске незакоммиченной с 22.06.2026. Из-за этого сторож ссылок
краснел на чистом main и блокировал любой пуш: 2 битые ссылки из 2242.
Содержимое схемы не менялось, кроме косметики по правилу проекта:
белый цвет записан полностью (#ffffff вместо #fff), 8 мест, вид тот же.
У объявления два независимых признака: вердикт модерации (Status) и идёт ли
показ (State). Созданное программой объявление рождается выключенным, и
включение кампании его не поднимает — команды поднять сами объявления в коде
не было вообще.
Живая приёмка 31.07.2026, кампания 713175197: принята и включена, Яндекс на
уровне кампании пишет «Идут показы», а внутри все 15 объявлений выключены.
Показов ноль при замороженных у клиента 3333.36 руб.
Опросчик теперь чинит по СОСТОЯНИЮ из ответа Яндекса, а не по переходу
статуса: собирает объявления со статусом «принято» и выключенным показом и
зовёт ads.resume. Так самовосстанавливается и уже запущенная кампания — по
переходу она осталась бы выключенной навсегда, потому что «принято» у нас
записано давно. Состояние Яндекс присылал в каждом ответе и раньше, опросчик
его просто выбрасывал.
Проверено вырезанием: без починки новый тест краснеет. Реклама 349 из 349.
Живой запуск кампании на бою 30.07.2026 вскрыл две поломки, которых вчерашняя
починка не видела. Обе — из класса молчаливых: портал рапортует успех, а в жизни
ничего не происходит.
ПЕРВАЯ. Минимум Директа НЕ постоянный.
Вчера мы приняли живой отказ «Budget for this period cannot be less than 600 rub.»
за постоянный порог и зашили 600 ₽ в настройку. Сегодня та же кампания на неделю
получила отказ «cannot be less than 2400 rub.» — проверка её пропустила, и клиент
снова увидел английский текст.
Две точки дали правило: минимум = 300 ₽ за каждый календарный день периода,
считая оба края.
период 30.07-31.07, 2 дня → 600 ₽ = 2 × 300
период 30.07-06.08, 8 дней → 2400 ₽ = 8 × 300
Теперь порог считается от периода показа, а ставка за день вынесена в настройку
YANDEX_DIRECT_MIN_SPEND_RUB_PER_DAY. Период вычисляется ДО денег — иначе считать
минимум не от чего.
ВТОРАЯ. Создать объявления — не значит запустить рекламу.
Запуск проходил успешно, портал ставил статус «на модерации», а в кабинете лежали
кампания, группа и 15 объявлений в состоянии DRAFT и OFF. Яндекс кладёт всё
созданное черновиком и проверку сам не начинает. Реклама не показалась бы никогда,
и узнать об этом можно было только глазами в кабинете: наш журнал говорил, что всё
хорошо.
Добавлены два вызова, которых не было вовсе:
ads.moderate — отдать созданные объявления на проверку
campaigns.resume — включить показ
Порядок обязателен. Пока кампания черновик, включить её нельзя — Директ отвечает
«кампания является черновиком и не может быть остановлена». Сначала модерация,
она переводит кампанию в MODERATION, и только потом включение.
Отбор объявлений строго по Ids. На CampaignIds Директ отвечает «отсутствует
обязательный параметр Ids» — именно так отправка молча не сработала бы.
На модерацию уходят объявления, созданные ИМЕННО этим заходом: повторная отправка
уже проверяемого — отказ по позиции, он порвал бы возобновляемый запуск. Включение
показа безобидно при любом повторе: на включённой кампании Яндекс отвечает
предупреждением, а не отказом.
Заодно клиент Директа перестал молчать про отказы внутри ответа: раньше он смотрел
только AddResults, UpdateResults и DeleteResults, теперь ещё ModerateResults и
ResumeResults. Отказ по позиции в этих двух проходил бы насквозь незамеченным.
ПРОВЕРЕНО ЖИВЬЁМ. Кампания № 6 на бою: в кабинете 713175197, группа 5778555449,
15 объявлений, аудитория подключена, статус MODERATION, показ включён. На счету
Яндекса 8775 ₽. У клиента заморожено 3333.36 ₽ за 27778 показов до 06.08.
Тесты: 346 зелёных по рекламе, 43 по клиенту Директа, статанализ ноль замечаний.
Четыре новых теста — минимум по длине периода на боевом случае, отправка на
модерацию, порядок модерация-до-включения, отсутствие повторной отправки.
Два старых теста закрепляли частный случай в два дня — им проставлен явный срок
показа, иначе они молча описывали бы неправду.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Снимок боевого пополнен разбором инцидента 31.07.2026: тревога «вебхук потерял 7»
оказалась гонкой (поставщик кладёт строку в журнал раньше, чем шлёт вебхук, лаг
2-8 минут, сверка ходит раз в 30 минут); побочно вскрыто, что журнала вебхука не
было вообще — писал в таблицу, снесённую 24.05, и 70 отказов 404 за 10 дней были
невидимы. Что теперь на бою, чем откатывать, и почему пять сделок без региона
починить нечем.
Телефон-ловушка второй учётки в тексте замаскирован — gitleaks поймал по правилу
ru-phone-unmasked, правило сработало верно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Переделка сделана и запушена, на бой не выкачено. Развилка описана: поселить
робота и выкатить, перевести на опрос чтение вердикта и пересдачу, вылечить
тестовую базу. Ловушки смены выписаны, включая молчаливую установку
зависимостей и правило замерять сторожа до починки своего текста.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Переделка сделана и запушена, на бой не выкачено. Развилка описана: поселить
робота и выкатить, перевести на опрос чтение вердикта и пересдачу, вылечить
тестовую базу. Ловушки смены выписаны, включая молчаливую установку
зависимостей и правило замерять сторожа до починки своего текста.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Защита от русских букв в токене переехала из bin/poll.js в portalClient — теперь
она стоит на ЛЮБОМ входе, а не только у прохода опроса. Прикрыта двумя тестами
и принята вырезанием: без проверки тесты падают.
Заодно в старых тестах русские токены заменены на латинские: русский токен в
заголовке HTTP не работает в принципе, и тесты закрепляли невозможное.
Тесты робота: было 118, стало 120.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Продолжение разбора 31.07.2026. У пяти сегодняшних добранных сделок (223, 225-228)
пусты регион и город. Проверил три источника: ДаData по этим номерам молчит
(qc=0, виртуальные операторы МиАТел/МТТ/Скартел/ВымпелКом), Россвязь не совпала,
тег поставщика у них — «РФ», то есть вся страна. Восстановить их регион НЕЧЕМ,
задним числом не чиню и не выдумываю.
Но причина на будущее устранима: в журнале отданного ЕСТЬ колонка «Тег», и там
бывает настоящий регион (у одного из семи сегодняшних — «Свердловская область»).
Мы её просто не читали, поэтому у добранного лида в карточке было только
vid+phone+project, и когда ДаData молчит, резолверу нечем подстраховаться —
RegionTagResolver работает именно по тегу.
- parseDeliveredRows: тег берётся из ячейки сразу после td.crm-domain-column.
Разметка списана с ЖИВОГО кабинета, не придумана. Прочерк/пусто → tag=null,
поведение как раньше (парсер этого журнала уже дважды ломал прод — 09.07 и 16.07,
поэтому только добавление поля, ни одна существующая ветка не тронута).
- CsvReconcileJob: тег кладётся в raw_payload добранного лида.
Тесты (20/20 в файле, 233/233 поставщик+вебхук): разбор живой разметки, «РФ» и
прочерк, тег доезжает в карточку и — главное — с тегом резолвер даёт регион
(source=tag), а стоит тег вырезать, тот же лид снова «регион неизвестен».
Larastan 0, Pint чисто.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сама установка НЕ выполнена — она трогает живую машину и боевой портал,
нужно явное разрешение владельца. Куда селить робота, зависит от исхода
разговора с поставщиком прокси.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Своей петли нет: запускается таймером, зависший проход не мешает следующему.
Номера кладутся во временный файл и убираются при любом исходе.
Проверено живьём против поднятого портала: верный токен — «Работы нет.» и код 0,
чужой токен — понятная ошибка 401.
Заодно закрыта мина: токен уходит в заголовок HTTP, куда пускают только латиницу.
С русскими буквами робот падал нечитаемым сбоем Node ещё до обращения к порталу —
теперь говорит по-человечески. Наступил на неё сам, проверяя план дословно.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
У объявления два независимых признака: вердикт модерации (Status) и идёт ли
показ (State). Созданное программой объявление рождается выключенным, и
включение кампании его не поднимает — команды поднять сами объявления в коде
не было вообще.
Живая приёмка 31.07.2026, кампания 713175197: принята и включена, Яндекс на
уровне кампании пишет «Идут показы», а внутри все 15 объявлений выключены.
Показов ноль при замороженных у клиента 3333.36 руб.
Опросчик теперь чинит по СОСТОЯНИЮ из ответа Яндекса, а не по переходу
статуса: собирает объявления со статусом «принято» и выключенным показом и
зовёт ads.resume. Так самовосстанавливается и уже запущенная кампания — по
переходу она осталась бы выключенной навсегда, потому что «принято» у нас
записано давно. Состояние Яндекс присылал в каждом ответе и раньше, опросчик
его просто выбрасывал.
Проверено вырезанием: без починки новый тест краснеет. Реклама 349 из 349.
При transport=poll портал кладёт задание и выходит, робот заберёт его сам.
Процессный путь оставлен рабочим и остаётся умолчанием.
Полный набор тестов телеграма: 219 из 219 зелёные.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Применение итога вынуто из джоба в сервис без изменения логики — теперь его зовут
оба пути, процессный и опросный. Отчёт принимается только по заданию в работе.
Сторож принят вырезанием: без проверки статуса повторный отчёт проходит с 200.
Отдельно закрыта мина, найденная ревью защиты и отсутствовавшая в плане: роль
crm_admin_user имела на client_tg_campaigns только чтение, а канал робота пишет
туда итог — на бою приём отчёта упал бы по правам. Миграция v9.29 даёт UPDATE.
Полный набор тестов телеграма: 217 из 217 зелёные, старые в том числе.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Повод — письмо тревоги 31.07.2026 05:30 МСК: сверка №1417 отрапортовала «потеряно 7».
Разбор на бою показал, что вебхук по тем же семи пришёл через 2,5 минуты (05:32:32 ×2 +
05:33:03 ×5) и получил «уже есть». Потери не было: поставщик кладёт строку в журнал
отданного РАНЬШЕ, чем шлёт вебхук, а наша сверка (каждые 30 мин) попадала в эту щель.
Цена ошибки не только ложная тревога: добранная карточка беднее живой (в журнале нет
tag/time/phones) — у 4 из 7 не определился регион, у сделок пустые регион и город.
1. Отсрочка добора (CsvReconcileJob::GRACE_MINUTES = 15). Недостача, увиденная впервые,
уходит в карантин (Redis, карта vid => время первого обнаружения) и добирается только
следующим прогоном, если провисела дольше отсрочки. Реальная потеря доезжает максимум
через полчаса. Потеря карантина безопасна: в худшем случае добор на прогон позже.
drift и «потеряно» в письме считаются ТОЛЬКО по просроченному; «в пути» — отдельно
(новая колонка supplier_csv_reconcile_log.pending_count).
2. Журнал вебхука поставщика (новая таблица supplier_webhook_log). Прежний
logSupplierWebhook писал в webhook_log, снесённую 24.05 вместе с legacy-каналом, и
молча выходил по Schema::hasTable — журнала не было ВООБЩЕ. Из-за этого 70 отказов 404
за 10 дней никто не видел; нашлись случайно в логе nginx. Пишем статус, адрес
отправителя, запрошенный хост и отпечаток присланного ключа (первые 8 символов md5 —
отвечает «наш ключ или чужой», секретом не является). Отказы дополнительно уровнем
warning: на бою LOG_LEVEL=warning, info в журнал не попадает.
3. Текст письма-тревоги переписан: «потеря» = только то, что не дошло даже за отсрочку;
«в пути» показывается отдельной строкой и потерей не считается.
4. В catch сверки — Log::error ПЕРВЫМ действием, до обращений к БД: при испорченной
транзакции следующий запрос бросал своё исключение и настоящая причина терялась.
Проверено: тесты поставщика и вебхука 230/230 (в т.ч. 5 новых на карантин, поздний
вебхук, отчёт «в пути» и смоук шаблона письма), Larastan 0, Pint чисто. Прогон всей базы
3566/3575; 5 падений — чужие и до этих правок (замерено откатом файлов): биллинг на стыке
месяцев и маршруты телеграм-ветки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Номера не кладём в задание и не пишем в журнал — отдельный запрос, без кеша.
Сторож принят вырезанием: без проверки статуса тест отдаёт 200 вместо 404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Выдача строго по одному: у робота один профиль браузера и одна сессия кабинета.
Задание, по которому робот не отчитался за срок аренды, возвращается в очередь.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Номера телефонов в задание не кладём — только текст, ссылка и смета.
После выката на бой перезапустить db/03_service_bypass_policies.sql.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Где работать, что уже стоит, красные линии, ловушки и приёмка вырезанием.
Отдельно записано, что проверка обращения к поставщику прокси умирает
вместе с сессией и её надо ставить заново.
Co-Authored-By: Claude Opus 5 (1M context) <noreply\@anthropic.com>
Нужна при ЛЮБОМ исходе с адресом: и на сервере, и на машине владельца робот стоит
не там, где очередь портала, а сегодня портал запускает его как процесс у себя.
Списано с работающего образца - робота креативов Яндекса.
12 задач по шагам с готовым кодом и тестами, переключатель process/poll оставляет
старый путь рабочим. Отдельно вынесены красные линии: перезапуск srv_bypass после
миграции, номера телефонов не в задании, приёмка сторожей вырезанием.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Было зашито channel msedge. На сервере Edge не ставится, робот там не запускался вообще.
Теперь выбор в config: на Windows msedge, иначе chrome, перебивается MTS_BROWSER_CHANNEL.
Три новых теста на выбор по умолчанию и на ручную перебивку.
Заодно в промт смены записаны живые замеры 30-31.07:
- рендер-сервер 51.250.1.97 настоящим Chrome получает от МТС отказ по адресу, 4 прогона;
та же проба с рабочей машины 77.74.123.226 даёт форму входа. Прежний вывод «тупик снят»
был сделан по curl, а curl получает от защиты МТС заглушку и врёт про доступ.
- прокси Proxy.Market сам отвечает 403 на стадии CONNECT для mts.ru, beeline.ru,
megafon.ru, tele2.ru, при этом yandex.ru, vk.com, avito.ru, rt.ru пропускает.
Значит режет поставщик прокси, а не сайт. Замер отправлен им, обращение 31074.
- связка портал-робот запускает робота как процесс на своей же машине, поэтому при любом
решении по адресу её придётся переделать на опрос портала роботом.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Живой запуск кампании на бою 30.07.2026 вскрыл две поломки, которых вчерашняя
починка не видела. Обе — из класса молчаливых: портал рапортует успех, а в жизни
ничего не происходит.
ПЕРВАЯ. Минимум Директа НЕ постоянный.
Вчера мы приняли живой отказ «Budget for this period cannot be less than 600 rub.»
за постоянный порог и зашили 600 ₽ в настройку. Сегодня та же кампания на неделю
получила отказ «cannot be less than 2400 rub.» — проверка её пропустила, и клиент
снова увидел английский текст.
Две точки дали правило: минимум = 300 ₽ за каждый календарный день периода,
считая оба края.
период 30.07-31.07, 2 дня → 600 ₽ = 2 × 300
период 30.07-06.08, 8 дней → 2400 ₽ = 8 × 300
Теперь порог считается от периода показа, а ставка за день вынесена в настройку
YANDEX_DIRECT_MIN_SPEND_RUB_PER_DAY. Период вычисляется ДО денег — иначе считать
минимум не от чего.
ВТОРАЯ. Создать объявления — не значит запустить рекламу.
Запуск проходил успешно, портал ставил статус «на модерации», а в кабинете лежали
кампания, группа и 15 объявлений в состоянии DRAFT и OFF. Яндекс кладёт всё
созданное черновиком и проверку сам не начинает. Реклама не показалась бы никогда,
и узнать об этом можно было только глазами в кабинете: наш журнал говорил, что всё
хорошо.
Добавлены два вызова, которых не было вовсе:
ads.moderate — отдать созданные объявления на проверку
campaigns.resume — включить показ
Порядок обязателен. Пока кампания черновик, включить её нельзя — Директ отвечает
«кампания является черновиком и не может быть остановлена». Сначала модерация,
она переводит кампанию в MODERATION, и только потом включение.
Отбор объявлений строго по Ids. На CampaignIds Директ отвечает «отсутствует
обязательный параметр Ids» — именно так отправка молча не сработала бы.
На модерацию уходят объявления, созданные ИМЕННО этим заходом: повторная отправка
уже проверяемого — отказ по позиции, он порвал бы возобновляемый запуск. Включение
показа безобидно при любом повторе: на включённой кампании Яндекс отвечает
предупреждением, а не отказом.
Заодно клиент Директа перестал молчать про отказы внутри ответа: раньше он смотрел
только AddResults, UpdateResults и DeleteResults, теперь ещё ModerateResults и
ResumeResults. Отказ по позиции в этих двух проходил бы насквозь незамеченным.
ПРОВЕРЕНО ЖИВЬЁМ. Кампания № 6 на бою: в кабинете 713175197, группа 5778555449,
15 объявлений, аудитория подключена, статус MODERATION, показ включён. На счету
Яндекса 8775 ₽. У клиента заморожено 3333.36 ₽ за 27778 показов до 06.08.
Тесты: 346 зелёных по рекламе, 43 по клиенту Директа, статанализ ноль замечаний.
Четыре новых теста — минимум по длине периода на боевом случае, отправка на
модерацию, порядок модерация-до-включения, отсутствие повторной отправки.
Два старых теста закрепляли частный случай в два дня — им проставлен явный срок
показа, иначе они молча описывали бы неправду.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Живой бой 30.07.2026. Сегмент дозрел за 15 часов — Яндекс опознал 1644 человека
из 1693, охват 3379. Портал нажал «Запустить» и получил отказ Директа:
Budget for this period cannot be less than 600 rub.
Стена оказалась не одна, а две. Обе — молчаливые: портал о них не знал.
Первая — аудитория. Сегмент в Аудиториях существует и при этом НЕГОДЕН, пока
Яндекс сводит загруженные номера с людьми. Портал этого не спрашивал вообще:
ни один файл не читал can_create_dependent. Он просто шёл в Директ строить
условие ретаргетинга и получал «объект не найден» — тот же отказ, что и на
несуществующий сегмент. Судить о готовности можно ТОЛЬКО по can_create_dependent:
статус is_processed означает «обрабатывается», а не «готов».
Вторая — деньги. Директ не берёт кампанию, у которой бюджет за период ниже
шестисот рублей. Приёмочная кампания на 1693 показа давала около 146 рублей.
Проверки минимума в портале не было тоже.
В обоих случаях клиент видел сырую ошибку Яндекса на английском и не понимал,
виноват ли он и что делать дальше.
Что сделано. Обе проверки выполняются ДО первого обращения к Яндексу: в кабинете
ничего не создаётся, деньги не морозятся, кампания остаётся черновиком.
аудитория ещё готовится → 202 и «подождите, обычно несколько часов»
смета мала → 422 и «нужно 6945 показов, это 833.40 рублей»
Наружу уходят ТОЛЬКО клиентские числа. Ни минимума площадки, ни нашей наценки:
по паре «минимум площадки — цена клиенту» долю Яндекса можно вычислить делением.
Минимум вынесен в настройку YANDEX_DIRECT_MIN_SPEND_RUB, Яндекс может его менять.
Тестом вперёд, в живой Яндекс из тестов не ходили. Четыре новых теста на запуск
и два на ответы портала. Реклама целиком: 342 теста зелёные.
Заодно приведён в порядок список исключений статанализа. Он был красным ЗАДОЛГО
до этой правки и не пускал ни одну правку кода: 629 замечаний до моих изменений,
638 после. Проверено в обеих папках работы — не артефакт подпапки.
Разбор 637 замечаний по составу:
611 — статанализ не понимает устройство тестов Pest и ругается на
обращения вида this->postJson и this->tenant. Таких записей в списке
исключений уже было 671 — просто новые тестовые файлы туда не дописали.
26 — придирки к типам, из них 7 в боевом коде.
Все семь в боевом коде разобраны поимённо и оказались ложной тревогой. Доказано
не рассуждением, а зелёными тестами, которые упали бы при настоящей поломке:
shows_until, три замечания — три теста прямо проверяют закрытие кампании по
истечении срока показа и разморозку остатка. Будь там строка вместо даты,
первый тест упал бы, а деньги клиента остались бы заперты навсегда.
snapshot_from и snapshot_to, два замечания — тесты ручного режима строят
аудиторию по этим датам, на строке они бы рухнули.
SetTenantContext строка 56 и CampaignMessageService строка 168 — лишние
подстраховки, вреда нет.
Корень ложных срабатываний: Larastan ищет приведения типов в старом виде —
свойством casts, а у нас метод casts. Код правильный, инструмент отстал.
После обновления списка статанализ зелёный: 0 замечаний. Теперь сторож снова
ловит НОВОЕ, а не молчит красным.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Вскрылось при приёмке починки 92 битых ссылок. Пуш прошёл, а сторож промолчал.
Стал разбираться — оказалось хуже, чем «в рабочей подпапке не запускается».
Замер, а не догадка. Одна и та же команда:
руками — 2242 ссылки
под lefthook — 183 ссылки
Сумма сходится ровно: 95 корневых .md + 3 в db + 85 на ОДИН уровень под docs.
То есть образец docs/**/*.md под хуком схлопывался до docs/*/*.md. Не проверялись
ни docs/*.md, ни всё глубже второго уровня — docs/superpowers/specs, plans, audits,
findings, runbooks и docs/observer/notes. Именно там лежали 88 из 92 битых ссылок.
Сторож годами показывал зелёное, потому что не смотрел туда, где было грязно.
Лечение: отдать lychee каталоги и фильтр по расширению вместо shell-образца —
он обходит дерево сам, без участия оболочки. Так же выправлена ручная команда
npm run links, чтобы она и хук проверяли одно и то же.
Проверено вырезанием защиты, а не рассуждением. В docs подложена заведомо битая
ссылка:
старый вариант — 183 ссылки, 0 ошибок, пуск разрешён. Не увидел.
новый вариант — 2243 ссылки, 1 ошибка, возврат 1. Остановил бы пуш.
После уборки подложки — 2242 ссылки, 0 ошибок, возврат 0.
Вторая находка того же захода: в рабочих подпапках-worktree хук вообще не
запускался — писал «Can't find lefthook in PATH» и пропускал пуш с успешным
кодом. Причина: сгенерированный хук ищет двоичный файл только в node_modules
той папки, откуда его позвали, а в подпапках их нет. Лечится вне хранилища —
lefthook поставлен глобально и положен в PATH, теперь находится из любой папки.
Проверено: из подпапки хук стартует и отрабатывает обе работы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>