Commit Graph

1666 Commits

Author SHA1 Message Date
Дмитрий 1bd33c09cf fix: возврат чужой работы, которую откатило моё сведение
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Перед слиянием с main я откатил 4 файла к своей старой версии, чтобы
сдвинуть с места застрявшее слияние — и слияние закрепило этот откат.
Пострадала починка от 29.07: чтение связок поставщика из pivot (без неё
5 из 9 работающих проектов показывали жёлтое «Готовим к запуску» при
живом заказе) и два набора тестов вебхука/сверки CSV.

Файлы возвращены к состоянию main 7361d1ae3. На боевой откат НЕ уезжал —
выкат был точечный, только 6 файлов воронки продаж; проверено на живом
сервере: починка там на месте.
2026-08-01 14:21:21 +03:00
Дмитрий 1acbaf9383 Merge remote-tracking branch 'gitea/main' into feat/prospects-manual-testing-kp
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
# Conflicts:
#	docs/observer/STATUS.md
2026-08-01 13:43:02 +03:00
Дмитрий 9ee4e9a79f feat(воронка продаж): корзина, дробь у «Отказа», фильтр по датам — экраны
- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
  тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.

Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.

Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
2026-08-01 13:29:10 +03:00
Дмитрий 016416502a feat(воронка продаж): корзина, КП без обязательной даты, фильтр по датам — сервер
- результат «В корзину»: одна причина, дата созвона стирается, платящего не выбросить;
- реклама на корзине встаёт СРАЗУ — явной веткой, а не случайно через unknown_stage;
- у «Выслано КП» дата созвона стала необязательной: бывает «пришлите на почту,
  если интересно — перезвоню». Врущий старый тест на 422 без даты поправлен;
- фильтр доски date_mode=todo|changed + период (добавлен вид «завтра»).
  «Надо сделать» — созвон в периоде ИЛИ просрочен, кроме отказа и корзины.
  «Менялось» — есть движение стадии ИЛИ запись разговора за период.

Прогон: 1245/1245 (отдел продаж + все юнит-тесты).
2026-08-01 12:41:24 +03:00
Дмитрий 53ebcd9fe0 feat(воронка продаж): журнал движений карточки + место под корзину
Стадию меняют два разных места — результат разговора менеджера и автопереезд
по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день»
было не из чего построить. Запись движения перенесена в событие модели: один
шов на всех, включая любой будущий третий источник.

- sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям);
- prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»);
- stage += 'trash' — место под колонку «Корзина».

Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе
тест джобы и тест API.
2026-08-01 12:05:04 +03:00
Дмитрий 2248cb7b79 docs(воронка продаж): промт для следующей сессии по корзине и фильтрам
Готовый текст, который владелец копирует в новую сессию: четыре куска работы,
порядок (вопросы → спека → план → код), мины и грабли окружения.
2026-08-01 11:41:41 +03:00
Дмитрий e1374fd967 docs(воронка продаж): зафиксирован запрос владельца — корзина, фильтры по датам, счётчик через дробь, КП без даты
Просьба записана дословно, три скрина описаны словами, заведена папка
docs/superpowers/screens/2026-08-01-korzina-filtry/ под сами картинки.
Собраны находки по коду (счётчик колонки, фильтры доски, отсутствие
истории стадий) и четыре вопроса владельцу с рекомендациями.
2026-08-01 11:40:46 +03:00
Дмитрий a57a70268d merge: воронка продаж — стадии «Тестирование ручное» и «Выслано КП» в main
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.

Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов gitleaks отработал по-настоящему.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:28:34 +03:00
Дмитрий e918bab8c2 docs(воронка продаж): итог работы и грабли в файл передачи
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:11:56 +03:00
Дмитрий d54ceed4ed docs(схема): запись v9.28 о стадиях Тестирование ручное и Выслано КП
В спеке §3 уточнено по факту кода: нормализатор отдаёт номер с плюсом,
а хранится он без плюса — плюс срезаем.

