From a1567ff545caaf300976fab9baabe5fa748e4e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 30 Jul 2026 10:57:38 +0300 Subject: [PATCH] =?UTF-8?q?fix=20=D1=80=D0=B5=D0=BA=D0=BB=D0=B0=D0=BC?= =?UTF-8?q?=D0=B0=20=D0=AF=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=B0:=20=D1=81?= =?UTF-8?q?=D0=B5=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=20=D0=90=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=B9=20=D1=83=D0=BA=D0=B0=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D1=81=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B0=D0=B2=D0=BA=D0=BE=D0=B9=2020=20?= =?UTF-8?q?=E2=80=94=20=D0=B1=D0=B5=D0=B7=20=D0=BD=D0=B5=D1=91=20=D0=94?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BA=D1=82=20=D0=BE=D1=82=D0=B2=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=C2=AB=D0=BE=D0=B1=D1=8A=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=BD=D0=B0=D0=B9=D0=B4=D0=B5=D0=BD=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Настоящая причина, по которой портал не мог завести кампанию. Прежний диагноз «сегменту нужно не меньше 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 --- .../Advertising/YandexDirectClient.php | 12 +- .../Advertising/YandexDirectClientTest.php | 3 +- .../YandexDirectMediaClientTest.php | 32 ++- ...-07-29-HANDOFF-telegram-sveden-i-proksi.md | 154 +++++++++++++ ...026-07-29-HANDOFF-yandex-robot-v-rabotu.md | 123 ++++++++++ ...OFF-yandex-reklama-posle-zhivoy-priemki.md | 210 ++++++++++++++++++ 6 files changed, 530 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/2026-07-29-HANDOFF-telegram-sveden-i-proksi.md create mode 100644 docs/superpowers/2026-07-29-HANDOFF-yandex-robot-v-rabotu.md create mode 100644 docs/superpowers/2026-07-30-HANDOFF-yandex-reklama-posle-zhivoy-priemki.md diff --git a/app/app/Services/Advertising/YandexDirectClient.php b/app/app/Services/Advertising/YandexDirectClient.php index 668718ba..7ba86e7d 100644 --- a/app/app/Services/Advertising/YandexDirectClient.php +++ b/app/app/Services/Advertising/YandexDirectClient.php @@ -29,6 +29,14 @@ final class YandexDirectClient * for goal or segment of Yandex.Metrica», а следом «Object not found» — довод считается * негодным целиком, и запуск кампании встаёт. Поймано живым запуском 29.07.2026. * Берём срок из настроек кампании: сколько дней назад собирали аудиторию. + * + * 🔴 Номер сегмента уходит С ПРИСТАВКОЙ «20»: сегмент 58034825 → ExternalId 2058034825. + * Приставка говорит Директу, ЧТО за объект ему дали: без неё он ищет цель Метрики + * с таким номером и отвечает «Object not found» — тем же отказом, что на несуществующий + * сегмент, поэтому поломка год могла бы выглядеть как «аудитория не готова». + * Замер 30.07.2026 на боевом кабинете, один и тот же ГОТОВЫЙ сегмент: без приставки — + * отказ 8800, с приставкой — условие создано. Приставку показал сам Яндекс: у готового + * условия в кабинете `retargetinglists.get` возвращает ровно `ExternalId: 2058034825`. */ public function addRetargetingList(string $name, int $segmentExternalId, int $membershipDays = 30): int { @@ -39,7 +47,7 @@ final class YandexDirectClient 'Rules' => [[ 'Operator' => 'ALL', 'Arguments' => [[ - 'ExternalId' => $segmentExternalId, + 'ExternalId' => (int) ('20'.$segmentExternalId), // Яндекс принимает от 1 до 540 дней. 'MembershipLifeSpan' => max(1, min(540, $membershipDays)), ]], @@ -233,7 +241,7 @@ final class YandexDirectClient // Тогда портал не может снять слепок, задание роботу не выдаётся, а клиент // видит вечное «готовим картинки». Поймано живым запуском 29.07.2026: // прежде ходили в песочницу, а она отвергала запрос раньше, ещё на входе. - 'SelectionCriteria' => new \stdClass(), + 'SelectionCriteria' => new \stdClass, 'FieldNames' => ['Id', 'Type', 'Width', 'Height'], 'Page' => ['Limit' => self::PAGE_LIMIT, 'Offset' => $offset], ]); diff --git a/app/tests/Unit/Advertising/YandexDirectClientTest.php b/app/tests/Unit/Advertising/YandexDirectClientTest.php index ecce7e6e..391498b9 100644 --- a/app/tests/Unit/Advertising/YandexDirectClientTest.php +++ b/app/tests/Unit/Advertising/YandexDirectClientTest.php @@ -25,7 +25,8 @@ it('adds a retargeting list referencing an audience segment', function () { && $body['method'] === 'add' && $body['params']['RetargetingLists'][0]['Type'] === 'AUDIENCE' && $body['params']['RetargetingLists'][0]['Rules'][0]['Operator'] === 'ALL' - && $body['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]['ExternalId'] === 58034825; + // Сегмент Аудиторий указывается с приставкой «20» — см. addRetargetingList(). + && $body['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]['ExternalId'] === 2058034825; }); }); diff --git a/app/tests/Unit/Advertising/YandexDirectMediaClientTest.php b/app/tests/Unit/Advertising/YandexDirectMediaClientTest.php index bbe93ae7..c93ece30 100644 --- a/app/tests/Unit/Advertising/YandexDirectMediaClientTest.php +++ b/app/tests/Unit/Advertising/YandexDirectMediaClientTest.php @@ -344,9 +344,39 @@ it('в условие ретаргетинга кладёт срок хране Http::assertSent(function ($req) { $arg = $req->data()['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]; - expect($arg['ExternalId'])->toBe(58276009); + expect($arg['ExternalId'])->toBe(2058276009); expect($arg['MembershipLifeSpan'])->toBe(30); return true; }); }); + +/** + * 🔴 Сегмент Аудиторий в условии ретаргетинга указывается НЕ своим номером, а номером + * с приставкой «20»: сегмент 58034825 → ExternalId 2058034825. Без приставки живой Директ + * отвечает «Object not found» — он ищет цель Метрики с таким номером и, конечно, не находит. + * + * Замер 30.07.2026 на боевом кабинете, один и тот же ГОТОВЫЙ сегмент: + * ExternalId 58034825 → отказ 8800 «Object not found» + * ExternalId 2058034825 → условие создано, № 41678818 + * Приставку подсказал сам Яндекс: у уже существующего в кабинете условия на этот сегмент + * в `retargetinglists.get` стоит ровно `"ExternalId": 2058034825`. + * + * 🪤 Прежний тест закреплял поломку: он проверял, что уходит номер БЕЗ приставки. Тест, + * написанный по нашему представлению о правде, а не по ответу живой системы, охраняет баг. + */ +it('сегмент Аудиторий уходит с приставкой 20 — иначе Директ не находит объект', function () { + Http::fake(['*/json/v5/retargetinglists' => Http::response(['result' => ['AddResults' => [['Id' => 7]]]])]); + + $client = new YandexDirectClient('https://api.direct.yandex.com', 'T'); + + expect($client->addRetargetingList('Лидерра #6', 58034825, 30))->toBe(7); + + Http::assertSent(function ($req) { + $arg = $req->data()['params']['RetargetingLists'][0]['Rules'][0]['Arguments'][0]; + + expect($arg['ExternalId'])->toBe(2058034825); + + return true; + }); +}); diff --git a/docs/superpowers/2026-07-29-HANDOFF-telegram-sveden-i-proksi.md b/docs/superpowers/2026-07-29-HANDOFF-telegram-sveden-i-proksi.md new file mode 100644 index 00000000..e11be464 --- /dev/null +++ b/docs/superpowers/2026-07-29-HANDOFF-telegram-sveden-i-proksi.md @@ -0,0 +1,154 @@ +# Промт для следующей смены — телеграм сведён, дальше выкат и адрес для МТС + +Дата: 29.07.2026, вечер. Свод закрыт и запушен. Открытым остался адрес для кабинета МТС. + +--- + +Продолжаем Лидерру. Две линии: **выкат телеграм-модуля** и **адрес для кабинета МТС**. + +## Линия 1 — телеграм сведён с main. ГОТОВО + +**Коммит `77f61fb1`**, ветка **`feat/client-telegram-ads-on-main`**, **в gitea запушено**. +314 файлов, +42708 / −148, два родителя: телеграм `6f469fb1` + main `8bdd58e8`. main влит целиком. + +Рабочий каталог `.claude/worktrees/telegram-merge` **полностью снаряжён** — свои `composer install` +и `npm ci`, свои бинари в `bin/`, своя тестовая база **`liderra_testing_tgmerge`**. Не выбрасывать: +следующий каталог придётся снаряжать заново с нуля. + +🔴 Исходная ветка `feat/client-telegram-ads` и 292 черновика в главном каталоге **не тронуты**. + +### Числа, померенные на своде + +| Что | Результат | +|---|---| +| Телеграм | 193/193 | +| Реклама | 336/336 при 1029 проверках | +| Вместе | 532/532 | +| Экраны | 1704/1708, 3 пропущено | +| Сборка фронта | чисто, 3.4 сек | +| Полный Unit+Feature | **3923/3960** | + +🔑 **16 падений полного прогона совпадают построчно с прогоном ветки БЕЗ телеграма** — свод не +добавил ни одного. Одиннадцать из них — тесты робота креативов Яндекса, они ходят в **настоящий** +Яндекс живым запросом: подмена ответа накрывает не все адреса. Это хвост соседней ветки. + +### Что было починено по дороге + +- **Помощник `rejectedCampaign` переименован в `tgRejectedCampaign`.** Одноимённый есть у рекламы, + помощники Pest глобальные — полный прогон падал фаталом. 🔑 Каждая ветка по отдельности этого + увидеть не могла: такие мины вылезают **только при сведении**. +- **Два теста уведомлений считали ВСЮ таблицу** вместо строк своего пользователя. В одиночку + зелёные, в полном прогоне красные. +- **Убраны две проверки отменённой сущности** «своё имя отправителя» — она дропнута в v9.27. +- **Журнал схемы:** телеграмные v8.86–v8.95 → **v9.18–v9.27**, блок наверх, пометки + «предварительный» сняты. Версия `db/schema.sql` не тронута: там v8.85 и у main, и у телеграма. +- 🔴 **Авто-починка разметки испортила смысл в `ПИЛОТ.md`**: «**+** нумератор» переписала в + «**−** нумератор». Прод-документ. Откачено, переписано на «**и** нумератор». + 🪤 Авто-фиксы разметки просматривать глазами — «+» в начале русской строки линтер считает списком. + +### Сторож денег поставлен + +`app/tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php` — три проверки под **боевой ролью** +`crm_app_user`, а не суперюзером. С контекстом клиента деньги двигаются, без контекста возврат +падает **громко**. + +🔑 **И тут же вскрылось, на чём держится эта громкость:** база без контекста молча отдаёт ноль +строк, шумит только `firstOrFail()` в сервисе. Заменят на `first()` — возврат денег за отклонённую +модерацию станет тихим. Записано в самом тесте. + +🪤 Тестовая база собирается миграциями, а права ролям на бою раздаёт **отдельный скрипт** +`db/02_grants.sql`. Без него сторож мерил бы отсутствующий доступ вместо защиты по клиентам. + +### Что дальше по этой линии + +1. Решить с владельцем: вливать ли `feat/client-telegram-ads-on-main` в `main`. +2. Выкат можно делать **не дожидаясь адреса** — модуль поедет с `TG_SANDBOX=true`: только + черновик, деньги не двигаются. Робот МТС без адреса не заработает, но это **другая задача**. +3. 🔴 При выкате на кластер ПЕРЕзапустить `db/03_service_bypass_policies.sql` — новым RLS-таблицам + мало `tenant_isolation`, иначе служебные роли получат тихий ноль. +4. Хвост: статанализ исключался при коммите. Из 628 замечаний **623 в тестах** обеих сторон + Pest-шум. Если браться — сначала пересобрать baseline под слитое состояние. + +## Линия 2 — адрес для кабинета МТС. ЖДЁМ ПОСТАВЩИКА + +**Куплено сегодня:** резидентный российский адрес, 1 месяц, `91.221.70.204:24009`, логин `lte4`, +выход в интернет **95.24.162.70**, срок до **28.08.2026**, смена адреса ручная по ссылке. +Списано **$12.28**, баланс **$131.06**. + +🔴 **Не годится.** Замер с нашего сервера через этот прокси: + +| Куда | Что вышло | +|---|---| +| ya.ru | HTTP 302 за 0.28 сек | +| **marketolog.mts.ru** | **обрыв за 0.02 сек** | +| mts.ru | обрыв за 0.02 сек | +| beeline.ru | обрыв за 0.02 сек | + +Настоящий Chrome через него получает `ERR_TUNNEL_CONNECTION_FAILED`. Это **тот же запрет на сайты +операторов**, что на мобильном канале. Их слова «наш домен под ограничения не попадает» замером не +подтвердились. **Обходить не пытались и не надо** — это их сознательный запрет. + +🔴 **Отдельно: пропали $53.33.** Баланс сам съехал 196.67 → 143.34, в истории операций строки +**нет**, мобильный канал 9493547 **не продлён** — срок как был 04.08.2026 19:50. Сумма копейка в +копейку равна цене продления. + +**Обращение №31027** — открыто сегодня, в нём и замер, и вопрос про 53.33. Прежнее №30979 они +закрыли **молча, без единого ответа**. + +Проверять появление адреса: `POST https://api.dashboard.proxy.market/dev-api/list/{ключ}` с телом +`{"type":"ipv4"}`; ключ в `%TMP%\pm_api_key.txt`. Резидентные в этом списке **не показываются** — +их видно только в кабинете. Браузер под управлением: Edge на порту **9222**, профиль +`C:\liderra\pm-profile`, вход выполнен. + +🪤 В форме нового обращения кнопка называется **`Create`**, и на странице есть ещё одна — +**`Create the ticket`**, которая в DOM стоит РАНЬШЕ. Поиск по началу строки цепляет её и молча +пересоздаёт пустую форму. Искать по точному совпадению. +🪤 Поле ответа в списке обращений однострочное и режет длинный текст; в форме НОВОГО обращения +поле нормальное, 1700 знаков проходят. Вставка не работает — печатать клавиатурой. +🪤 Своё фоновое слежение перезагружает вкладку и стирает набранное — останавливать перед работой. + +**Если ответа не будет:** искать другого поставщика резидентных российских адресов без запрета на +сайты операторов. Похоже, у Proxy.Market это правило на весь дом, а не на конкретный продукт. + +## Роботы и сервер + +**`liderra-render`** — `51.250.1.97`, доступ `ssh -i ~/.ssh/liderra_deploy ubuntu@51.250.1.97`. +Стоят Chrome 150, Xvfb, privoxy:8118, playwright в `/opt/liderra-render`. Соседи — Asterisk, Caddy, +рендер 2ГИС; не мешать. + +**Робот Яндекса** от прокси не зависит. Для доселения нужны **от владельца**: логин рекламного +кабинета, «прихожая» — остановленная кампания и группа, разрешение прописать `CREATIVE_ROBOT_TOKEN` +на боевом. + +🔴 Голосовой сервер `185.182.110.115` **мёртв** — ни пинга, ни одного порта. Александра и Даша +лежат там. + +## 🪤🪤 Главная ловушка дня — про инструменты + +**В свежем git-worktree проверки не работают и при этом ВРУТ человеческим текстом.** Первый заход +коммита показал пять «замечаний»; три из пяти оказались упавшим инструментом, а не находкой: + +| Проверка | Настоящая причина | +|---|---| +| «cspell нашёл слова» | пакета `cspell` нет — падение при запуске | +| «gitleaks нашёл ПДн» | нет бинаря `bin/gitleaks.exe` | +| «lychee нашёл битые ссылки» | нет бинаря `bin/lychee.exe`, потом — два файла, которых нет в git | +| «larastan нашёл ошибки» | нет `app/_ide_helper_models.php`, phpstan молча выходит с ошибкой | + +🔑 **Датчик:** проверка жалуется содержательно, но ты не увидел ни одной конкретной строки — +значит смотришь на упавший инструмент, а не на находку. Ставить инструмент и перезапускать. + +**Рецепт снаряжения свежего каталога:** `composer install` + `npm ci` в `app/`, `npm ci` в корне, +скопировать `bin/gitleaks.exe` и `bin/lychee.exe`, при нужде — `app/_ide_helper_models.php`. +🪤 На Windows `vendor` и `node_modules` **нельзя подсунуть ссылкой** — нужен свой установ. +🪤 `npm ci --ignore-scripts` оставляет пакеты **обрезанными** — половина файлов отсутствует, а +ошибки нет. Ставить полностью. +🪤 Настоящие находки тоже были: разрешение gitleaks по **номеру коммита** не переживает сведение +ветки — тот же текст приходит новым коммитом, нужен путь. + +## Красные линии + +Прод — живые клиенты и деньги, выкат только по явному «go», БД по умолчанию только чтение. +**Пуш только в gitea**, GitHub мёртв. Коммит по эскейпу, сообщение без круглых скобок, в файле, +явные пути. **В СМС-модуль не лезть.** Кампанию № 713051718 не трогать. Владелец не программист — +говорить простым русским. diff --git a/docs/superpowers/2026-07-29-HANDOFF-yandex-robot-v-rabotu.md b/docs/superpowers/2026-07-29-HANDOFF-yandex-robot-v-rabotu.md new file mode 100644 index 00000000..c51dac42 --- /dev/null +++ b/docs/superpowers/2026-07-29-HANDOFF-yandex-robot-v-rabotu.md @@ -0,0 +1,123 @@ +# Промт для следующей смены — запустить рекламу в Яндексе в работу + +Дата: 29.07.2026, вечер. Всё ниже **проверено живой командой сегодня**, а не по памяти. + +--- + +Задача одна: **довести рекламу в Яндексе до работающего состояния на боевом liderra.ru.** +Портал умеет всё, кроме одного шага — картиночный креатив программно не создать (приём +`creatives.add` берёт только видео, конструктор Яндекс закрыл 01.06.2026). Этот шаг делает +робот-грузчик в браузере. Робота написали, живьём он ни разу не работал. + +## Что уже живо на боевом + +- **Загрузка аудиторий в Яндекс работает** — ключ `YANDEX_AUDIENCE_TOKEN` прописан, + в `sales_ad_audience_platforms` 3 записи. +- Экран «Яндекс Аудитория» открыт клиентам с 25.07. +- Кампаний в базе **2, обе черновики** (клиент 2 от 25.07, клиент 10 от 29.07 05:42). + +## Что мертво (и почему) + +| Что | Состояние на 29.07 | Что это значит | +|---|---|---| +| `YANDEX_DIRECT_ENABLED` | **`false`** | канал Директа закрыт флагом | +| `YANDEX_DIRECT_TOKEN` | прописан | ключ есть, ждёт включения | +| `CREATIVE_ROBOT_TOKEN` | **отсутствует** | дверь робота в портал закрыта наглухо | +| `ad_creative_jobs` | **0 строк** | робот не отрабатывал ни разу | +| Каталог `bots/` на боевом | **отсутствует** | 🔴 выкат копирует только `app/`, роботы лежат рядом в `bots/` — они на прод НЕ приезжают | +| Робот на рендер-виртуалке | **отсутствует** | там только служба снимков сайтов | + +## Три вещи, без которых не начинать — они за владельцем + +1. **Логин рекламного кабинета Яндекса**, под которым будет работать робот. Идёт в адресах + Директа параметром `ulogin`. +2. **«Прихожая»** — заранее заведённые в кабинете **остановленная** кампания и группа + (`YC_ANTEROOM_CAMPAIGN_ID`, `YC_ANTEROOM_GROUP_ID`). Робот открывает форму нового + объявления в них только чтобы дойти до окна «Загрузить креативы», ничего не сохраняет. + 🔑 Прихожая обязана быть отдельной остановленной кампанией — тогда робот не приближается + к работающей рекламе вовсе. +3. **Разрешение** прописать `CREATIVE_ROBOT_TOKEN` на боевом и включить + `YANDEX_DIRECT_ENABLED=true`. + +Плюс от владельца понадобятся **глаза и телефон один раз** — вход в кабинет заводится +руками (пароль + код из СМС) и дальше живёт в папке профиля браузера. + +## Что такое `CREATIVE_ROBOT_TOKEN` (владелец спрашивал) + +Это **пароль на служебную дверь «робот → портал»**, к Яндексу отношения не имеет, никто +его не выдаёт — придумываем сами длинной случайной строкой. Робот стучится в портал по +трём адресам (`/api/creative-robot/next`, `.../jobs/{id}/banners/{id}/file`, +`.../jobs/{id}/done` + `/inspection`) и показывает заголовок `X-Creative-Robot-Token`. +Проверка — `app/app/Http/Middleware/CreativeRobotToken.php`. Пароль пишется в двух местах: +боевой `.env` портала и `.env` робота. Пустой в настройках = дверь закрыта (401) — сейчас +именно так, и это нарочно. + +## Где робот жить будет — рекомендация + +**Рендер-виртуалка `liderra-render` 51.250.1.97**, а не боевой сервер: браузером боевой +нагружать не надо. Проверено сегодня, что там есть: + +- `Xvfb` — есть; `node v20.20.2`; `google-chrome` системный; playwright-браузер + `chromium-1228` в `~/.cache/ms-playwright`; +- диск 15 ГБ свободно, память 7.9 ГБ (занято 0.4); +- служба-сосед `liderra-render.service` — **не мешать**, там же Asterisk и Caddy. + +🔴 **VNC/RDP на виртуалке НЕТ** (`x11vnc`/`xrdp` не установлены). Для разового входа глазами +их надо поставить и пробросить портом через SSH-туннель — наружу порт не открывать. + +Доступ: `ssh -i ~/.ssh/liderra_deploy ubuntu@51.250.1.97`. + +## Порядок работ + +1. **Спросить у владельца три вещи выше.** Без прихожей и логина дальше идти нельзя. +2. Прочитать `bots/yandex-creatives/README.md` и `docs/cabinet-flow.md` (разметка экранов + кабинета, снята живьём) — там же список писем-алярмов и что делать по каждому. +3. Разложить робота на рендер-виртуалку: скопировать `bots/yandex-creatives`, свой + `npm ci` (playwright + nodemailer + dotenv), заполнить `.env` из `.env.example`. +4. Поставить `x11vnc`, поднять `Xvfb`, пробросить порт туннелем, позвать владельца — + `npm run login`. Профиль ложится в `YC_BROWSER_PROFILE_DIR`. +5. Прописать `CREATIVE_ROBOT_TOKEN` в боевой `.env` портала (тот же, что у робота) + + `php artisan config:cache` **от www-data** (🪤 квирк 107: под root ломает прод). +6. Проверить дверь: запрос к `/api/creative-robot/next` без заголовка → 401, с верным + заголовком → осмысленный ответ. +7. Включить `YANDEX_DIRECT_ENABLED=true`. **Это открывает деньги и показы** — только по + явному «да» владельца. +8. Расписание на виртуалке: `run:once` часто, `keepalive` раз в ~15 минут. Наложиться они + не могут — общий файл-замок `robot.lock`, занят → выход с кодом 0. +9. **Живая приёмка на одной кампании**: завести кампанию с баннерами, дождаться задания, + посмотреть, что робот залил креативы, портал сверил список до/после и кампания поехала. + Смотреть глазами в кабинете, а не только в журнал. + +## Ловушки, известные заранее + +🪤 **Выкат портала не везёт `bots/`.** Каталог лежит в корне репозитория, а rsync идёт из +`app/`. Робота на любую машину класть отдельно, руками. + +🪤 **11 падений полного прогона тестов — это тесты робота креативов**, они ходят в +**настоящий** Яндекс живым запросом: подмена ответа накрывает не все адреса. Падают +одинаково и с телеграмом, и без него. Не пугаться и не «чинить» вслепую. + +🪤 **Робот не нажимает «Сохранить изменения»** — единственную кнопку, которая меняет +кабинет. На это есть тест-сторож. Если появится соблазн «дожать» — сначала подумать, зачем +сторож ставили. + +🪤 **Снимки экрана робота показывают логин и остаток счёта** в боковой панели кабинета. +Робот сам чистит старше двух недель; в переписку и в git их не тащить. + +🪤 **Повторная заливка оставляет дубли креативов**, вычищать можно только руками. Если +письмо-алярм кончается словами «Креативы в кабинет уже загружены» — сначала посмотреть +кабинет, потом решать про перезапуск. + +## Красные линии + +Прод — живые клиенты и деньги, любое включение только по явному «да» владельца, БД по +умолчанию только чтение. **Пуш только в gitea**, GitHub мёртв. Коммит по эскейпу, сообщение +без круглых скобок, в файле, явные пути. **В СМС-модуль не лезть.** Телеграм-кампанию +№ 713051718 не трогать. Владелец не программист — говорить простым русским. + +## Сегодняшний фон (не мешает, но знать) + +- Телеграм-модуль выкачен на боевой 29.07 в песочнице (`TG_SANDBOX=true`), деньги не + двигаются. Подробности — `2026-07-29-HANDOFF-telegram-sveden-i-proksi.md`. +- Адрес для кабинета МТС не найден, обращение к Proxy.Market №31027 открыто. **К Яндексу + отношения не имеет** — робот Яндекса от прокси не зависит. diff --git a/docs/superpowers/2026-07-30-HANDOFF-yandex-reklama-posle-zhivoy-priemki.md b/docs/superpowers/2026-07-30-HANDOFF-yandex-reklama-posle-zhivoy-priemki.md new file mode 100644 index 00000000..21ba9c60 --- /dev/null +++ b/docs/superpowers/2026-07-30-HANDOFF-yandex-reklama-posle-zhivoy-priemki.md @@ -0,0 +1,210 @@ +# Промт для следующей смены — реклама Яндекса после живой приёмки + +Дата: 30.07.2026, утро. Всё ниже **проверено живой командой**, а не по памяти. +Предыдущий промт — `2026-07-29-HANDOFF-yandex-robot-v-rabotu.md` (он уже исполнен). + +> 🔴 **Правка от 30.07 09:00 МСК.** Первая версия этого промта содержала **неверный диагноз**: +> будто Директ отказывает из-за порога «не меньше 1000 опознанных людей» в сегменте. +> Это оказалось неправдой, и вся ветка «нужен список побольше» была ложной. Настоящих причин +> было **две**, обе найдены и одна из них — наша ошибка в коде. Текст ниже исправлен. + +--- + +Задача одна: **довести до конца последний шаг рекламы в Яндексе и убрать за приёмкой.** +Связка «портал → робот → кабинет Яндекса → опознание креативов → условие ретаргетинга +в Директе» **работает и проверена живьём по частям**. Не сделан последний стык: кампания +№ 6 ещё не заведена в Директе, и упирается это теперь **только в сторону Яндекса**. + +## Что живо и проверено настоящим прогоном 29–30.07 + +| Что | Состояние | +|---|---| +| Робот на рендер-виртуалке `51.250.1.97`, `/opt/liderra-creative-robot` | стоит, **84 из 84** проверок зелёные | +| Вход в кабинет `sasha261185` | заведён глазами владельца, живёт в профиле, `keepalive` подтверждает | +| Расписание робота | `…-run.timer` каждые 2 мин, `…-keepalive.timer` каждые 15 мин, оба **включены** | +| Дверь «робот → портал» | без пароля 401, с верным 200 | +| Заливка картинок роботом | **15 из 15**, задание закрыто как `done` | +| Опознание креативов порталом | **15 из 15** номеров проставлены баннерам | +| **Условие ретаргетинга в Директе кодом портала** | **создано живьём** (№ 41678854 на готовом сегменте, сразу убрано) | +| Разведка причины отказа | читает обе причины целиком, 5–6 с на объявление | + +Доступ к виртуалке робота: `ssh -i ~/.ssh/liderra_deploy ubuntu@51.250.1.97` (отдельной строки +в `~/.ssh/config` нет). Боевой — `ssh liderra-prod` (через бастион gitea). + +## Кампания-подопытная + +- Кампания **№ 6**, фирма **2** (`info@lkomega.ru`), имя «Приёмка робота — займ под залог». +- Статус **черновик**, режим ручной, свой список номеров. +- **1693 номера** — графа Д из `C:\Users\Administrator\Downloads\1785082209_report_606058_380216.xlsx` + (выгрузка лидов, личные мобильные). 🔴 Это ПДн: в git не тащить, в переписку не выводить. +- Сегмент Аудиторий **58283031**, файл переварен целиком, 1693 из 1693 годных. +- Смета 1693 показа, клиенту ≈ **203,16 ₽**. В рекламном кошельке фирмы **300 ₽**, + заморожено 0, не списано ничего. +- 15 баннеров нарисованы Claude: «ЗАЙМ ПОД ЗАЛОГ АВТО» + обязательное предупреждение про + условия займа на 22 % высоты. 🪤 На 320×50 надпись 4 пункта — площадь требованию отвечает, + но модерация может придраться к читаемости. +- Все выгрузки лидов владельца вместе дают всего **1873** личных мобильных (замер 30.07): + больше номеров из этого источника взять негде, и **этого больше не требуется** — см. ниже. + +## Почему кампания не заводилась — две причины, не одна + +Директ отвечал **8800 «Object not found»**. Долго считалось, что дело в размере аудитории. +**Это неверно.** Разбор 30.07 дал две независимые причины: + +**Причина 1 — наша ошибка в коде (исправлена, проверена живьём).** +Сегмент Аудиторий указывается в условии ретаргетинга **не своим номером, а с приставкой +«20»**: сегмент `58034825` → `ExternalId 2058034825`. Мы посылали номер без приставки, Директ +искал цель Метрики с таким номером и честно отвечал «объект не найден» — тем же отказом, что +и на несуществующий сегмент. Отсюда и путаница. + +Замер на одном и том же **готовом** сегменте: + +| Что послали | Ответ живого Директа | +|---|---| +| `ExternalId 58034825` (как было) | отказ 8800 «Object not found» | +| `ExternalId 2058034825` (с приставкой) | **условие создано**, № 41678818 | + +Приставку показал **сам Яндекс**: у уже существующего в кабинете условия `retargetinglists.get` +возвращает ровно `"ExternalId": 2058034825`. Документация Директа про приставки говорит +глухо (10/20/30 — «для интересов»), живой ответ кабинета оказался надёжнее. + +**Причина 2 — Яндекс ещё не сопоставил наши номера (ждём, ускорить нельзя).** +У свежего сегмента поля `matched_quantity` / `matched_percentage` **пустые**, статус +`is_processed`, в кабинете «Обрабатывается». Пока сопоставления нет, Директ сегмента не видит +даже с правильной приставкой. + +🪤 **Ловушка на будущее:** `is_processed` — это НЕ «готов». У готового сегмента статус +`processed` (без `is_`), заполнены `matched_quantity` и `cookies_matched_quantity`, +и стоит `can_create_dependent: true`. Судить о готовности **только** по `can_create_dependent`. + +**Порога в 1000 человек не существует.** Живое доказательство: сегмент 58034825 годится +для Директа при **134 опознанных людях** из 151 номера. Прежнее утверждение про «минимум +1000» — выдумка, построенная на статьях, а не на замере. Сюда же: **никакого дефекта +«портал пишет 100, а надо 1000» нет** — минимум портала трогать не нужно. + +## Что сейчас делает сторож — и что делать смене + +Служба `liderra-priemka-storozh` (скрипт `/usr/local/bin/storozh.sh`) переписана 30.07: +раз в 10 минут пишет в журнал **живые цифры** (статус, сколько людей опознано, охват), +ждёт до **48 часов** и при `can_create_dependent: ДА` сам запускает кампанию через +`/tmp/zapusk.php 6`. + +**Первое действие смены — `sudo tail -20 /var/log/liderra-priemka-storozh.log`.** Три ветки: + +1. Строка «КАМПАНИЯ ПОЕХАЛА» — связка доказана целиком. Дальше: посмотреть кабинет глазами, + дождаться вердикта модерации, проверить окно передачи отказов на живом отказе. +2. В журнале появились цифры опознанных, но `годен нет` — Яндекс считает, ждать. +3. 48 часов прошло, цифры так и не появились — **это уже вопрос к Яндексу**, а не к коду. + Написать в поддержку Аудиторий (сегмент 58283031 висит непосопоставленным больше двух + суток при 100 % годных строк) и **спросить владельца**, ждать или брать другой список. + +🪤 Сторож стоит **не в git** — только на боевом. Прежняя версия ждала 12 часов и писала +глухое «нет»; она бы дождалась готовности и всё равно упала на приставке. Это тот же класс: +**сторож, который «проверяет», может не проверять того, что нужно.** + +## 🔴 Главное, что нельзя забыть + +**Починки на боевом лежат руками, а не из хранилища.** Прямо на прод подложены: + +- `app/Services/Advertising/YandexDirectClient.php` (в нём же приставка «20») +- `app/Services/Advertising/CampaignLauncher.php` +- на виртуалке `src/cabinet.js`, `bin/run.js`, `bin/keepalive.js` + +В хранилище: ветка **`fix/robot-yandex-zamok`** (коммиты `61ac314d`, `f2bfe3f1` отправлены +в gitea; правка приставки — **ещё не закоммичена на момент написания**, спросить эскейп). +В main **не влито**. 🪤 Любой обычный выкат из main вернёт все семь дыр обратно. +Первое дело — **предложить владельцу влить ветку в main**. + +## Что чинили — чтобы не «починить» обратно + +1. Слепок креативов просил кабинет пустым **массивом** — живой Яндекс отказывает 8000. + Нужен пустой **объект**. +2. Отказы Яндекса **по позиции внутри ответа** глотались молча, портал падал «нет ключа Id». + Теперь слова Яндекса выходят наружу — именно это позволило найти следующий пункт. +3. В условии ретаргетинга не было `MembershipLifeSpan`. Границы Яндекса 1..540; **ноль + нельзя** при создании, хотя в старых условиях кабинета нули стоят. +4. Робот не нажимал «Добавить выбранные». «Набор» в кабинете и «креатив» в `creatives.get` — + разные вещи: без добавления в черновик формы Яндекс креативы не регистрирует. Замер + 18 → 32 в ту же секунду. Объявление при этом НЕ сохраняется. +5. Гонка: «Создать» открывается на 4-й секунде при 11 принятых файлах из 15, остальные + доезжают к 7-й. Три прогона дали 14 из 15, каждый раз пропадала другая картинка. +6. Замок робота не снимался — `process.exit()` не даёт выполниться `finally`. +7. **Приставка «20» у номера сегмента** (30.07) — см. выше. Тестов на неё было два, и **оба + закрепляли поломку**: проверяли, что уходит номер БЕЗ приставки. + +🪤 **Урок из пункта 7:** тест, написанный по нашему представлению о правде, а не по ответу +живой системы, **охраняет баг** и делает его невидимым. Оба теста были зелёные всё время. + +## Незакрытый вопрос продукта — решать владельцу + +Порога в 1000 нет, но **задержка сопоставления есть**, и клиент её увидит: он загружает +номера, жмёт «Запустить» и получает непонятную ошибку, потому что Яндекс ещё считает. +Правильно — не пускать в запуск, пока сегмент не стал годным, и писать по-человечески: +«Яндекс ищет ваших клиентов в сети, обычно это занимает несколько часов». Это **меняет +поведение продукта** (появляется ожидание вместо мгновенного запуска), поэтому Claude +сам не менял. Спросить. + +## Уборка, которую Claude намеренно не делал + +- 🔴 **В кабинете Яндекса ~57 лишних креативов** — четыре набора от прогонов. Все называются + «Новый креатив», отличить от настоящих нельзя, ошибка стоила бы боевых креативов владельца. + Удалять только владельцу, кнопкой «Удалить набор». +- 🔴 **Четыре лишних сегмента** в Аудиториях, все с именем «Лидерра кампания #6»: + 58275925 (503), 58275978 (503), 58276009 (503), 58282999 (2025). + **НЕ удалять:** 58283031 (1693 — рабочий) и 58034825 («Лидерра — кандидаты», в работе). +- **300 ₽ на рекламном кошельке фирмы 2** — положены Claude для приёмки, не потрачены. + +## Настройки боевого, изменённые 29–30.07 — и как откатить + +| Строка в `.env` | Стало | Откат | +|---|---|---| +| `YANDEX_DIRECT_ENABLED` | `true` | `false` | +| `YANDEX_DIRECT_BASE_URL` | `https://api.direct.yandex.com` | убрать строку = вернётся песочница | +| `CREATIVE_ROBOT_TOKEN` | выпущен | убрать строку = дверь робота закрыта | + +Копии прежнего файла рядом: `.env.bak-2026-07-29-robot`, `.env.bak-2026-07-29-direct-off`, +`.env.bak-2026-07-29-sandbox`. Прежняя версия кода с приставкой — рядом с файлом: +`YandexDirectClient.php.bak-2026-07-30-bez-pristavki` (восстановлена из коммита `f2bfe3f1`, +🪤 первый бэкап затёрся новой версией — проверять бэкап, а не верить ему). +После любой правки `.env` — `config:clear` + `config:cache` **от www-data** +(🪤 квирк 107: под root ломает прод) и `reload php8.3-fpm`. + +## Ловушки, добытые кровью + +🪤 **Выкат портала не везёт `bots/`** — робота на любую машину класть отдельно, руками. + +🪤 **`php artisan tinker <файл>` открывает интерактив и вешает ssh.** Работает только +`--execute`, а для длинного — отдельный php-скрипт с ручным `bootstrap()` в `/tmp`. + +🪤 **`app(YandexDirectClient::class)` не собирается** — у него в конструкторе адрес и токен +строками. Создавать через `new` с `config(...)`. + +🪤 **`pkill -f имя-скрипта` через ssh убивает сам себя** — в командной строке удалённого +bash встречается то же имя. Гасить по номеру процесса. + +🪤 **Список кампаний/лидов через `tinker` приходит ПУСТЫМ** — это защита по фирмам, а не +правда о базе. Читать через `DB::connection('pgsql_admin')`, писать — в транзакции +с `SET LOCAL app.current_tenant_id`. + +🪤 **У клиентских пользователей нет токенов** (`createToken` не существует) — кабинет на +сессии. Ходить настоящими контроллерами с подстановкой пользователя, обернув как middleware. + +🪤 **Браузер робота и расписание не делят профиль** — перед ручным заходом в кабинет +останавливать оба таймера, после — возвращать. Иначе робот пишет «уже работает» и уходит. + +🪤 **Скрипт `.ps1` с кириллицей надо сохранять с меткой UTF-8**, иначе Windows PowerShell +читает его как абракадабру и падает на разборе. + +🪤 **Сторож, который «проверяет», может не проверять ничего.** Дважды за две смены: сначала +проверка гонки считала размеры в окне и не заметила постоянного фильтра из тех же размеров; +потом сторож приёмки ждал готовности сегмента, но запустил бы кампанию в сломанный запрос. +🔑 **Всегда искать независимый признак, что сторож правда сработал** — и спрашивать +не «сработал ли признак», а «что именно он доказывает». + +## Красные линии + +Прод — живые клиенты и деньги, любое включение только по явному «да» владельца, БД по +умолчанию только чтение. **Пуш только в gitea**, GitHub мёртв. Коммит по эскейпу, сообщение +без круглых скобок, в файле, явные пути. **В СМС-модуль не лезть.** Телеграм-кампанию +№ 713051718 не трогать. Владелец **не программист** — говорить простым русским, без +технических слов; длинные разборы он не читает, нужны короткие абзацы по существу.