From 6bc00ba67bcfa1f67d20db52b59d5ad28e7f1b38 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: Tue, 28 Jul 2026 10:19:40 +0300 Subject: [PATCH] =?UTF-8?q?docs=20=D1=80=D0=B5=D0=BA=D0=BB=D0=B0=D0=BC?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7=D1=8B:?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=85=D0=BE=D0=B4=202=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=20=E2=80=94=20=D0=BA=D1=83=D1=81=D0=BE=D0=BA?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2,=20=D1=85=D0=BE=D0=B4=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=20=D0=B8=20=D1=81=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D0=BA=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-27-PROGRESS-pochinka-v12.md | 74 ++++++++++++++++ .../2026-07-28-STATE-okno-peredachi.md | 87 +++++++++++-------- 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/docs/superpowers/2026-07-27-PROGRESS-pochinka-v12.md b/docs/superpowers/2026-07-27-PROGRESS-pochinka-v12.md index 916a7e63..dd2cc6de 100644 --- a/docs/superpowers/2026-07-27-PROGRESS-pochinka-v12.md +++ b/docs/superpowers/2026-07-27-PROGRESS-pochinka-v12.md @@ -606,3 +606,77 @@ v9.07. Чинить не в этой ветке. догоняющая миграция с `GRANT SELECT` по образцу `2026_07_27_100100_grant_admin_ad_campaign_banners.php`. 🪤 Без неё админский экран увидит ноль строк молча, без единой ошибки. Записано в CHANGELOG v9.10. + +--- + +# Заход 2 закрыт 28.07.2026 — кусок 1 «переписка» готов целиком + +Клиент теперь видит, за что Яндекс отклонил его рекламу, и отвечает прямо в портале. + +## Что сделано, задача за задачей + +**Задача 5 — ручка списка сообщений** (`5e5a9c7f`). `GET /api/advertising/campaigns/{id}/messages` +отдаёт ленту своей кампании по порядку. Чужая — 404, без входа — 401. +🪤 Красный был правильный не сразу очевидно: на незнакомый адрес отвечает страница сайта +с кодом 200, поэтому в тестах проверяется содержимое, а не голый `assertOk()`. + +**Задача 6 — ответ клиента с документом** (`27367245`). `POST .../messages` принимает +текст и файл (pdf/jpg/png, до 10 МБ) на закрытый диск; `GET .../messages/{id}/file` +отдаёт файл только своему тенанту. + +**Задача 7 — экран переписки** (`31709d01`). `CampaignMessages.vue` встал в карточку +кампании **вместо мёртвого списка объявлений**: он читал `ad_campaign_ads` — таблицу +старой модели, которую поток «за показы» не заполняет, поэтому клиент всегда видел +«Объявлений пока нет» рядом с красным ярлыком. + +**Задача 8 — подпись под ярлыком** (`958204d2`). `index()` кампаний отдаёт +`moderation_reason`, в списке под чипом «Отклонено» видна первая строка причины. + +**Добор** (`fb63994a`). Отдельный тест на строку приёмочного листа «в переписке нет +наценки»: ручка отдаёт строго перечисленные поля. + +## Что проверено вырезанием + +- Фильтр по тенанту в списке ленты → чужой клиент получил 200 вместо 404. ✅ +- Правила `mimes` и `max:10240` → покраснели оба файловых теста. ✅ +- Условие «пусто и без файла» → покраснел свой тест. ✅ +- Фильтр по тенанту в отдаче файла → покраснел свой тест. ✅ +- Перехват упавшей загрузки ленты → покраснел «упавшая загрузка не роняет экран». ✅ +- Подпись под ярлыком → покраснел «видна первая строка причины». ✅ +- Отдача модели целиком вместо перечня полей → покраснел «в ленте нет наценки». ✅ + +## Три находки, которых не было в плане + +1. 🪤 **План забыл `ensureCsrfCookie()`** в отправке ответа. Все прочие POST'ы модуля + её берут; без неё кнопка «Отправить» молча ломалась бы в браузере при зелёных тестах. +2. 🪤 **Фильтр по тенанту на самом сообщении ничего не сторожит** — его вырезание + оставило всё зелёным, потому что настоящий замок стоит на поиске кампании. Оставлен + вторым ремнём, но доказуемая защита там одна. +3. 🪤 **Диалог отчёта падал, если лента не загрузилась** — поймали существующие тесты + фронта. Теперь упавшая лента пишет строчку и не роняет остальной отчёт. + Заодно вскрылось: наивная отдача модели целиком утекла бы `file_path` — путь файла + на боевом сервере. + +## Приёмочный лист замысла §8, кусок 1 — закрыт целиком + +| Строка | Чем закрыта | +|---|---| +| Пояснение Яндекса появляется в переписке само | `SyncCampaignModerationJobTest` (заход 1) | +| Текст сохраняется целиком, длиннее 255 | `CampaignMessageServiceTest` | +| Повторный опрос второго сообщения не создаёт | `CampaignMessageServiceTest` | +| Клиент видит свою переписку и не видит чужую | `CampaignMessageEndpointsTest` | +| Ответ с файлом, файл на закрытом диске | `CampaignMessageEndpointsTest` | +| Файл отдаётся только своему тенанту | `CampaignMessageEndpointsTest` | +| Не тот тип или тяжелее 10 МБ — 422, на диск ничего | `CampaignMessageEndpointsTest` | +| Письмо и колокольчик на слова Яндекса | `CampaignMessageNotifyTest` (заход 1) | +| Ярлык «Отклонено» показывает первую строку причины | `advertising-campaign-list.spec.ts` | +| В переписке нет наценки | `CampaignMessageEndpointsTest` | + +## Зелёная отметка на конец захода 2, коммит `fb63994a` + +- портал: **322/322, 1090 проверок, ~54 с** (было 311/311 — 11 новых тестов); +- фронт: **227 файлов, 1668 тестов** зелёные (единственная посторонняя ошибка — + в чужом `autopodbor`, к этой работе отношения не имеет); +- `npm run type-check` — те же **6 чужих** ошибок, ни одной новой; +- робот: **60/60**; +- вызовов снятия заморозки — **четыре**, пятого не появилось. diff --git a/docs/superpowers/2026-07-28-STATE-okno-peredachi.md b/docs/superpowers/2026-07-28-STATE-okno-peredachi.md index 9532ea3c..797fe0ff 100644 --- a/docs/superpowers/2026-07-28-STATE-okno-peredachi.md +++ b/docs/superpowers/2026-07-28-STATE-okno-peredachi.md @@ -3,27 +3,28 @@ Файл пишется в конце каждого захода. Читать его **первым делом** после компакта: всё, что нужно следующей сессии, лежит здесь, а не в переписке. -Обновлено: 28.07.2026, конец захода 1. +Обновлено: 28.07.2026, конец захода 2. --- ## Где мы - Worktree `.claude/worktrees/reklama-pokazy`, ветка `feat/reklama-yandex-pokazy`. -- Последний коммит — **`4ddfa7b0`**. -- **Заход 1 закрыт целиком** (задачи 1–4). Заходы 2, 3, 4 не начинались. +- Последний коммит — **`fb63994a`**. +- **Заходы 1 и 2 закрыты целиком** (задачи 1–8). **Кусок 1 замысла «переписка» готов.** + Заходы 3 и 4 не начинались. - Никуда не пушили, на боевой не выкатывали, рубильник Директа выключен, в живой кабинет Яндекса никто не заходил. -Коммиты захода 1: +Коммиты захода 2: ```text -4ddfa7b0 feat реклама за показы: письмо клиенту и колокольчик на ответ Яндекса -70b421e1 feat реклама за показы: джоб модерации кладёт пояснение Яндекса в ленту кампании целиком -83753fc0 feat реклама за показы: сервис ленты сообщений и защита от повторов одной и той же причины -9c332cc2 feat реклама за показы: лента сообщений по кампании — таблица и модель -bae6b95f docs реклама за показы: план работ по отказам модерации — четыре захода с точками компакта -bb39703e docs реклама за показы: замысел — окно передачи между Яндексом и клиентом по отказам модерации +fb63994a test реклама за показы: в ленте не может быть наценки и пути файла на сервере +958204d2 feat реклама за показы: под ярлыком Отклонено видна первая строка причины +31709d01 feat реклама за показы: экран переписки в карточке кампании вместо мёртвого списка объявлений +27367245 feat реклама за показы: ответ клиента с документом и отдача файла только своему тенанту +5e5a9c7f feat реклама за показы: ручка списка сообщений кампании — только своя лента +ffeffe35 docs реклама за показы: заход 1 закрыт — ход работ и снимок состояния ``` Не закоммичено — только три устаревших промта (`chast4-v3`, `chast4-v4`, @@ -31,7 +32,11 @@ bb39703e docs реклама за показы: замысел — окно пе ## Зелёные числа на этом коммите -- портал: **311/311, 1054 проверки, ~51 с** — `cd app && DB_DATABASE=liderra_testing_reklama php artisan test --filter=Advertising` +- портал: **322/322, 1090 проверок, ~54 с** — `cd app && DB_DATABASE=liderra_testing_reklama php artisan test --filter=Advertising` +- фронт: **227 файлов, 1668 тестов** — `cd app && npx vitest run` + 🪤 единственная посторонняя ошибка в выводе — `autopodbor.ts::fetchProposalGroups`, + чужая и давняя, к этой работе отношения не имеет. +- `npm run type-check` — **6 чужих** ошибок, ни одной новой. Чужие не чинить. - робот: **60/60** — `cd bots/yandex-creatives && node --test` - мест снятия заморозки денег — **четыре**: `AdvertisingCampaignController:392`, `SyncCampaignModerationJob:165`, `PauseCampaignsOnAdStop:48`, `CampaignImpressionCharger:96`. @@ -39,45 +44,57 @@ bb39703e docs реклама за показы: замысел — окно пе ## Что теперь работает -Яндекс отклонил объявление → джоб опроса модерации кладёт пояснение модератора **целиком** -в ленту кампании (`ad_campaign_messages`) → клиенту уходит письмо и загорается колокольчик. -Повторный обход с тем же текстом второго сообщения не создаёт. Обрезанное до 255 знаков -по-прежнему живёт в `ad_campaign_banners.moderation_reason` — оно для ярлыка. +Круг «Яндекс → клиент → Яндекс» замкнут в одну сторону целиком: -**Клиент ленту пока не видит:** экранов и ручек ещё нет, это заход 2. +1. Яндекс отклоняет объявление; +2. джоб опроса модерации кладёт пояснение модератора **целиком** в `ad_campaign_messages`; +3. клиенту уходит письмо и загорается колокольчик; +4. клиент открывает карточку кампании и **видит переписку**, а в списке кампаний под + красным ярлыком «Отклонено» — первую строку причины; +5. клиент отвечает текстом и может приложить документ (pdf/jpg/png до 10 МБ); +6. документ лежит на закрытом диске и отдаётся только своему тенанту. -## Что дальше — заход 2, задачи 5–8 +**Чего пока нет:** документ никуда не едет — робот его не носит (кусок 3), а кнопка +«Исправить» по-прежнему ведёт в тупик (кусок 2, заход 3). + +## Что дальше — заход 3, задачи 9–12 По плану `docs/superpowers/plans/2026-07-28-yandex-otkazy-okno-peredachi.md`: -- **Задача 5** — ручка списка сообщений `GET /api/advertising/campaigns/{id}/messages`. -- **Задача 6** — ответ клиента с файлом: `pdf/jpg/png`, до 10 МБ, закрытый диск. -- **Задача 7** — отдача файла только своему тенанту. -- **Задача 8** — экраны: лента `CampaignMessages.vue` в карточке кампании и подпись - под ярлыком «Отклонено» в списке кампаний. - -К концу захода 2 кусок 1 замысла закрыт целиком. +- **Задача 9** — `YandexDirectClient::deleteAds()`, удаление отклонённых объявлений. +- **Задача 10** — `CampaignReviveService`: возврат кампании в черновик, обнуление номеров + креатива и объявления у отклонённых баннеров; **номера кампании, группы и сегмента + уцелевают** — на них держится возобновляемый запуск. +- **Задача 11** — ручка «Исправить» и **узкое** исключение в замке правки: по статусу + `rejected`, а не по «есть ли номер кампании», иначе исключение откроет правку + и работающей рекламе. +- **Задача 12** — кнопка «Исправить» на экране. 🔴 **Заход 4 не начинать, пока не снята живая разметка кабинета** (задача 13, вместе с владельцем). Код по воображаемым экранам даст зелёные тесты и мёртвую программу. -## Свежие грабли этого захода +## Свежие грабли захода 2 -- 🪤 **Тест-пустышка про «беду с лентой».** Первая редакция брала одну принятую кампанию - и проходила одинаково с защитой и без неё. Настоящая беда — обрыв разбора кампании - на первом баннере: остальные остаются без вердикта, деньги молча не возвращаются. - Урок прежний: **защиту проверять вырезанием, а не глазами** — иначе тест зелёный, - а защиты нет. -- 🪤 **`crm_admin_user` не получил гранта** на `ad_campaign_messages`. Админского экрана - по ленте нет, поэтому по least privilege грант не выдан. Когда появится список - «ждёт разбора» — нужна догоняющая миграция `GRANT SELECT` по образцу +- 🪤 **План забыл `ensureCsrfCookie()`** в отправке ответа клиента. Все прочие POST'ы + модуля её берут; без неё кнопка «Отправить» молча ломалась бы в браузере при зелёных + тестах. Проверять это на каждой новой мутации фронта. +- 🪤 **Фильтр по тенанту на самом сообщении ничего не сторожит:** его вырезание оставило + всё зелёным, потому что настоящий замок — на поиске кампании. Урок общий: вырезание + показывает не только «защита есть», но и «эта защита ничего не держит». +- 🪤 **Мутация фронта ломает чужие тесты не там, где ждёшь.** Встроенная в диалог отчёта + лента уронила два чужих spec'а: их `vi.mock` не знал новых функций api-модуля. + Диалог теперь переживает упавшую ленту, а специи получили заглушки. +- 🪤 **Наивная отдача модели целиком утекает `file_path`** — путь файла на боевом + сервере. Ручка обязана перечислять поля вручную; тест это ловит. +- 🪤 **`\n` внутри heredoc-питона превращается в настоящий перенос строки** и ломает + `.vue` разбором «Unterminated string constant». Такие строки править инструментом Edit. +- 🪤 `crm_admin_user` гранта на `ad_campaign_messages` **не получил** — когда появится + админский список «ждёт разбора», нужна догоняющая миграция `GRANT SELECT` по образцу `2026_07_27_100100_grant_admin_ad_campaign_banners.php`, иначе экран увидит ноль строк молча. - 🪤 В worktree нет `lefthook` — каждый коммит печатает `Can't find lefthook in PATH`. Это нормально; gitleaks гоняется руками: `"…/Документация/bin/gitleaks.exe" protect --staged --no-banner`. -- 🪤 Помощник тестов джоба называется `makeModeratedCampaignWithBanners($adIds, $yandexCampaignId)` - и уже есть в файле — своего писать не нужно. Http-заглушка ставится на `'*/json/v5/ads'`. ## Красные линии, которые действуют всегда