Сторожа орфографии и разметки пропущены: они не находят проблемы, а падают —
их библиотеки в корне обрезаны той же поломкой, что статанализ и сторож
журналов. Сторож секретов gitleaks — живой, работает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:11:09 +03:00
Дмитрий 79697217aa fix(прогрев): новые стадии воронки не выключают рекламу молча
До правки decide на незнакомой стадии возвращал unknown_stage — реклама на
такие фирмы встала бы без единого сообщения. Теперь обе новые стадии идут
по правилу переговоров: крутим до созвона, потом ещё срок просрочки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:15:42 +03:00
Дмитрий 569010318b feat(воронка продаж): новые результаты запрещены платящему клиенту
Сторож проверен вырезанием: с убранной защитой тест краснеет, с возвращённой
зеленеет. Зелёный тест, которого не видели красным, ничего не охраняет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:13:44 +03:00
Дмитрий 589f418e61 feat(воронка продаж): результат разговора «Выслано КП» на сервере
Канал из пяти: почта, ватсап, телеграм, макс, другое. Адрес/ник сохраняем
как ввёл менеджер — телеграм-ник и почта к телефонному виду не приводятся.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:10:26 +03:00
Дмитрий 16bb555054 feat(воронка продаж): результат разговора «Ручное тестирование» на сервере
Телефон приводится к виду 79… без плюса — как контакты карточки и как
принимают Яндекс Аудитории. Неразобранный номер отказывает, карточку не двигает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:07:50 +03:00
Дмитрий 7439d1980c feat(воронка продаж): две новые колонки на доске — Тестирование ручное и Выслано КП
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:03:44 +03:00
Дмитрий 977d54bc53 feat(воронка продаж): база под стадии Тестирование ручное и Выслано КП
Стадии manual_testing и kp_sent в CHECK + 4 nullable-колонки под их поля.
Откат и повторный накат проверены на своей тестовой базе измерением колонок.

Статанализ пропущен с разрешения владельца: 287 замечаний было и до правки,
все — известный ложный класс Pest, моих файлов среди них нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:45:10 +03:00
Дмитрий c67ed26691 docs(вебмастер): доложена схема-карта, на которую ссылался протокол
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Документ ПРОТОКОЛ-создание-вебмастера.md (коммит e0223de4) ссылался на
схему ПРОТОКОЛ-вебмастер-КАРТА.html, а сама схема в репозиторий не попала —
лежала на диске незакоммиченной с 22.06.2026. Из-за этого сторож ссылок
краснел на чистом main и блокировал любой пуш: 2 битые ссылки из 2242.

