Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 40 знаков),
когда в объявлении ссылка на САЙТ, а не на телеграм-канал. Робот про это поле не знал,
«Продолжить» молча не срабатывало, кампания вставала на шаге «Объявление» — в бою уже
ПОСЛЕ списания денег. Проверено живьём: 2234454 (сайт — встала) против 2234462 (канал —
дошла до подтверждения) и 2234490 (сайт с заголовком — дошла).
Портал спрашивает заголовок заранее, на создании черновика: обязателен только для
не-телеграмной ссылки (App\Support\TelegramLink), колонка ad_headline varchar(40),
поле на экране появляется по той же развилке. Робот заполняет его в кабинете.
Три ловушки, добытые живыми прогонами (описаны в коде):
- поле дорисовывается в ОТВЕТ на ссылку, с задержкой — надо ждать, а не спрашивать;
- под описание подходит несколько элементов — нужен .first();
- серая надпись внутри поля НЕ placeholder, а нарисованная подпись: поиск по атрибуту
давал ноль совпадений при видимом на снимке поле. Опознаём по видимой надписи.
Тесты: робот 130/130, ClientTg 250/250, экран 23/23. Полный прогон бэкенда — те же
13 падающих классов до и после правки (ни одного в телеграм-части). В baseline
статанализа добавлен известный ложный класс Pest для нового файла тестов.
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>
Защита от русских букв в токене переехала из bin/poll.js в portalClient — теперь
она стоит на ЛЮБОМ входе, а не только у прохода опроса. Прикрыта двумя тестами
и принята вырезанием: без проверки тесты падают.
Заодно в старых тестах русские токены заменены на латинские: русский токен в
заголовке HTTP не работает в принципе, и тесты закрепляли невозможное.
Тесты робота: было 118, стало 120.
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>
Было зашито 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>
Ни одну нельзя было увидеть без настоящего прогона: часть закрыта песочницей Яндекса,
часть — швами между кусками, которые по отдельности покрыты тестами.
1. Слепок креативов уходил с SelectionCriteria пустым МАССИВОМ. Живой Яндекс отвечает
отказом 8000 «SelectionCriteria cannot contain an array», портал не может снять слепок,
задание роботу не выдаётся, клиент видит вечное «готовим картинки». В JSON нужен пустой
ОБЪЕКТ. Песочница до этой проверки не доходила — отвергала запрос раньше, на входе.
2. Отказы Яндекса ПО ПОЗИЦИИ внутри ответа глотались молча. Запрос успешен, а нужного Id
в позиции нет — вместо него Errors с человеческим объяснением. Портал падал ошибкой PHP
«Undefined array key Id», ни клиенту, ни в журнал не попадало ни слова из ответа Яндекса.
Теперь слова Яндекса выходят наружу — именно это и позволило найти пункт 3.
3. В условии ретаргетинга не передавался MembershipLifeSpan — срок хранения человека
в сегменте. Живой Яндекс отказывает «Required field: Not specified time for goal or
segment», и следом «Object not found»: довод негоден целиком, запуск встаёт. Берём
audience_days кампании, границы Яндекса 1..540.
4. Робот не отдавал набор Яндексу: заливал файлы, нажимал «Создать» и уходил. Оказалось,
«набор» в кабинете и «креатив» в creatives.get — разные вещи: пока набор не отмечен
галочкой и не нажато «Добавить выбранные», Яндекс креативы не регистрирует. Замер:
14 залитых картинок были невидимы для creatives.get, после добавления в ЧЕРНОВИК формы
счётчик прыгнул 18 -> 32 в ту же секунду. Объявление при этом не сохраняется,
«Сохранить изменения» робот по-прежнему не трогает никогда.
5. Гонка при заливке. Три живых прогона подряд из 15 картинок доносили 14, и каждый раз
пропадала ДРУГАЯ: 480x320, потом 300x600, потом 336x280. Замер объяснил: кнопка
«Создать» разблокируется на 4-й секунде, когда принято 11 файлов из 15, остальные
дозагружаются к 7-й. Робот жал сразу по разблокировке. Ждём теперь по строкам принятых
файлов и по исчезновению слова «Загружается».
Первая попытка этой починки НЕ РАБОТАЛА и выглядела рабочей: сторож считал размеры
в окне, не заметив постоянного фильтра из тех же 15 размеров вверху. Поймано по тому,
что прогон занял ровно столько же секунд, сколько до починки. Отсюда тире в признаке
приёма — фильтр его не содержит.
Каждая починка закрыта тестом, который сначала падал на своей поломке. Прежние тесты
проверяли разбор ОТВЕТОВ и путь ДО «Создать» — форму запросов и то, что после, не смотрел
никто. Робот 84 из 84, реклама портала 42 из 42.
Проверено живьём на боевом: задание роботу отработало со статусом done, опознано
15 креативов из 15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
process.exit стоял внутри try, а снятие замка — в finally. Выход обрывает процесс
немедленно, и до finally дело не доходило никогда. Замок оставался лежать после каждого
удачного прохода, и следующие полчаса и run.js, и keepalive.js молча уходили со словами
«робот уже работает»: задание клиента ждало в очереди, а в журнале при этом всё
выглядело благополучно. Молчаливый сбой того же класса — «успех» без работы.
Ни один из 80 тестов этого не видел: проверялись модуль замка и проход по отдельности,
а дыра была ровно в шве между ними.
Новый тест bin-lock.test.js запускает bin/run.js настоящим процессом ДВАЖДЫ подряд
против портала-обманки: одиночный запуск эту дыру не показывает. Тест сначала упал
на обоих утверждениях, после починки зелёный. Было 80 тестов, стало 82.
Проверено живьём на рендер-виртуалке: после прохода за заданием и после захода
в кабинет замок снят.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.
Сверх самого слияния:
- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.
Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разведку впервые прогнали живьём, с разрешения владельца: вызвали саму функцию робота
против боевого кабинета на двух отклонённых объявлениях кампании-пустышки. Живую кампанию
не трогали, робот только читал. Прогон окупился сразу — вскрылись две поломки, которых
не видел ни один из 74 зелёных тестов.
Первая. Робот не нашёл объявление, которое в кабинете было: провал за три секунды,
«объявления в списке нет». Замерили — список рисуется две секунды, а робот считал ячейки
сразу после человекоподобной паузы в 800 миллисекунд. Счёт не ждёт, он отвечает про
«прямо сейчас». На бою это значило бы, что КАЖДЫЙ отказ уезжает в «ждёт разбора»,
а клиент причину не узнаёт никогда.
Вторая. После первой починки робот стал находить объявление и приносить 29 знаков —
один заголовок «Модератор отклонил объявление», без причины. Та же ошибка: строка причины
появляется позже окна, робот считал её и получал ноль, раскрывать было нечего. Клиенту
уехало бы сообщение от Яндекса, в котором нет ни слова о том, что чинить.
Текст окна нарастает по частям: 29 знаков, потом 82, потом 785. Окно не отдаёт ошибку —
честно показывает то, что успело нарисоваться. Поэтому промах молчаливый: робот считал бы,
что справился.
Починка одна на обе: ждать, а не считать. Раскрытие строки подтверждаем появлением
подробности, а не паузой — пауза это надежда, элемент это факт. Не дождались подробности,
остаёмся с короткой причиной: она честная и клиенту полезна, промолчать было бы хуже.
Оба сторожа написаны ДО починки и падали с теми самыми живыми ошибками.
Хвост доклада: по решению владельца срезаются подписи кнопок кабинета «Написать в чат»
и «Написать письмо» — у клиента этих кнопок нет, а выглядят они приглашением написать
Яндексу. Режем только хвост и только точное совпадение строки: те же слова внутри
пояснения это слова Яндекса, их не трогаем.
Живая проверка обрезки вскрыла третью ловушку: «Написать письмо» срезалось, а «Написать
в чат» оставалось. Яндекс ставит между короткими словами неразрывный пробел. Глазу он
неотличим от обычного, а сравнению это совсем другой символ. Правило для этого кабинета:
сравнивать текст только по человеческому виду строки, а элементы ждать, а не считать.
Замеры записаны в разметку кабинета, раздел 7.8.
Итог живой проверки с продовой паузой: причины приезжают целиком, кнопок в них нет —
760 знаков по финансовым услугам и 589 по медицине, шесть-семь секунд на объявление.
Снимок берётся только с окна, логин и остаток счёта в кадр не попадают.
Робот 80 из 80. Портал не тронут. На боевой не выкатывалось, рубильник Директа выключен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 16 закрыта решением владельца. Дороги «робот везёт документ в кабинет Яндекса»
не существует — доказано двумя нарочными отказами, обычной тематикой и лицензируемой:
в окне отказа ноль полей для файла, документы Яндекс принимает только снаружи кабинета.
Приём документа оставлен, но портал больше не молчит: клиент приложил файл — в ленту сразу
ложится отметка «документ у нас, передать его Яндексу автоматически нельзя, при
необходимости отнесём сами и напишем здесь», а владельцу уходит письмо на адрес алертов.
Без письма обещание было бы пустым: файл просто лёг бы на диск. Сам файл письмом
не отправляем — это чужие бумаги. Обычный ответ без файла ни отметки, ни письма не даёт.
Экран «ждёт разбора» в админке: ручка была, экрана не было. Третья карточка на странице
«Реклама» — клиент, кампания, что робот делал человеческими словами, номер объявления
и на чём споткнулся. В подписи прямо сказано, чего там НЕ будет: обычных отказов,
их клиент разбирает сам.
Дальше — разбор собственной работы этого дня. Найдено четыре ошибки, все исправлены.
1. ТЯЖЁЛАЯ. Доклад разведки на бою уронил бы очередь заданий целиком. Робот пишет в ленту
под служебной ролью, а у неё на этой таблице было только чтение. Отказ по правам,
500 роботу, три повтора — и задание навсегда «в работе». Пока хоть одно задание
в работе, выдача отвечает «работы нет» ВСЕМ клиентам. Лечение — запись схемы v9.17:
право на запись плюс нумератор. В плане про это было написано прямым текстом,
я прошёл мимо. Тесты поймать не могли: ходят суперпользователем.
2. Признак «набор создан» я выдумал: взял метку, которая в нашей же разметке описана
как СКРЫТАЯ галочка. Проверка «видно ли её» не сработала бы никогда. Признак с экрана
убран совсем: успех определяет портал слепком креативов, а «окно не закрылось» —
это норма, так и есть живьём.
3. Сломал ленту для повторного отказа. Поменял защиту от дублей на «такой текст уже
когда-либо был» — и клиент, починивший рекламу и получивший тот же отказ, не увидел бы
ничего. Вернул сравнение с последним сообщением, а заглушку «причину выясняем» держит
теперь сам джоб: показываем один раз, пока сказать нечего.
4. Мой собственный тест оказался пустышкой: оставался зелёным при вырезанной защите.
В нём отклонялись ВСЕ объявления, а тогда кампания уходит в «отклонена» и обход её
больше не берёт. Сценарий существует только при частичном отказе — тест переписан
на два объявления и теперь вырезание защиты его роняет.
Портал 391/391, админские экраны 9/9, фронт на затронутых наборах 52/52, робот 74/74,
мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен, живьём разведка не гонялась.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое
объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране
кабинета, и добыть её может лишь тот, у кого есть глаза.
Как теперь работает:
опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению;
робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу,
раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой
вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту
письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо,
в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора».
Четыре ловушки, пойманные по дороге и проверенные вырезанием:
1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит
по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы
в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16.
2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок
креативов не нужен, значит при выключенном рубильнике она получила бы задание,
и робот пошёл бы в живой кабинет.
3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота
обход снова клал бы «причину выясняем» поверх настоящей причины.
4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ
и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это
в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя
транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен
на сам механизм: обычным тестом это не ловится, они ходят суперпользователем.
Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс
внутри открытой транзакции.
Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Разметку кабинета 27.07 снимали глазами, ничего не загружая, и поле файлов выбрали
по виду — CreativeActionsMenu.FileInput. Живая загрузка 28.07 показала: это поле файлы
молча забирает, а кнопка «Создать» остаётся серой. Робот падал бы «кабинет не принял
файлы» на каждой попытке. Работает только поле внутри окна загрузки.
Вторая правка из той же поправки разметки: окно после «Создать» живьём НЕ закрывается,
а переключается на вкладку «Мои креативы» со списком наборов. Робот считал это бедой
и слал владельцу письмо-алярм на КАЖДОЙ удачной загрузке. Теперь признак успеха —
появившийся список наборов; человека зовём, только когда исход непонятен: ни списка,
ни закрытия.
Заодно снято лишнее ожидание: раньше на удачной загрузке робот стоял две минуты,
дожидаясь закрытия, которого не бывает.
Обе правки проверены вырезанием по отдельности. Робот 63/63.
Вердикт по второй пробе модерации записан в cabinet-flow.md §7.6: у лицензируемой
тематики окно отказа ровно такое же, поля для документа нет и там. Дороги «отвезти
документ роботом в кабинет» не существует.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Один запрос на чтение боевым ключом, с разрешения владельца: ads.get на отклонённое
пробное объявление отдаёт StatusClarification = «Отклонено на модерации.» и больше
ничего. На экране в этот же момент — «Нет предупреждения: финансовые услуги» и абзац
с указанием, что дописать в баннер. В подполях Creative и TurboPageModeration причины
тоже нет.
Следствие первое: задача 15 из удобства стала обязательной — без робота-разведчика
портал знает только факт отказа. Проверил до того, как писать код: вывод мог оказаться
и обратным.
Следствие второе, важнее: в куске 1, который считался готовым, живой клиент увидит
пустоту. В переписке — единственная фраза «Отклонено на модерации.», а в списке
кампаний под ярлыком «Отклонено» не будет ничего вовсе: подпись берётся как первая
строка причины, а текст Яндекса начинается с переноса строки.
Тесты этого не ловили — они подставляют выдуманную причину, и на ней всё работает.
Дефект живёт в зазоре между выдуманными данными и живыми. Записан, чинится следующим
коммитом.
Хвост №1 из STATE ревью-правок: оформили доказанный в A2 путь пересдачи в
постоянный режим робота mode:'resubmit'. Робот берёт ОТКЛОНЁННУЮ кампанию, входит
в её редактор через «Исправить», вносит исправления и повторно отправляет на
модерацию БЕЗ ОПЛАТЫ (0 ₽ — деньги не списываются).
Что нового:
- parseResubmitTask (task.js): своё задание пересдачи — campaignId + submitMode
(draft|live, без дефолта, чтобы live не случился сам) + правки на выбор
(moderatorFile и/или adText/buttonUrl/ordCategory). Нужна хоть одна правка —
иначе тот же контент снова отклонят. Номера/бюджет не нужны (уже у кампании).
- openResubmitEditor (cabinet.js): нативный клик «Исправить» в строке кампании по
id → ждём редактор /telegram-a2p/{id}/message (в чужую кампанию не лезем).
- submitWithoutPayment (cabinet.js): на /payment жмём ТОЛЬКО «Отправить на
модерацию без оплаты»; денежные кнопки («Списать…»/«Оплатить») — двойная защита
через isForbiddenButtonText, не жмём никогда.
- editResubmitFields + вынос fillAd в хелперы (fillAdText/fillAdLink/
selectOrdCategory/fillAdMedia): пересдача правит только заданные поля тем же
проверенным кодом (DRY, поведение fillAd не изменилось).
- runResubmit (runner.js) + ветка mode:'resubmit' в bin/run.js (до parseTask, как
read-status). draft — предохранитель (до /confirmation, не шлём); live — отправка
без оплаты.
Живая проверка на реальных отклонённых кампаниях (28.07.2026):
- DRAFT (2231132, правка текста + документ): вошёл «Исправить» → правка → загрузка
документа → /confirmation → НЕ отправил (resubmitted:false, stoppedAt:confirmation).
- LIVE (2231134, правка текст+ссылка+ОРД, без документа): полный цикл → «без оплаты»
(0 ₽) → resubmitted:true; read-status подтвердил moderating. Баланс не тронут.
Робот npm test 110/110 (было 94, +16). Приёмочный лист и живые результаты:
docs/superpowers/2026-07-28-robot-resubmit-mode-ACCEPT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В ту же пустышку № 713110757 добавлено объявление № 17787102785 со стоматологией:
медуслуги лицензируются, значит модерация должна потребовать лицензию, то есть
документ. Первая проба дала отказ «поправьте креатив» без документов, поэтому
проверяем, отличается ли окно отказа у лицензируемых тематик. Вердикт ждём.
Попутно подтвердилось на втором случае: сохранение объявления само отправляет его
на модерацию — кнопку «Запустить кампанию» не нажимали, статус сразу стал
«Объявление на модерации». Это подтверждает вывод §7.1 о том, что кнопки повторной
модерации в кабинете нет.
Грабли: переименование набора креативов карандашиком оставляет поле в режиме правки
и перехватывает клик по «Создать» — загрузка встаёт без внятной ошибки. Имя набору
не задаём, берём первый в списке.
Живая кампания владельца № 713051718 не тронута, расход пустышки ноль.
Вердикт по нарочно непроходному объявлению пришёл на вторые сутки:
«Модератор отклонил объявление · Нет предупреждения: финансовые услуги».
Разметка записана в bots/yandex-creatives/docs/cabinet-flow.md §7 вместе со снимком
окна отказа. Причина живёт только в списке объявлений — форма объявления знает
«Показы не идут» и молчит про модерацию. Окно открывается кликом, не наведением,
а подробное пояснение видно лишь после раскрытия строки причины.
Два отрицательных ответа важнее найденного:
кнопки повторной модерации не существует — Яндекс шлёт на перепроверку сам
по факту сохранения правки; прикладывать документ в кабинете некуда — в окне
отказа ноль полей для файла, документы уходят наружу через чат или форму
обратной связи. Задача 16 остановлена до проверки вторым отказом
на лицензируемой тематике — решение владельца.
Заодно поправлены даты: работа прошлого захода помечалась 29.07, фактически
всё делалось 28.07. Имена миграций не трогали — они уже закоммичены.
Живая проверка A1 (28.07.2026) на реальном отказе кампании 2231134 вскрыла два бага в
readModerationStatus, из-за которых боевой код не находил строку кампании — на юнит-тестах
было зелено, а кабинет показал обратное:
1. Паттерн ссылки: в списке кампаний ссылка = /cabinet/campaigns/telegram/{id}, а код искал
/telegram-a2p/{id} — это адрес детальной/визард-страницы. Matcher расширен на оба варианта
через campaignHrefRe/hrefMatchesCampaignId плюс юнит-тест.
2. Глубина подъёма по DOM: строка списка — грид из div, не tr/li; контейнер со статусом ряда
на ~8 уровней выше ссылки. Предел подъёма поднят с 6 до 10; возврат на первом предке со
статусом — выше склеиваются две кампании.
Итог живого прогона: робот вернул rejected плюс полный текст 5 пунктов модерации из слайд-модалки
Причины. Метка FLOW-CONFIRM в cabinet.js снята. Робот npm test 94/94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное.
Поведение в песочнице не меняется; правки готовят ветку к боевому включению.
F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по
4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа
23505 в боевом режиме.
F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200;
длинное значение даёт ошибку поля, а не замаскированный 422 от БД.
F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и
считает дневной лимит под lockForUpdate строки правила.
F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс
предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем.
F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id
значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони.
F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке
domClickButton, finalize целит только кнопку отправки на модерацию.
F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment;
не дошли до /payment → падаем громко.
F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком.
F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе
кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в
000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql.
Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Поправлены три неверные записи прежней разметки, снятой глазами без загрузки файлов:
поле файлов внутри окна вместо CreativeActionsMenu.FileInput, прикрепление креатива
галочкой BatchesList вместо кнопки Выбрать, и список объявлений, который при будущем
сроке кампании отдаёт пусто. Добавлен весь путь мастера создания медийной кампании.
В боевом кабинете заведена отдельная пустышка со сроком в октябре и нулевым расходом:
кампания 713110757, объявление 17787055204 с креативом регулируемой тематики. Живая
кампания 713051718 не тронута. Ждём вердикт модерации, чтобы снять экран отказа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
«На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
{ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.
## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
(.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
+ freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).
## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.
TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.
Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.1b. Робот создаёт реальный
черновик в кабинете уже на шаге «Аудитория»; упавший прогон плодит осиротевшие
черновики (27.07 чистили руками, 2 шт.). Узаконенный инструмент вместо ручного.
- src/cleanup-drafts.js — чистые предикаты (покрыты юнит-тестом) + браузерная
обёртка cleanupDrafts(). Предикаты: isDeletableDraftRow (только строка-черновик
«Telegram по своей базе», НЕ «на модерации»/«отклонена»/«активна», НЕ шапка),
isHeaderOrAggregateRow, selectionIsSafe (гейт перед удалением: ≥1 выбран, ВСЕ
черновики, select-all снят — иначе аварийный стоп).
- bin/cleanup-drafts.js — CLI: read-only по умолчанию (показывает, что БЫ удалил);
удаление только по явному флагу --delete, через сверку selectionIsSafe.
- Логика перенесена с проверенного живьём 27.07 ручного скрипта; критический гейт
проходит через Node-предикат selectionIsSafe (а не только браузерный скан).
- Процедура задокументирована в FLOW-FINDINGS.md.
Приёмка: юнит cleanup-drafts 17/17, весь Node-набор 65/65 (npm test). Без БД,
без денег, без выхода в кабинет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.1. Плюс закрыта задача 3.0
(живая разведка экрана отказа) — вердикт МТС по кампании «займ» (2231134)
пришёл: «Отклонена». Разведка read-only, деньги не тронуты.
Задача 3.1 — колонка mts_campaign_id + РАННЕЕ и надёжное сохранение:
- Миграция client_tg_campaigns.mts_campaign_id (varchar(32) NULL, после
status_reason) + запись CHANGELOG_schema v8.89 (предварит., ветка). RLS не
меняется; rls-reviewer не требуется (nullable-колонка данных).
- Робот (Node): чистый хелпер parseCampaignId(url) в cabinet.js (покрыт
тестом); runner.js захватывает id СРАЗУ после создания черновика (шаг
аудитории) и печатает маркер MTS_CAMPAIGN_ID=<id> в stderr; id теперь
идёт и в ветке ОТКАЗА (раньше терялся).
- Обёртка (PHP): TelegramRobotRunner восстанавливает id из stderr-маркера во
всех путях (таймаут/непарсабельный вывод/JSON без id); RobotResult::failed
принимает id.
- Джоб: finalize сохраняет mts_campaign_id при ЛЮБОМ исходе (успех/отказ), не
затирая ранее сохранённый id. Метод failed() не трогали — туда результат не
доходит (осознанный residual, закроют уборщик 3.1b и sweeper 3.3).
Разведка отказа (3.0) записана в bots/mts-telegram-ads/FLOW-FINDINGS.md:
причина показана текстом в слайд-модалке «Причины отклонения кампании»
(кнопка «Причины»); поля загрузки файла на экране отказа нет — документ
грузится через «Исправить» → шаг «Сообщение» → «Комментарий для модератора»;
кнопка пересдачи — «Исправить».
TDD, робот замокан, тесты на liderra_testing (номера 7999…).
Приёмка: Node 48/48 (npm test), Pest ExternalIdTest 2/2 + регрессия ClientTg
122/122, phpstan (4 боевых файла) 0, deptrac 0 нарушений, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в
находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ
(резерв → списание по факту показов → возврат остатка, экран /payment).
Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно
сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения).
Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2.
- 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка,
чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать
её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле
позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял.
- 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость
кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая
неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа →
null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена
(селектор строки стоимости подтвердим живьём в Этапе 3).
- Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план
Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт).
TDD, робот замокан, тесты на liderra_testing.
Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest
(потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0,
pint чисто, deptrac 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Мелочи приёмочного листа v12 §8. Каждая правка с тестом; где защита уже стояла
в коде — тест проверен вырезанием этой защиты.
Предел веса картинки. Две прежние проверки были пустышками: сравнивали константу
саму с собой и с тем же числом в ответе сервера. Вырезание правила max: оставляло
обе зелёными. Настоящий тест грузит перевес и ждёт отказа — это четвёртая найденная
пустышка за ветку.
Обход модерации. Объявление без статуса и причина отказа длиннее колонки роняли
запись в базу ВНЕ защиты, и обход обрывался на середине: остальные клиенты не узнавали,
приняли их рекламу или отклонили, а деньги за отклонённый набор не возвращались.
Запись ответа теперь под той же защитой, что и сеть; пустой статус не пишем вовсе,
причину храним обрезанной.
Робот. Адрес файла приходил в ответе сервера, а шли по нему со своим токеном без
всякой сверки. Теперь адрес обязан вести на портал. Папка снимков экрана росла
бесконечно, а на снимках видны логин и остаток счёта — старше двух недель убираются.
Ещё: порядок посредников служебного канала — токен раньше служебного соединения;
BannerGenerator больше не отдаёт молча файл тяжелее предела и берёт предел из общей
константы; нулевой номер креатива ловится намеренно, а не случайно нестрогим сравнением.
Портал 300/300, робот 60/60. Мест снятия заморозки денег по-прежнему четыре.
Не тронуто намеренно: цена за 1000 показов и бюджет приходят от клиента — но это
видимое поле мастера и принятое продуктовое решение, а не недосмотр. Решает владелец.
Закрыты 5 находок аудита (безопасность и валидация входа, деньги не задеты):
- #11 ПДн-скрины робота: полноэкранный скриншот кабинета МТС (мог содержать
телефоны базы, 152-ФЗ) больше не снимается и не уходит письмом по умолчанию —
только по явному TG_DEBUG_SHOTS. Чистый хелпер src/shots.js.
- #12 стоп-лист opt-out теперь нормализуется при сравнении (8XXXX / 10-значные
формы вычищаются), сравнение по голому 7XXXXXXXXXX.
- #13 верхние пределы входа: phones max:200000, phones.* max:32, audience_days
max:365; store возвращает dropped_count (сколько номеров не распозналось).
- #14 идемпотентность запуска: Фаза A джоба читает кампанию с lockForUpdate —
два воркера сериализуются на блокировке строки (защита от гонки).
- #2 предстартовый гейт аудитории: в боевом режиме launch НЕ бронирует деньги
под кампанию с <367 / пустой аудиторией (иначе бронь залипала бы). Порог —
config client_tg.auto_batch_threshold (367). В песочнице гейта нет.
TDD, робот в тестах замокан, тесты на liderra_testing (7999… номера).
Приёмка: Node 28/28, Pest ClientTg 105/105, phpstan 0, pint чисто, deptrac 0.
Обновлён CampaignApiTest (тест «денег не хватает → 409» засеян ≥367 контактами,
чтобы дойти до проверки денег после нового гейта).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Хвосты денег Д1-Д6 и робота Р-х1-Р-х6 из приёмочного листа v12.
Д1 цена, по которой заморожены деньги, записывается на кампанию. Пока поле было
пустым, списание читало глобальную цену — админ менял её, и клиент платил больше
обещанного при запуске.
Д2 суточное списание берёт кампанию под замком строки. Ключ идемпотентности зависит
от числа показов, поэтому два одновременных прогона получали разные ключи и списали
бы клиента дважды.
Д3 пауза, не дошедшая до Директа, больше не считается паузой: отказ 409, заморозка
остаётся. Раньше реклама крутилась дальше, портал показывал паузу, а деньги были уже
свободны. У возобновления поведение намеренно прежнее — иначе понадобилось бы пятое
место разморозки, а их ровно четыре. Там же убрана мина строгого сравнения рубильника.
Д4 не трогали — это вопрос владельца.
Д5 рубильник Директа держит и служебный канал робота: выдача задания и приём отчёта
ходили в живой кабинет мимо него.
Д6 проверка рубильника приведена к общему виду: YANDEX_DIRECT_ENABLED=0 давало строку,
которую строгое сравнение читало как включено.
Р-х1 настройки читаются из .env робота, а не каталога запуска.
Р-х2 файл-замок robot.lock: проход и поддержание входа больше не дерутся за профиль
браузера. Занят — уходим молча, задание остаётся в очереди. Брошенный замок
перехватывается через полчаса.
Р-х3 письмо-алярм честно говорит, залиты ли уже креативы в кабинет. Побочно вскрылось,
что тексты писем не проверялись ни одним тестом — транспорт вынесен в src/smtp.js.
Р-х4 тест-пустышка про рабочую папку заменён настоящим: запуск из чужого каталога без
явной папки. Проверено вырезанием.
Р-х6 пустое значение в окружении читается как значение по умолчанию, мусор даёт внятную
ошибку вместо тихого NaN.
Портал 293/293, робот 57/57. Денежных выходов снятия заморозки по-прежнему четыре.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Хвосты портала П1-П8 из приёмочного листа v12.
П1 обрыв постановки задания больше не даёт клиенту голый 500 — 503 с человеческим
текстом и записью в журнал. Таймаут у HTTP-клиента Laravel уже был, эта половина
находки не подтвердилась.
П2 и П6 роботу отдаются только баннеры без номера креатива — тот же список
сопоставляется при отчёте. Раньше стороны расходились и опознание падало на
безупречной работе робота, плодя дубли в кабинете. Плюс постраничный обход описи
креативов: слепок обрывался на первой странице.
П3 слепок «до» снимается при выдаче задания, а не при постановке, и в той же
транзакции, что и перевод в работу. Два задания в очереди получали одинаковый
слепок, второе падало всегда. Постановка перестала зависеть от живости Яндекса.
П4 перед созданием объявлений сверяется настоящий размер каждого креатива одним
запросом. Не сошлось или креатива нет — запуск не идёт.
П5 уникальный индекс uq_ad_campaign_banner_slot, запись v9.09 в CHANGELOG схемы,
rls-reviewer GO.
П7 замок на правку расширен на сегмент Аудиторий — он создаётся раньше кампании.
Смежная находка: перезаливка картинки теперь обнуляет номер креатива.
П8 и Р-х5 файл отдаётся под настоящим расширением и типом содержимого, имя от
номера баннера; робот берёт расширение из ответа портала.
Портал 287/287, робот 43/43. Денежных выходов снятия заморозки по-прежнему четыре.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Р6 — задание, брошенное «в работе», держало очередь для всех клиентов навсегда.
Причина сбоя обрезается до 900 знаков, иначе портал отвергал отчёт о самом частом
виде поломки. Провал отчёта больше не глотается молча — едет в письмо человеку.
Приём отчёта ловит любую ошибку и закрывает задание сбойным вместо 500 роботу.
Новый сторож creative-jobs:reap каждые 10 минут разбирает пробку.
Р7 — обрыв на докладе «готово» объявлял провалом уже сделанную работу. Доклад
вынесен из-под обработчика сбоя, повторяется трижды, при неудаче только письмо.
Осечка ухода со страницы после «Создать» больше не считается провалом загрузки.
Р8 — пустой список размеров считался успехом, и робот заливал одни и те же файлы
по кругу, оставляя мусор в живом кабинете. Теперь громкий отказ до похода в Яндекс.
Портал 274/274, робот 41/41. Все защиты проверены вырезанием.
Выходов снятия заморозки денег по-прежнему четыре.
Клиентский модуль «Реклама в Телеграме по своей базе» (робот-в-браузере, МТС Маркетолог).
Задача 5.2 — обратная связь по отказу:
- миграция client_tg_campaigns.status_reason (varchar 500, nullable; RLS без изменений — колонка на существующей таблице), запись v8.88 в CHANGELOG_schema
- Campaign: status_reason в fillable
- NotificationService::notifyTelegramCampaignRejected — in-app уведомление всем активным
пользователям тенанта без pref-гейта (важное операционное сообщение доходит всегда)
- RunTelegramCampaignJob: при провале робота сохраняет причину в status_reason и шлёт уведомление
(Tenant грузится внутри tenantTx для RLS, notify — после транзакции)
- фронт: telegram.ts (+status_reason), AdvertisingTelegramView показывает причину отказа/ошибки
- тесты: RejectNotifyTest (4 Pest) + 2 Vitest на экран
Закалка робота против частых глюков кабинета МТС:
- browser.js: gotoStable() — терпеливая загрузка с перезагрузкой (кабинет виснет на пустой крутилке)
- session.js: isLoggedIn через gotoStable — больше нет ложного «вход слетел»
- cabinet.js: знакомство пропускается если его нет (только для новичков); оферта по #isOfferAccepted;
осознан шаг /payment (денежная развилка «оплатить/без оплаты» — Сессия 6)
Живая разведка кабинета зафиксирована в FLOW-FINDINGS.md (селекторы, шаг /payment, поле файла модератору).
Проверки: Pest ClientTg 93/93, Vitest экрана 7/7, node-тесты робота 20/20.
Робот в тестах замокан, тесты на liderra_testing. Реальных ПДн нет (номера фейковые 7999…).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Р3 хвост. Адрес файла баннера стал /api/creative-robot/jobs/{jobId}/banners/{bannerId}/file.
Раньше портал подставлял «какое-нибудь задание в работе» — защита выдачи чужих картинок
держалась на внешнем условии, а не на самом запросе. Робота править не пришлось: он берёт
адрес из ответа портала как есть. Запись v9.08 в журнал схемы про частичный уникальный
индекс; rls-reviewer по миграции — GO.
Р4. Замок на наборе баннеров после заведения кампании в Яндексе: перезаливка, включение
и удаление отдают 409 по тому же признаку yandex_campaign_id, что и замок на параметрах.
Без него в портале была новая картинка, а в Яндексе крутилась старая, а удаление строки
с номером объявления заставляло возобновление завести второе объявление того же размера —
старое продолжало крутиться за деньги клиента. Баннер с номером объявления не удаляется
никогда.
Р5. Захват кампании под запуск: новый промежуточный статус launching, перевод под замком
строки в транзакции. Два одновременных нажатия «запустить» проходили проверку статуса оба
и заводили две CPM-кампании при одной заморозке денег. На любой ошибке прежний статус
возвращается, номера созданных в Яндексе сущностей уцелевают — возобновляемый запуск
не тронут. Брошенный захват старше пятнадцати минут перехватывается.
Тесты: портал 266/266, робот 35/35. Все новые защиты проверены вырезанием.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Любой сбой даёт отчёт порталу и письмо со снимком экрана, кампания остаётся
черновиком. Отчёт порталу идёт ПЕРВЫМ: упавшая почта не должна подвешивать
задание в работе. Картинки клиента стираются при любом исходе.
Поправлены два расхождения плана с живым кодом: в кабинет уходят пути к файлам,
а не описания баннеров, и папки считаются от корня робота, а не от каталога
запуска - робота запускает расписание.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Заглушка плана заменена настоящим кодом: разметка снята в задаче 11.
Прихожая — существующие кампания и группа из настроек: своей кампании у робота
в этот момент ещё нет, портал ставит задание до её создания в Яндексе.
Окно, не закрывшееся после Создать, провалом НЕ считается — оно может остаться
открытым и после удачной загрузки. Робот закрывает его крестиком и отдаёт слова
кабинета наружу, а приговор выносит портал по слепку креативов.
Кнопку Сохранить изменения робот не нажимает никогда — на это есть тест-сторож.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Снято живьём вместе с владельцем в кабинете sasha261185, кампания 713051718.
Ничего не сохранялось и не создавалось: до и после — те же 15 объявлений,
кнопки «Сохранить изменения» и «Создать» не нажимались.
Ключевое для задачи 14: системное окно выбора файла роботу не нужно —
в форме есть input type=file с data-testid CreativeActionsMenu.FileInput,
принимает png/jpeg/gif и сразу несколько файлов. Весь путь до окна загрузки
проверен программным прогоном, окно открылось.
Подтверждено живьём: конструктор креативов закрыт, Яндекс пишет это сам;
у всех 15 креативов имя «Новый креатив», потому что поле имени одно на всю
загрузку — опознание по разнице слепков и размеру остаётся единственным
надёжным способом.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Живой прогон runner в режиме draft отработал целиком: визард -> аудитория
(matched=437) -> объявление -> стоимость -> подтверждение -> скрин; кнопку
отправки не жмём, баланс 5010 РУБ не тронут. Юнит-тесты 20/20.
- обход коуч-марка: нативный .click() по DOM-кнопке (domClickButton/continueTo),
т.к. оверлей перехватывает мышиный клик даже с force
- фикс чтения аудитории «Не МТС» (неразрывный пробел, код 160) в readNonMts
+ поллинг завершения асинхронного матчинга
- надёжная категория ОРД (combobox: fill -> pressSequentially -> role=option)
- шаги submitAd->/budget, submitBudget->/confirmation; finalize на /confirmation
(draft=скрин без клика; live=оферта+отправка, за FLOW-CONFIRM)
- fillAd: убраны adTitle/ordCompanyName (полей в кабинете нет, рекламодатель авто)
- task.js/тесты/example: убраны adTitle/ordCompanyName/cpmRub; budgetRub оставлен
как потолок стоимости для боевого запуска
- FLOW-FINDINGS.md: полная карта флоу + две ключевые грабли и их решения
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
(fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
(разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
снимаются на первом реальном черновике после разового логина владельца.
Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задачи 8–9 плана (код, проверка против кабинета — при логине владельца в
профиль бота):
- browser.js — launchPersistentContext(profileDir), humanPause по темпу
- session.js — isLoggedIn по стабильному пункту меню «Рассылки и звонки»
(селектор подтверждён живьём при разведке)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 1 плана: пройден визард Telegram Ads → «Своя база клиентов» в живом
кабинете в режиме только-чтение. Зафиксированы 7 шагов степпера, селекторы,
требования к файлу номеров (TXT/CSV/XLS/XLSX, формат 79XXXXXXXXX, минимум 367
не-МТС), счётчик «МТС/Не МТС», путь удаления черновика. Черновик 2229823 убран,
баланс 5010₽ не изменился. cspell-words: добавлены CPM/CPF/MTC/ЕРИР/Таргеты/алярм.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>