Commit Graph

8 Commits

Author SHA1 Message Date
Дмитрий 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
Дмитрий 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
Дмитрий 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
Дмитрий 9cc3f5e950 fix реклама за показы: пауза не врёт про остановку, цена запуска остаётся на кампании, робот не дерётся сам с собой
Хвосты денег Д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>
2026-07-28 06:36:11 +03:00
Дмитрий 0548fc0d8f fix реклама за показы: слепок креативов при выдаче, сверка размера перед объявлением, один баннер на слот
Хвосты портала П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>
2026-07-28 05:37:56 +03:00
Дмитрий 542b7d2d22 fix реклама за показы: очередь робота больше не встаёт колом, доклад об успехе не врёт, тихий ноль ловится
Р6 — задание, брошенное «в работе», держало очередь для всех клиентов навсегда.
Причина сбоя обрезается до 900 знаков, иначе портал отвергал отчёт о самом частом
виде поломки. Провал отчёта больше не глотается молча — едет в письмо человеку.
Приём отчёта ловит любую ошибку и закрывает задание сбойным вместо 500 роботу.
Новый сторож creative-jobs:reap каждые 10 минут разбирает пробку.

Р7 — обрыв на докладе «готово» объявлял провалом уже сделанную работу. Доклад
вынесен из-под обработчика сбоя, повторяется трижды, при неудаче только письмо.
Осечка ухода со страницы после «Создать» больше не считается провалом загрузки.

Р8 — пустой список размеров считался успехом, и робот заливал одни и те же файлы
по кругу, оставляя мусор в живом кабинете. Теперь громкий отказ до похода в Яндекс.

Портал 274/274, робот 41/41. Все защиты проверены вырезанием.
Выходов снятия заморозки денег по-прежнему четыре.
2026-07-27 20:42:23 +03:00
Дмитрий d26716ed90 fix реклама за показы: срок показа закрывает кампанию, отчёт робота только по заданию в работе, гонка выдачи заданий
Р1. Кампания, не добравшая смету показов, оставалась running навсегда, а заморозка
денег клиента - ACTIVE навсегда: единственным переходом в completed было
delivered >= paid_impressions, а задачи, закрывающей кампанию по истечении срока
показа, не существовало вовсе. Для медийки по списку телефонов недокрут сметы -
типовой исход, а не редкий случай.

Новая колонка ad_campaigns.shows_until хранит последний день показа - ровно тот,
что уходит в Директ параметром EndDate. Пишется вместе с yandex_campaign_id, то
есть в момент, когда дату начинает держать Яндекс; возобновляемый запуск
переиспользует уже записанную дату, чтобы портал и Директ считали срок одинаково.
У выхода 1 в CampaignImpressionCharger появилось второе условие - новых мест
вызова AdWalletService::release не добавилось, их по-прежнему ровно четыре.
Запись v9.07 в журнале схемы, rls-reviewer GO.

Р2. Отчёт робота принимался по любому заданию в любом состоянии: номер брался из
адреса как есть. Готово по чужому ещё не выданному заданию разложило бы номера
креативов чужой кампании по её баннерам - картинка одного клиента уехала бы в
объявление другого; сбой по уже закрытому заданию переписал бы правильный
результат на failed. Теперь done принимает отчёт только по заданию в статусе
taken - 409 в остальных случаях, та же проверка продублирована в сервисе.

Р3, первая половина. Проверка «в работе никого» в takeNext не блокировала строку:
две одновременные выдачи обе её проходили и уносили разные задания. Слепки
creatives.get перемешивались, а размеры блоков у всех клиентов одинаковые, поэтому
итог - тихая привязка чужого номера креатива. Гарантию даёт частичный уникальный
индекс uq_creative_job_single_taken, плюс advisory-замок первой строкой транзакции,
чтобы штатный путь спокойно отвечал роботу «работы нет».

Осталось по Р3: привязать выдачу файла к номеру задания, rls-reviewer по индексу,
запись v9.08 в журнал схемы. Ход работы - в файле PROGRESS рядом с промтом v12.

Тесты, прогнаны в одиночку: портал 256/256, робот 35/35. Все девять новых тестов
были красными до правок, каждая защита проверена вырезанием.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:36:03 +03:00
Дмитрий 9234a9c2bc feat реклама показы: очередь заданий робота-грузчика, служебный канал и постановка при запуске
Портал ставит роботу задание, когда у баннеров ещё нет номеров креативов: вместо
ошибки клиент видит «готовим картинки», кампания остаётся черновиком, деньги не
морозятся. Робот берёт задания строго по одному — иначе слепки креативов до и
после перемешаются, и опознать их будет нельзя.

Канал робота закрыт своим сервис-токеном, внесён в исключения проверки CSRF и
отдаёт файл только того задания, которое сейчас в работе. Постановка задания
стоит внутри проверки рубильника Директа — при выключенном рубильнике портал в
Яндекс не ходит.

Права на новую таблицу выданы роли crm_admin_user: канал идёт через посредник
admin-db, подменяющий подключение. Нумератор выдан crm_app_user — он единственный
вставляет строки. Журнал схемы — запись v9.06.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:13:02 +03:00