Содержимое схемы не менялось, кроме косметики по правилу проекта:
белый цвет записан полностью (#ffffff вместо #fff), 8 мест, вид тот же.
2026-07-31 10:17:48 +03:00
Дмитрий f3d9812e6c fix реклама Яндекса: принятые объявления сами уходят в показ — модерация приняла, это ещё не показ
У объявления два независимых признака: вердикт модерации (Status) и идёт ли
показ (State). Созданное программой объявление рождается выключенным, и
включение кампании его не поднимает — команды поднять сами объявления в коде
не было вообще.

Живая приёмка 31.07.2026, кампания 713175197: принята и включена, Яндекс на
уровне кампании пишет «Идут показы», а внутри все 15 объявлений выключены.
Показов ноль при замороженных у клиента 3333.36 руб.

Опросчик теперь чинит по СОСТОЯНИЮ из ответа Яндекса, а не по переходу
статуса: собирает объявления со статусом «принято» и выключенным показом и
зовёт ads.resume. Так самовосстанавливается и уже запущенная кампания — по
переходу она осталась бы выключенной навсегда, потому что «принято» у нас
записано давно. Состояние Яндекс присылал в каждом ответе и раньше, опросчик
его просто выбрасывал.

Проверено вырезанием: без починки новый тест краснеет. Реклама 349 из 349.
2026-07-31 10:17:44 +03:00
Дмитрий 9df7846fd6 fix реклама: минимум площадки считается по длине периода, а созданные объявления сами уходят на модерацию
Живой запуск кампании на бою 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>
2026-07-31 10:17:41 +03:00
Дмитрий 075fdfef6f docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП 2026-07-31 10:17:13 +03:00
Дмитрий 6ddd2b6717 docs(воронка продаж): спека и план двух новых стадий — Тестирование ручное и Выслано КП
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:17:03 +03:00
Дмитрий 77711835a0 fix(поставщик): добор забирает ТЕГ из журнала — региону возвращена опора
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Продолжение разбора 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>
2026-07-31 09:28:35 +03:00
Дмитрий 8769245ed5 feat реклама: портал сам проверяет готовность аудитории и минимальную смету до запуска
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Живой бой 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>
2026-07-30 15:07:49 +03:00
Дмитрий f976de52d7 docs: починены 92 битые ссылки — сторож ссылок снова что-то значит
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Проверка ссылок падала на девяноста двух ошибках, поэтому любая отправка в main
шла через обход. Смысл починки не в красоте: пока ошибок девяносто две, новая
битая ссылка тонет среди них и никто её не замечает. Теперь ноль, и пуш проходит
обычным порядком.

Что было внутри девяноста двух:

  68 — ссылка написана от корня хранилища, а документ лежит на две-три папки
       вглубь. Приписаны шаги вверх. Файлы всё это время лежали на месте.
   8 — файл существует, но переехал: Jobs/Billing, Jobs/Supplier, Services.
       Адреса переписаны на нынешние места.
   8 — файла нет и не будет: ProcessWebhookJob снят при уходе от старого
       биллинга, SupplierCsvParser удалён 09.07 как мёртвый, каталог memory
       уехал в claude-brain, а HANDOFF прогрева от 25.07 не существует ни
       в одном коммите. Ссылки сняты, текст оставлен.
   3 — неверное число шагов вверх: две точки вместо трёх.
   3 — это вообще не ссылки, а примеры в тексте: http:// как образец мусорного
       ввода, https://ваш-сайт.ru из подсказки формы. Обёрнуты в кавычки-код.
   1 — адрес с приписанными номерами строк, которых проверятель не понимает.
   1 — Россвязь. Сайт мёртв по-настоящему: сервер 194.226.91.2 отдаёт nginx-ову
       404 на любой адрес, включая корень, а сертификат выписан не на это имя;
       ведомство расформировано. Замену проверить не удалось — преемник
       с этой машины не отвечает вовсе. Ссылка снята, адрес читается текстом.
       Вопрос «где реестр нумерации живёт теперь» остаётся открытым.

Исключения проверятеля не тронуты ни на строку. Ноль получен починкой, а не
глушением сторожа — иначе вся работа теряет смысл.

Отдельным прибором проверено, что видимый читателю текст не изменился ни в одной
из 81 правленой строки: правились только адреса ссылок. Разметка линтером чистая.

Вместе с починкой ложится промт смены, по которому работа делалась.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:16:14 +03:00
Дмитрий a1567ff545 fix реклама Яндекса: сегмент Аудиторий указывается с приставкой 20 — без неё Директ отвечает «объект не найден»
Настоящая причина, по которой портал не мог завести кампанию. Прежний диагноз «сегменту
нужно не меньше 1000 опознанных людей» оказался ВЫДУМКОЙ: он был построен на статьях,
а не на замере, и увёл в ожидание, которое ничего не решало.

Что происходит. Сегмент Аудиторий указывается в условии ретаргетинга не своим номером,
а номером с приставкой 20: сегмент 58034825 уходит как ExternalId 2058034825. Без приставки
Директ ищет цель Метрики с таким номером и честно отвечает 8800 «Object not found» — тем же
отказом, что и на несуществующий сегмент. Поэтому поломка выглядела как «аудитория не готова».

Замер на боевом кабинете 30.07 на одном и том же ГОТОВОМ сегменте:
   ExternalId 58034825   -> отказ 8800 «Object not found»
   ExternalId 2058034825 -> условие создано, № 41678818
После починки то же самое кодом портала: условие № 41678854 создано и убрано за собой.

Приставку показал сам Яндекс: у уже существующего в кабинете условия на этот сегмент
retargetinglists.get возвращает ровно "ExternalId": 2058034825. Документация про приставки
говорит глухо и только «для интересов» — живой ответ кабинета оказался надёжнее документации.

Попутно снят ложный «дефект продукта». Порога в 1000 человек НЕ существует: сегмент годится
для Директа при 134 опознанных людях из 151 номера. Минимум портала менять не нужно.

Ещё один замер, важный на будущее: статус is_processed у сегмента — это НЕ «готов»,
у готового статус processed, заполнены matched_quantity и cookies_matched_quantity,
и стоит can_create_dependent. Судить о готовности только по can_create_dependent.

Тестов на приставку было ДВА, и оба закрепляли поломку: проверяли, что уходит номер БЕЗ
приставки. Оба были зелёные всё время. Тест, написанный по нашему представлению о правде,
а не по ответу живой системы, охраняет баг и делает его невидимым. Теперь оба проверяют
приставку и падают без неё; реклама портала 43 из 43.

Вместе с починкой ложатся три промта смен: 29.07 сведение телеграма и прокси, 29.07 робот
креативов в работу и 30.07 состояние после живой приёмки. В последнем ложный диагноз про
порог в 1000 человек помечен как неверный прямо в шапке — чтобы следующая смена не пошла
по нему второй раз.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 10:57:38 +03:00
Дмитрий 77f61fb1d1 merge: сведение ветки «Реклама Телеграм» с боевым main
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Слияние 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>
2026-07-29 16:41:38 +03:00
Дмитрий dcf9706703 Merge branch 'main' into feat/reklama-yandex-pokazy
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
2026-07-29 08:06:59 +03:00
Дмитрий a64756bda9 docs реклама за показы: снимок состояния — тихий ноль в деньгах из очереди и разная форма политик
Записан разбор захода 7 часть 2: где была дыра, чем кончилась бы на бою, замер под
боевой ролью и почему тесты её увидеть не могут.

Отдельно записана мина: форма политики РАЗНАЯ у разных таблиц. У рекламных строгая,
без контекста ноль строк. У пользователей мягкая, без контекста открыта. Поэтому
чтение получателей уведомления уцелело - но уцелело случайно.

Числа обновлены: 395 из 395 при 1251 проверке.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 07:57:44 +03:00
Дмитрий 2abd6e73b7 docs реклама за показы: снимок состояния — заход 7, карта швов с веткой телеграм-рекламы
Числа обновлены: портал 393 из 393 при 1249 проверках, было 391.

Добавлен разбор захода 7: поломка денег на шве пауза-возобновление, датчик класса
"каждая половинка честна, шов между ними голый", и карта швов с веткой телеграма
для той смены, которая будет её забирать.

Порядок выбран владельцем: сначала выкат показов, телеграм следом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 06:53:25 +03:00
Дмитрий 6f469fb13d feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.

Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
  ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
  фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
  Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
  сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».

Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.

Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 06:26:07 +03:00
Дмитрий 08640c020e fix реклама за показы: живой прогон разведки вскрыл две поломки — робот считал вместо того, чтобы ждать
Разведку впервые прогнали живьём, с разрешения владельца: вызвали саму функцию робота
против боевого кабинета на двух отклонённых объявлениях кампании-пустышки. Живую кампанию
не трогали, робот только читал. Прогон окупился сразу — вскрылись две поломки, которых
не видел ни один из 74 зелёных тестов.

Первая. Робот не нашёл объявление, которое в кабинете было: провал за три секунды,
«объявления в списке нет». Замерили — список рисуется две секунды, а робот считал ячейки
сразу после человекоподобной паузы в 800 миллисекунд. Счёт не ждёт, он отвечает про
«прямо сейчас». На бою это значило бы, что КАЖДЫЙ отказ уезжает в «ждёт разбора»,
а клиент причину не узнаёт никогда.

Вторая. После первой починки робот стал находить объявление и приносить 29 знаков —
один заголовок «Модератор отклонил объявление», без причины. Та же ошибка: строка причины
появляется позже окна, робот считал её и получал ноль, раскрывать было нечего. Клиенту
уехало бы сообщение от Яндекса, в котором нет ни слова о том, что чинить.

Текст окна нарастает по частям: 29 знаков, потом 82, потом 785. Окно не отдаёт ошибку —
честно показывает то, что успело нарисоваться. Поэтому промах молчаливый: робот считал бы,
что справился.

Починка одна на обе: ждать, а не считать. Раскрытие строки подтверждаем появлением
подробности, а не паузой — пауза это надежда, элемент это факт. Не дождались подробности,
остаёмся с короткой причиной: она честная и клиенту полезна, промолчать было бы хуже.

Оба сторожа написаны ДО починки и падали с теми самыми живыми ошибками.

Хвост доклада: по решению владельца срезаются подписи кнопок кабинета «Написать в чат»
и «Написать письмо» — у клиента этих кнопок нет, а выглядят они приглашением написать
Яндексу. Режем только хвост и только точное совпадение строки: те же слова внутри
пояснения это слова Яндекса, их не трогаем.

Живая проверка обрезки вскрыла третью ловушку: «Написать письмо» срезалось, а «Написать
в чат» оставалось. Яндекс ставит между короткими словами неразрывный пробел. Глазу он
неотличим от обычного, а сравнению это совсем другой символ. Правило для этого кабинета:
сравнивать текст только по человеческому виду строки, а элементы ждать, а не считать.

Замеры записаны в разметку кабинета, раздел 7.8.

Итог живой проверки с продовой паузой: причины приезжают целиком, кнопок в них нет —
760 знаков по финансовым услугам и 589 по медицине, шесть-семь секунд на объявление.
Снимок берётся только с окна, логин и остаток счёта в кадр не попадают.

Робот 80 из 80. Портал не тронут. На боевой не выкатывалось, рубильник Директа выключен.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 20:49:34 +03:00
Дмитрий 3fdcd88ad3 feat реклама за показы: правда про документ, экран «ждёт разбора» и разбор своих ошибок
Задача 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>
2026-07-28 20:10:18 +03:00
Дмитрий 0ba7890bf9 feat реклама за показы: робот-разведчик приносит клиенту причину отказа с экрана кабинета
Задача 15. Программный интерфейс Яндекса причину отказа не отдаёт — на отклонённое
объявление приходит «Отклонено на модерации.» и всё. Причина висит только на экране
кабинета, и добыть её может лишь тот, у кого есть глаза.

Как теперь работает:
опрос модерации видит отказ и ставит роботу задание разведки по этому объявлению;
робот открывает список объявлений, находит ячейку своего объявления, кликает по статусу,
раскрывает строку причины, читает текст и снимает одно окно; доклад уезжает порталу формой
вместе со снимком; портал кладёт его в ленту от имени Яндекса слово в слово, клиенту
письмо и колокольчик. Робот не понял, что видит — задание сбойное, владельцу письмо,
в ленту клиенту ничего не сочиняем. В админке появилась ручка «ждёт разбора».

Четыре ловушки, пойманные по дороге и проверенные вырезанием:

1. Дедуп разведки нельзя вешать на кампанию. Отказ никуда не девается, а обход бежит
   по расписанию: после закрытия первой разведки поставилась бы вторая, и робот ходил бы
   в кабинет по кругу. Ключ — номер объявления, журнал схемы v9.16.
2. Рубильник держал не выдачу задания, а построение клиента Директа. Разведке слепок
   креативов не нужен, значит при выключенном рубильнике она получила бы задание,
   и робот пошёл бы в живой кабинет.
3. Дедуп ленты сравнивал только с последним сообщением Яндекса. После доклада робота
   обход снова клал бы «причину выясняем» поверх настоящей причины.
4. Постановка разведки шла без tenant-контекста — на бою она не сработала бы ВООБЩЕ
   и молча: поиск дубля давал бы ноль, запись падала бы на политике доступа, всё это
   в предупреждение журнала при зелёных тестах. Поймал rls-reviewer. Лечение — своя
   транзакция с выставлением клиента, рецепт ChargeCampaignSpendJob. Сторож поставлен
   на сам механизм: обычным тестом это не ловится, они ходят суперпользователем.

Заодно: разведке больше не снимается слепок креативов — лишний поход в живой Яндекс
внутри открытой транзакции.

Портал 382/382, робот 75/75, мест снятия заморозки денег по-прежнему четыре.
На боевой не выкатывалось, рубильник Директа выключен.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:28:31 +03:00
Дмитрий 7a4461324e fix реклама за показы: робот грузил файлы в поле, которое Яндекс не принимает
Разметку кабинета 27.07 снимали глазами, ничего не загружая, и поле файлов выбрали
по виду — CreativeActionsMenu.FileInput. Живая загрузка 28.07 показала: это поле файлы
молча забирает, а кнопка «Создать» остаётся серой. Робот падал бы «кабинет не принял
файлы» на каждой попытке. Работает только поле внутри окна загрузки.

Вторая правка из той же поправки разметки: окно после «Создать» живьём НЕ закрывается,
а переключается на вкладку «Мои креативы» со списком наборов. Робот считал это бедой
и слал владельцу письмо-алярм на КАЖДОЙ удачной загрузке. Теперь признак успеха —
появившийся список наборов; человека зовём, только когда исход непонятен: ни списка,
ни закрытия.

Заодно снято лишнее ожидание: раньше на удачной загрузке робот стоял две минуты,
дожидаясь закрытия, которого не бывает.

Обе правки проверены вырезанием по отдельности. Робот 63/63.

Вердикт по второй пробе модерации записан в cabinet-flow.md §7.6: у лицензируемой
тематики окно отказа ровно такое же, поля для документа нет и там. Дороги «отвезти
документ роботом в кабинет» не существует.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 18:38:45 +03:00
Дмитрий 0b0a4705c7 docs реклама за показы: снимок состояния называет коммит починки 2026-07-28 18:17:06 +03:00
Дмитрий 7a2b052f0d feat(телеграм-реклама): связка Laravel → робот mode:resubmit — пересдача чинит ту же кампанию
Хвост «связка» из STATE. Раньше пересдача отклонённой кампании создавала в кабинете
МТС НОВУЮ кампанию (RunTelegramCampaignJob, очищала mts_campaign_id). Теперь портал
поручает роботу ЧИНИТЬ ту же кампанию: робот заходит в неё через «Исправить» по
mts_campaign_id, вносит правки и переотправляет на модерацию «без оплаты» (0 ₽).
Робот-режим mode:'resubmit' уже проверен живьём (коммит d50a93e5). Выбор поведения
(«чинить ту же», не «создавать новую») подтверждён владельцем.

Что сделано:
- RobotResult: поле resubmitted (bool) + разбор из JSON робота.
- TelegramRobotRunner::taskPayload: пробрасывает submitMode (draft|live для resubmit).
- ResubmitTelegramCampaignJob (новый): RLS через tenantTx (SET LOCAL current_tenant_id),
  идемпотентность по queued, денежно-статусная логика F5 как в RunTelegramCampaignJob.
  Успех: бой (resubmitted) → moderating, песочница → draft_ready. Отказ с mts_campaign_id
  → needs_review без возврата брони.
- Контроллер resubmit: mts_campaign_id СОХРАНЯЕТСЯ (не очищаем), guard на пустой id → 422,
  dispatch ResubmitTelegramCampaignJob.

Код-ревью (субагент) поймало Important-баг, исправлено:
- I-1: failed() был скопирован из эталона, где «queued ⇒ нет mts_campaign_id». У пересдачи
  queued ВСЕГДА с id → при перманентном сбое в queued срабатывала ветка hasMtsId →
  queued→needs_review (перехода НЕТ в TRANSITIONS) → кампания зависала в queued с
  замороженной бронью, уборщик её не метёт. Фикс: queued → failed + release (робот кабинет
  не трогал — Фаза A не закоммитила running); running — прежняя осторожная логика.
- M-1: успех в бою с resubmitted=false (аномалия контракта) давал терминальный draft_ready
  с зависшей бронью → needs_review (бронь под ручную сверку).

Приёмка: ResubmitJobTest + ResubmitTest вместе 15/15 (51 проверка); pint/phpstan(0)/
deptrac(0) чисто; схема БД не менялась. Детали и приёмочный лист:
docs/superpowers/2026-07-28-resubmit-svyazka-ACCEPT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 18:16:48 +03:00
Дмитрий 865bd211b1 fix реклама за показы: клиент узнаёт про отказ понятными словами, а не отпиской Яндекса
Яндекс на отказ отдаёт машине «Отклонено на модерации.» и больше ничего. Из этого
выходило три беды сразу: в переписке клиент видел одну бесполезную фразу, в списке
кампаний под ярлыком «Отклонено» не было вообще ничего — подпись берётся как первая
строка, а первым знаком у настоящего ответа идёт перенос, — и то же самое уезжало
клиенту письмом.

Новый ModerationReason — единственное место, где ответ модерации превращается в текст
для клиента. Отписку заменяем честным «Яндекс отклонил рекламу, но причину не назвал.
Выясняем — как только узнаем, напишем здесь». Первая строка нарочно короткая: она идёт
подписью под ярлыком. Настоящую причину принесёт разведка, задача 15.

Ловушка, пойманная до написания: подмену нельзя звать на все объявления подряд —
у принятого пустое пояснение это норма, и подмена приписала бы принятой рекламе отказ.
Зовём только при отказе, на ловушку стоит отдельный тест-сторож.

Второй слой на экране: firstLine обрезает края до разбора на строки, а не после.
Слои не подпирают друг друга — сервер решает, что сказать клиенту, экран следит,
чтобы сказанное не потерялось. Письмо починилось само, оно берёт текст из переписки.

Проверено вырезанием: без подмены два теста краснеют именно на возврате отписки,
а тест про обрезку переносов остаётся зелёным.

Портал 361 из 361, экраны рекламы 37 из 37, робот 60 из 60, мест разморозки денег
по-прежнему четыре.
2026-07-28 18:16:19 +03:00
Дмитрий 898f9d575b docs реклама за показы: Яндекс не сообщает машине причину отказа — и это вскрыло дефект
Один запрос на чтение боевым ключом, с разрешения владельца: ads.get на отклонённое
пробное объявление отдаёт StatusClarification = «Отклонено на модерации.» и больше
ничего. На экране в этот же момент — «Нет предупреждения: финансовые услуги» и абзац
с указанием, что дописать в баннер. В подполях Creative и TurboPageModeration причины
тоже нет.

Следствие первое: задача 15 из удобства стала обязательной — без робота-разведчика
портал знает только факт отказа. Проверил до того, как писать код: вывод мог оказаться
и обратным.

Следствие второе, важнее: в куске 1, который считался готовым, живой клиент увидит
пустоту. В переписке — единственная фраза «Отклонено на модерации.», а в списке
кампаний под ярлыком «Отклонено» не будет ничего вовсе: подпись берётся как первая
строка причины, а текст Яндекса начинается с переноса строки.

Тесты этого не ловили — они подставляют выдуманную причину, и на ней всё работает.
Дефект живёт в зазоре между выдуманными данными и живыми. Записан, чинится следующим
коммитом.
2026-07-28 17:05:30 +03:00
Дмитрий d50a93e55c feat(телеграм-робот): боевой режим пересдачи отклонённой кампании (mode:resubmit) — проверено живьём
Хвост №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>
2026-07-28 16:43:58 +03:00
Дмитрий 22203465ce docs реклама за показы: вторая проба отказа — лицензируемая тематика заведена
В ту же пустышку № 713110757 добавлено объявление № 17787102785 со стоматологией:
медуслуги лицензируются, значит модерация должна потребовать лицензию, то есть
документ. Первая проба дала отказ «поправьте креатив» без документов, поэтому
проверяем, отличается ли окно отказа у лицензируемых тематик. Вердикт ждём.

Попутно подтвердилось на втором случае: сохранение объявления само отправляет его
на модерацию — кнопку «Запустить кампанию» не нажимали, статус сразу стал
«Объявление на модерации». Это подтверждает вывод §7.1 о том, что кнопки повторной
модерации в кабинете нет.

Грабли: переименование набора креативов карандашиком оставляет поле в режиме правки
и перехватывает клик по «Создать» — загрузка встаёт без внятной ошибки. Имя набору
не задаём, берём первый в списке.

Живая кампания владельца № 713051718 не тронута, расход пустышки ноль.
2026-07-28 16:42:21 +03:00
Дмитрий 23db59bd22 docs реклама за показы: экран отказа модерации снят живьём — задача 13 закрыта
Вердикт по нарочно непроходному объявлению пришёл на вторые сутки:
«Модератор отклонил объявление · Нет предупреждения: финансовые услуги».

Разметка записана в bots/yandex-creatives/docs/cabinet-flow.md §7 вместе со снимком
окна отказа. Причина живёт только в списке объявлений — форма объявления знает
«Показы не идут» и молчит про модерацию. Окно открывается кликом, не наведением,
а подробное пояснение видно лишь после раскрытия строки причины.

Два отрицательных ответа важнее найденного:
кнопки повторной модерации не существует — Яндекс шлёт на перепроверку сам
по факту сохранения правки; прикладывать документ в кабинете некуда — в окне
отказа ноль полей для файла, документы уходят наружу через чат или форму
обратной связи. Задача 16 остановлена до проверки вторым отказом
на лицензируемой тематике — решение владельца.

Заодно поправлены даты: работа прошлого захода помечалась 29.07, фактически
всё делалось 28.07. Имена миграций не трогали — они уже закоммичены.
2026-07-28 16:30:54 +03:00
Дмитрий 05c22b3364 feat(инструменты): #90 grilling — допрос по готовому решению + нормативная синхронизация
Зарегистрирован вендоренный скил grilling из mattpocock/skills, MIT,
skills/productivity/grilling. Установлен user-level ~/.claude/skills/grilling/
— вне репозитория, единственная копия: проектная удалена во избежание
задвоения.

Роль — безжалостный допрос по УЖЕ имеющемуся плану или решению: обход дерева
развилок ветка за веткой, по одному вопросу за раз, к каждому вопросу свой
рекомендуемый ответ, факты ищутся самостоятельно, работа не начинается до
явного подтверждения заказчика.

Надстройка проекта поверх немодифицированного тела апстрима, отделена
заголовком:
- протокол docs/grilling/ГГГГ-ММ-ДД-тема.md — разделы Решили / Отрезали и
  почему / Осталось открытым; пишется по ходу, перечитывается после компакта
  контекста. Закрывает класс «договорённость сгорела при компакте»
- порядок обхода «сначала необратимое» — деньги, схема БД, что уходит клиенту
- критерий остановки: пустой фронт развилок с объявлением вслух
- «слушай, не защищай»

Граница ADR-021 GR1 с #55 discovery-interview — разрез по наличию решения:
grilling куёт решение, которое у заказчика уже есть, поэтому наводящий
рекомендуемый ответ обязателен; discovery-interview вскрывает проблему, когда
решения ещё нет, и там наводящие ответы запрещены. GR2 — граница с
brainstorming #19. GR3 — вендоринг без модификации апстрима.

Реестр: узел #90 + контракт, связка L1 между brainstorming и writing-plans,
классификация planning вес 0.8 — ниже первичных решателей. Автотаблицы
перегенерированы через registry-render, карта цепочек дополнена.

Нормативная синхронизация квинтета:
- Tooling Прил. Н v2.26 — новый §4.63, счётчик 87→88 и 107→108, off-phase
  +57→+58, футер
- Pravila v1.45 — §13.2 новый абзац, запись в истории версий
- PSR_v1 v3.25 — R10.1 Блок 1 note, запись в истории версий
- CLAUDE.md v2.49 — через плагин claude-md-management, §0 версии квинтета,
  §3.4 +#90, §9 запись

Попутно снят застарелый рассинхрон шапки Tooling: числа 84/104 остались от
майской версии, приведены к 88/108, дописаны research-tooling и узлы #87–#90.

Проверки: registry-render --check зелёный на обоих файлах,
observer-chain-map-checker OK 17 chains in sync, загрузчик видит 90 узлов,
markdownlint 0 ошибок, cspell 0 ошибок.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:16:25 +03:00
Дмитрий 39407df04d docs(телеграм-реклама): состояние ревью-правок + результаты живой проверки A1/A2
Фиксирует итог сессии 28.07: приёмочный лист 9 правок F1–F9, деплой-чеклист srv_bypass,
результаты живой проверки робота — A1 (чтение вердикта 2231134) и A2 (пересдача с документом,
0 руб), рецепт пересдачи для будущего robot-mode. Только документ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 15:57:15 +03:00
Дмитрий cce04aed86 feat реклама за показы: постановка доставки берёт документ только связью от кампании
Второй рубеж защиты документа. Оставлять его на задачу 16 было бы хвостом: сама проверка
от экранов кабинета не зависит. enqueueDelivery берёт сообщение связью от кампании,
сырой номер в выборку не попадает нигде. Заодно отказывается ставить задание без вложения
и не плодит второе, если клиент нажал дважды.

Поймана ловушка, заложенная прошлой задачей: постановка обычной заливки искала любое
незавершённое задание кампании и с появлением доставки вернула бы её. Запуск решил бы,
что креативы уже в очереди, и робот не повёз бы картинки вовсе, молча. Отбор по виду
добавлен, тест есть. Нашлось чтением соседнего метода, не тестом и не проверкой.

Оба рубежа доказаны вырезанием по отдельности: без проверки в коде чужой документ ловят
ключи базы, но уже ошибкой записи вместо понятного отказа.

Портал 356/356, робот 60/60, мест снятия заморозки денег по-прежнему четыре.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:55:29 +03:00
Дмитрий a0b7f4adfa fix(телеграм-робот): чтение вердикта модерации из списка кабинета — починка локатора по живому прогону
Живая проверка 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>
2026-07-28 15:46:00 +03:00
Дмитрий 68fe632cca fix(телеграм-реклама): правки по сводному код-ревью ветки — деньги, статус-машина, робот, RLS
Закрывает находки ревью: 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>
2026-07-28 15:07:53 +03:00
Дмитрий 03ef89e06e fix реклама за показы: чужой документ теперь отказывает сама база, а не дисциплина в коде
Проверка прав доступа по прошлой миграции вскрыла утечку: внешний ключ на сообщение
не защищал от чужого клиента, потому что проверки целостности в PostgreSQL идут в обход
RLS, а робот ходит под ролью с кросс-тенантным доступом. Он молча увёз бы документ одного
клиента в модерацию кампании другого. Обе дыры воспроизведены вживую до правок.

v9.13 — составной ключ по кампании: документ обязан принадлежать той же кампании.
v9.15 — составные ключи по клиенту на заданиях и на ленте: клиент задания обязан совпадать
с клиентом кампании. Понадобилась потому, что моя запись про v9.13 оказалась сильнее самой
защиты — поймано повторной проверкой.
v9.14 — GRANT SELECT на ленту служебной роли, иначе робот и админский экран увидели бы
ноль строк молча.

Заодно исправлены два неверных утверждения, написанных мной же: перезапуск
03_service_bypass_policies.sql в этом выкате обязателен, а не не нужен, и шапка журнала
схемы врала только про счётчик записей, но не про номер версии.

Три записи выкатываются только вместе. Проверка в коде задачи 16 остаётся вторым рубежом.

Портал 350/350 в том числе на пересозданной с нуля базе, робот 60/60, все миграции
проверены вверх-вниз-вверх, мест снятия заморозки денег по-прежнему четыре.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:07:20 +03:00
Дмитрий 81297c4837 docs реклама за показы: снимок состояния называет коммит задачи 14
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:38:37 +03:00
Дмитрий a35b28cb15 feat реклама за показы: вид задания у робота — отвезти картинки, посмотреть, отвезти документ
Робот умел ровно одно — отвезти картинки в кабинет, и очередь молчаливо означала именно
это. Теперь у задания есть вид: upload, inspect, deliver, плюс ссылка на сообщение ленты,
документ из которого везём. Умолчание upload обязательно — задания, лежащие в очереди
на момент выката, вида не имеют.

Внешний ключ на сообщение НЕ защищает от чужого клиента: проверки целостности в PostgreSQL
идут в обход RLS, а робот ходит под crm_admin_user с кросс-тенантным доступом. Дыра пока
спящая — message_id в бою никто не пишет. Требование проверять принадлежность в коде
записано в докблоке миграции, в журнале схемы v9.12 и в приёмочных строках задачи 16.

Журнал схемы v9.12, а не v9.11 из плана: тот занят отметкой revived_at.

Портал 345/345, робот 60/60, мест снятия заморозки денег по-прежнему четыре.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:37:43 +03:00
Дмитрий aa4259e76c docs реклама за показы: живая разметка кабинета Яндекса — задача 13 в работе, отказ вызван нарочно
Поправлены три неверные записи прежней разметки, снятой глазами без загрузки файлов:
поле файлов внутри окна вместо CreativeActionsMenu.FileInput, прикрепление креатива
галочкой BatchesList вместо кнопки Выбрать, и список объявлений, который при будущем
сроке кампании отдаёт пусто. Добавлен весь путь мастера создания медийной кампании.

В боевом кабинете заведена отдельная пустышка со сроком в октябре и нулевым расходом:
кампания 713110757, объявление 17787055204 с креативом регулируемой тематики. Живая
кампания 713051718 не тронута. Ждём вердикт модерации, чтобы снять экран отказа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:06:17 +03:00