- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.
Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.
Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
- результат «В корзину»: одна причина, дата созвона стирается, платящего не выбросить;
- реклама на корзине встаёт СРАЗУ — явной веткой, а не случайно через unknown_stage;
- у «Выслано КП» дата созвона стала необязательной: бывает «пришлите на почту,
если интересно — перезвоню». Врущий старый тест на 422 без даты поправлен;
- фильтр доски date_mode=todo|changed + период (добавлен вид «завтра»).
«Надо сделать» — созвон в периоде ИЛИ просрочен, кроме отказа и корзины.
«Менялось» — есть движение стадии ИЛИ запись разговора за период.
Прогон: 1245/1245 (отдел продаж + все юнит-тесты).
Стадию меняют два разных места — результат разговора менеджера и автопереезд
по деньгам. Джоба не оставляла следа вообще, поэтому «что менялось за день»
было не из чего построить. Запись движения перенесена в событие модели: один
шов на всех, включая любой будущий третий источник.
- sales_prospect_moves — журнал всех движений (append-only, с GRANT'ами ролям);
- prev_stage на карточке — откуда приехала (кормит счётчик «69/1» у «Отказа»);
- stage += 'trash' — место под колонку «Корзина».
Сторож проверен вырезанием: без записи движения краснеют 4 теста, в том числе
тест джобы и тест API.
Просьба записана дословно, три скрина описаны словами, заведена папка
docs/superpowers/screens/2026-08-01-korzina-filtry/ под сами картинки.
Собраны находки по коду (счётчик колонки, фильтры доски, отсутствие
истории стадий) и четыре вопроса владельцу с рекомендациями.
Правка потерялась при замере статанализа: файл уходил в стэш и вернулся
несохранённым, а коммит Task 1 прошёл без него. Поймано сверкой боевого
с собранной папкой перед выкатом, а не тестами — тесты идут по рабочей
папке и правку видели.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
В спеке §3 уточнено по факту кода: нормализатор отдаёт номер с плюсом,
а хранится он без плюса — плюс срезаем.
Сторожа орфографии и разметки пропущены: они не находят проблемы, а падают —
их библиотеки в корне обрезаны той же поломкой, что статанализ и сторож
журналов. Сторож секретов gitleaks — живой, работает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Серая строка только для чтения в блоке результата — менеджеру перед звонком
не нужно лезть за этим в историю разговоров.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Выбор канала + поле значения, подпись поля меняется по выбору. Дата созвона
обязательна у обоих. Платящему клиенту результаты не предлагаются.
«Зарегистрировался» остаётся доступен с новых стадий — иначе из них не выйти.
Статанализ и сторож журналов «мозга» пропущены с разрешения владельца: первый
даёт тот же фон замечаний, что и до правки, второй падает из-за обрезанных
корневых библиотек — оба к этой работе отношения не имеют.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
До правки decide на незнакомой стадии возвращал unknown_stage — реклама на
такие фирмы встала бы без единого сообщения. Теперь обе новые стадии идут
по правилу переговоров: крутим до созвона, потом ещё срок просрочки.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Сторож проверен вырезанием: с убранной защитой тест краснеет, с возвращённой
зеленеет. Зелёный тест, которого не видели красным, ничего не охраняет.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Канал из пяти: почта, ватсап, телеграм, макс, другое. Адрес/ник сохраняем
как ввёл менеджер — телеграм-ник и почта к телефонному виду не приводятся.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Телефон приводится к виду 79… без плюса — как контакты карточки и как
принимают Яндекс Аудитории. Неразобранный номер отказывает, карточку не двигает.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Список стадий правится в двух местах сразу: PROSPECT_STAGES на фронте и
STAGES в контроллере. Расхождение дало бы пустую колонку.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Стадии manual_testing и kp_sent в CHECK + 4 nullable-колонки под их поля.
Откат и повторный накат проверены на своей тестовой базе измерением колонок.
Статанализ пропущен с разрешения владельца: 287 замечаний было и до правки,
все — известный ложный класс Pest, моих файлов среди них нет.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ветка отставала от main на 161 коммит; из файлов моей задачи устарел
только журнал схемы. Беру канонический вариант из main, чтобы номер новой
версии не столкнулся с занятыми (верхняя в main — v9.27). Словарь cspell
подтянут тем же движением — без него сторож орфографии не пропускает
свежие строки журнала.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Хвост «связка» из 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>
Хвост №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>
Два дефекта, внесённых предыдущим коммитом, оба мои:
1. cspell-words.txt — в словарь вместе с четырьмя новыми словами попали
служебные строки вывода хука: "[INFO] Recording command outcome: cd" и
"[OK] Command outcome recorded", плюс лишняя пустая строка. Причина —
дописывание словаря через printf с перенаправлением в конец файла:
вывод хука приклеился к тому же файлу. Строки удалены, словарь проверен —
cspell на изменённых файлах даёт 0 ошибок.
2. tools/observer-chain-map.json — файл был переписан скриптом через
json.dumps с отступом 2, из-за чего компактные однострочные массивы
развернулись в многострочные: диф раздулся до 180 добавленных и 56
удалённых строк вместо одной строки по существу. Восстановлено исходное
компактное оформление, узел grilling вставлен строкой после brainstorming.
Чистый итог обоих файлов против состояния до работы над #90 — четыре слова
в словаре и одна строка в карте цепочек. Проверки: observer-chain-map-checker
OK 17 chains in sync, cspell 0 ошибок.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Зарегистрирован вендоренный скил 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>
Фиксирует итог сессии 28.07: приёмочный лист 9 правок F1–F9, деплой-чеклист srv_bypass,
результаты живой проверки робота — A1 (чтение вердикта 2231134) и A2 (пересдача с документом,
0 руб), рецепт пересдачи для будущего robot-mode. Только документ.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Живая проверка A1 (28.07.2026) на реальном отказе кампании 2231134 вскрыла два бага в
readModerationStatus, из-за которых боевой код не находил строку кампании — на юнит-тестах
было зелено, а кабинет показал обратное:
1. Паттерн ссылки: в списке кампаний ссылка = /cabinet/campaigns/telegram/{id}, а код искал
/telegram-a2p/{id} — это адрес детальной/визард-страницы. Matcher расширен на оба варианта
через campaignHrefRe/hrefMatchesCampaignId плюс юнит-тест.
2. Глубина подъёма по DOM: строка списка — грид из div, не tr/li; контейнер со статусом ряда
на ~8 уровней выше ссылки. Предел подъёма поднят с 6 до 10; возврат на первом предке со
статусом — выше склеиваются две кампании.
Итог живого прогона: робот вернул rejected плюс полный текст 5 пунктов модерации из слайд-модалки
Причины. Метка FLOW-CONFIRM в cabinet.js снята. Робот npm test 94/94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрывает находки ревью: C1-блокер + рассинхроны длины + все оранжевые. TDD, всё зелёное.
Поведение в песочнице не меняется; правки готовят ветку к боевому включению.
F1 (блокер): AdWalletService::freeze реактивирует released-hold через updateOrCreate по
4-ключу — пересдача кампании и повторная заявка на имя больше не падают на дубле ключа
23505 в боевом режиме.
F2: длины валидации выровнены под колонки БД — имя 64, ad_link 500, ord_category 200;
длинное значение даёт ошибку поля, а не замаскированный 422 от БД.
F3: авто-рассылка морозит потолок бюджета симметрично ручному запуску только в бою и
считает дневной лимит под lockForUpdate строки правила.
F4: кампания не зависает в moderating вечно — переход moderating→needs_review плюс
предохранитель уборщика по возрасту client_tg.moderation_stuck_hours=48, бронь не трогаем.
F5: единое осторожное правило возврата брони в finalize и failed — есть mts_campaign_id
значит могла уйти на модерацию → needs_review без release; нет id → failed плюс возврат брони.
F6: робот cabinet.js — денежные кнопки оплатить/списать/запустить в чёрном списке
domClickButton, finalize целит только кнопку отправки на модерацию.
F7: finalize live не врёт launched:true на шаге /payment — launched:false, stoppedAt:payment;
не дошли до /payment → падаем громко.
F8: assertCostWithinCap подключён в live-finalize — сверка фактической стоимости с потолком.
F9: GRANT SELECT служебным ролям на client_tg_campaigns миграцией 000016 — иначе
кросс-тенантные джобы Poll/Sweep видели бы 0 строк на проде; правка ложного комментария в
000011. CHANGELOG v8.93, rls-reviewer CLEAN. ДЕПЛОЙ: ПЕРЕзапустить db/03_service_bypass_policies.sql.
Приёмка: бэкенд ClientTg 196/196; робот npm test 89/89; pint/phpstan/deptrac чисто. Фронт не трогали.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрывает Этап 5 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
Клиент управляет именем отправителя и авторассылкой сам из кабинета; авто не тратит
без денег и сверх дневного лимита. TDD, весь бэкенд+фронт зелёный.
## 5.1 — предохранители авторассылки
Накопитель в БОЮ перед постановкой пачки проверяет: смета умещается И в свободный
остаток кошелька (balance−frozen), И в дневной лимит правила за вычетом трат за
сегодня. Не прошла — держим черновиком + Log::info('client_tg.auto_skipped', reason).
Дефолт daily_limit_rub=0 → авто выключено (само денег не потратит). В песочнице гейта
нет (деньги не трогаются, как ручной launch). Новые колонки client_tg_auto_rule:
daily_limit_rub, spent_today_rub, spent_date (счётчик за день, сброс при смене даты).
## 5.2 — клиентское API имени (SenderController)
GET /sender (статус + остаток грейса), POST /sender (завести), /sender/disable,
/sender/enable (suspended→active, идемпотентно по периоду — без двойной оплаты в месяц;
проверка средств ДО списания). Логика в TelegramSenderService (enableSender+snapshot),
контроллер тонкий. Всё скоуп тенантом.
## 5.3 — экран имени (TelegramSenderPanel.vue)
Самодостаточная панель: статус имени человеческими словами, дата оплаты, остаток грейса
при долге; suspended → «Отключено за долг» + «Включить»; нет имени → форма «Завести имя».
telegram.ts: fetchSender/createSender/disableSender/enableSender.
## 5.4 — экран авторассылки + API (AutoRuleController)
GET/PUT /api/telegram/auto-rule (вкл/выкл, объявление, порог, бюджет, дневной лимит).
Порог клиентский — новая колонка client_tg_auto_rule.batch_threshold (NULL → дефолт
конфига 367; ниже 367 API не даёт — минимум МТС). TelegramAutoRulePanel.vue: тумблер +
поля порога/бюджета/лимита. В списке кампаний авто-кампании (created_by=null) помечены
чипом «авто».
Обе панели встроены в AdvertisingTelegramView.
## Схема
Две аддитивные миграции на существующую таблицу client_tg_auto_rule (000014 daily_limit
+ счётчик за день; 000015 batch_threshold). RLS/GRANT не тронуты (табличный GRANT
покрывает новые колонки). Записи db/CHANGELOG_schema.md v8.91/v8.92; обе прогнаны через
rls-reviewer — CLEAN.
## Приёмка
TDD. Бэкенд ClientTg 177/177; phpstan/pint/deptrac чисто. Фронт полный набор
212 файлов/1538 тестов зелёные; vue-tsc+ESLint по нашим файлам чисто. Проверено вживую
в браузере (клиент demo, песочница): обе панели читают и пишут — правило сохраняется
точь-в-точь (порог/бюджет/лимит), имя заводится (pending, деньги не тронуты).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрывает фронтенд Этапа 4 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md
(бэкенд 4.3 — уведомление об одобрении — сделан в Этапе 3). Чистый Vue+Vitest, TDD.
## 4.1 — смета и охват ДО запуска
Разделены create/launch в UI: кнопка «Рассчитать» создаёт черновик (createTelegram) и
показывает прогноз стоимости + охват; «Запустить» доступна только после расчёта. Любая
правка формы сбрасывает расчёт (watch), чтобы не запустить по устаревшим данным.
## 4.2 — автообновление статуса
Пока есть «живые» кампании (queued/running/moderating) — экран сам зовёт fetchTelegram по
интервалу (15с), setInterval со снятием в onUnmounted; вне живых статусов не опрашивает.
## 4.4 — экран отказа: пересдача + пустая причина
У rejected-кампании кнопка «Исправить и пересдать» → инлайн-форма правки (текст/ссылка/ОРД +
опц. документ модератору) → новый resubmitTelegram в telegram.ts (multipart POST .../resubmit).
При пустой status_reason — единая заглушка REASON_PLACEHOLDER (действенная, без «круга»).
В интерфейс TelegramCampaign добавлено moderator_file_path.
## 4.5 — подсказки
У moderating — «проверка ~4 часа»; пока песочница — метка «песочница» на каждой кампании.
## Причёсывание под домашний стиль кабинета (осмотр вживую 28.07)
Экран приведён к виду соседних клиентских экранов (эталон DashboardView): обёртка
v-container fluid pa-6 + scoped max-width:1100px (были прижаты к краям во всю ширину);
крупный заголовок + строка-описание; поля density=compact variant=outlined (была «рыхлая»
форма); контент собран в карточки «Новая кампания» / «Мои кампании» (была «полосатая» зона).
Палитра Forest не тронута. STATUS_LABELS дополнен moderating/needs_review/cancelled.
Починена мелочь-логика: «Рассчитать»/«Запустить» больше не активны с пустым списком номеров
в режиме «Свой список».
Миграция не нужна (moderator_file_path добавлена в Этапе 3; новые ярлыки статусов — без БД).
TDD. Приёмка (моя область): фронт-тесты Телеграма 22/22; полный фронт-набор 210 файлов /
1529 зелёные; vue-tsc + ESLint по изменённым файлам чисто. Проверено вживую в браузере
(клиент demo): двухшаговые кнопки, скрытие/показ полей по аудитории, гейт по номерам.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрывает Этап 3 «Жизненный цикл и модерация» целиком (3.1–3.6) и бэкенд-часть 4.3.
План docs/superpowers/plans/2026-07-27-telegram-module-hardening.md.
## 3.5 — робот читает статус/причину модерации по mts_campaign_id (Node)
- `parseModerationStatus` (cabinet.js) — чистый парсер статус-текста кабинета в канон
Laravel-опросчика: «Отклонена»→rejected, «Одобрена»/«Активна»→approved,
«На модерации»→moderating, «Черновик»→draft, иначе null (тогда опросчик ждёт).
Терминальные вердикты приоритетнее слова «модераци…» в строке-блобе. Юнит-тест
read-status.test.mjs (11 кейсов: регистр/nbsp/блоб/приоритет/мусор).
- `readModerationStatus` (cabinet.js) + `runReadStatus` (runner.js) + режим `read-status`
в bin/run.js: открывает список кампаний, находит ряд по id, читает статус; при отказе —
причину из слайд-модалки «Причины» (#slide-modal-root). Отдаёт JSON
{ok, moderationStatus, reason?, campaignId} — его уже разбирает RobotResult (3.4).
🔴 Читалка кабинета помечена <FLOW-CONFIRM>: DOM-обёртки ряда/модалки собраны по
живой разведке «Сессии 6» (FLOW-FINDINGS.md), но именно этим кодом live ещё не прогнаны —
подтвердить на следующем цикле модерации с разрешения владельца. Парсер от DOM не зависит.
## 3.6 — пересдача отклонённой кампании (rejected → queued + документ модератору)
- Миграция 000013: колонка `client_tg_campaigns.moderator_file_path` (varchar 500 NULL,
после media_path) + CHANGELOG схемы v8.90; rls-reviewer прогнан — чисто (nullable-колонка
данных, не tenant-скоуп, RLS не меняется). Модель — fillable.
- Endpoint `POST /api/telegram/campaigns/{id}/resubmit`: только отклонённую (иначе 422);
правки ad_text/ad_link/ord_category (валидация как store) + опц. файл модератору
(.png/.jpeg/.jpg/.pdf ≤10 МБ, сохраняется на диск local). Успех: поля обновлены,
status_reason и mts_campaign_id очищены (робот создаст новую кампанию в кабинете),
rejected→queued, dispatch RunTelegramCampaignJob afterCommit. В бою — гейт аудитории
+ freeze budget_cap_rub заново (при отказе бронь вернул опросчик 3.4; freeze идемпотентен
по ACTIVE-холду), нехватка → 409, остаётся rejected. Песочница — без брони.
- Робот: task `moderatorFile` (task.js passthrough + TelegramRobotRunner.taskPayload),
RunTelegramCampaignJob отдаёт moderator_file_path; fillAd грузит файл в поле «Комментарий
для модератора» (третий file-input, accept pdf) — помечено <FLOW-CONFIRM> (live не прогнан).
- Тесты: ResubmitTest.php (7 кейсов: rejected→queued+очистка+джоб / файл сохранён /
не-rejected→422 / live 409 / валидация / .exe→422 / чужой→404); Node task.test.js (+2).
## 4.3 — уведомление об одобрении (бэкенд был готов в 3.4, добор покрытия)
- ApproveNotifyTest.php (4 кейса): notifyTelegramCampaignApproved шлёт in-app всем активным
юзерам тенанта без pref-гейта, тело «одобрена/показы пошли»; неактивный/чужой не получают.
TDD. Приёмка (моя область, чистый прогон): весь ClientTg 150/150, робот 78/78,
ApproveNotify 4/4; phpstan 0, deptrac 0, pint чисто.
Приёмочный лист 3.6 — docs/superpowers/2026-07-28-telegram-3.6-resubmit-acceptance.md.
Осталось в Этапе 4: фронтенд 4.1/4.2/4.4/4.5 (Vue-экран + Vitest) — НЕ начато.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.4. Живая отправка кампании
ставит статус `moderating` (задача 3.2), но одобрение/отказ приходит от МТС
позже и без API. Опросчик раз в цикл робот-читалкой заходит в кабинет по
`mts_campaign_id` и применяет вердикт — КОНСЕРВАТИВНО с деньгами.
- PollTelegramModerationJob: раз в 15 минут перечисляет `moderating`-кампании
с `mts_campaign_id` (без id на модерацию уйти не могли — пропускаем) и читает
вердикт роботом РОВНО ОДИН РАЗ на кампанию (каждый вызов = заход в браузер):
· `rejected` («Отклонена») → статус `rejected` + причина из кабинета; возврат
брони кошелька (в бою, ключ совпадает с freeze); уведомление «отклонена»;
· `approved` («Одобрена») → `launched`; уведомление «одобрена». Деньги НЕ
трогаем — бронь под потолок держится, фактическую стоимость спишет билинг
по факту показов (2.4 → позже);
· `moderating` / робот не прочитал вердикт (null) → НЕ трогаем, ждём цикла.
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; возврат брони и уведомление —
ПОСЛЕ транзакции статуса, каждый в своём try/catch. Повторная проверка
status===moderating под lockForUpdate (гонка). Зеркалит SweepStuckTelegramCampaignsJob.
- RobotResult: поле `moderationStatus` (approved|rejected|moderating|null) + разбор
в fromRobotJson.
- TelegramRobotRunner: метод `readModeration($mtsCampaignId)` (режим read-status,
Node-сторона — задача 3.5); `campaignId` проброшен в task-payload.
- NotificationService: `notifyTelegramCampaignApproved` (зеркало Rejected, in-app
без pref-гейта) + событие EVENT_TG_CAMPAIGN_APPROVED.
- console.php: расписание опросчика everyFifteenMinutes с heartbeat-трекингом.
Селекторы экрана отказа — из живой разведки Part B (bots/mts-telegram-ads/
FLOW-FINDINGS.md, «Разведка Сессии 6»); повторного захода в кабинет не потребовалось.
Миграция не нужна — новых колонок/статусов нет (moderating/rejected уже были).
TDD. Приёмка (моя область, чистый прогон): PollModerationTest 6/6 (отклонена/
одобрена/ещё-на-модерации/не-прочиталось/без-id + деньги: при отказе бронь
возвращена, при одобрении не тронута) + весь набор ClientTg 143/143.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка перед отправкой светит по всему хранилищу, включая чужие ветки, и
блокировала любой push. Находки — номера-образцы в файле-примере для клиента
и в демо-данных экрана смс-рассылки, из параллельных веток. Владелец
подтвердил 28.07.2026, что номера выдуманные.
Помечены поимённо в разделе «разобранные находки» с пояснением по каждому.
Пути из-под охраны НЕ выводятся: новые утечки в тех же файлах ловятся как
прежде.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.3. Робот в браузере может
оборваться на полпути (воркер убит, сеть отвалилась) — кампания навсегда
виснет в `running`, а замороженные деньги залипают. Уборщик добивает статус,
но КОНСЕРВАТИВНО с деньгами.
- SweepStuckTelegramCampaignsJob: раз в 10 минут ищет `running` старше 15 минут
(штатный робот ≤ 420с). Развилка по факту создания черновика в кабинете:
· нет mts_campaign_id (черновик не создан) → кампания заведомо не ушла →
`failed` + возврат брони кошелька;
· есть mts_campaign_id (черновик создан) → могла уйти на модерацию МТС →
деньги вслепую НЕ возвращаем, `needs_review` до ручной сверки (задача 3.4).
Кросс-тенант через pgsql_supplier (BYPASSRLS), правки под SET LOCAL
app.current_tenant_id на дефолтном соединении; release гейтится на !sandbox,
ключ совпадает с freeze. Повторная проверка status===running под lockForUpdate
(гонка с finalize). Зеркалит связку ChargeTgNameFeeJob + RunTelegramCampaignJob.
- Campaign.php: константа STATUS_NEEDS_REVIEW; переходы running→needs_review
(терминальный) и queued→failed (для failed() джоба при раннем сбое). Миграция
не нужна — на колонке status нет CHECK, машина статусов в модели.
- RunTelegramCampaignJob: public int $timeout = 420 (робот ~300с + навигации);
failed() теперь умеет queued→failed (переход добавлен в модель).
- console.php: расписание уборщика everyTenMinutes с heartbeat-трекингом.
TDD. Приёмка (моя область, чистый прогон): SweepStuckTest 7/7 (в т.ч. деньги —
с черновиком не тронуты, без черновика возвращены) + StatusMachine/Moderating/
ExternalId/RunCampaignJob/LaunchIdempotency/RobotResult/RobotRunner 49/49.
phpstan 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.
Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.
Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.2. Живая отправка робота в МТС
означает «ушло на модерацию», а не «запущено»: кампания получает статус
`moderating`, а `launched` придёт позже, когда опросчик вердикта (задача 3.4)
увидит одобрение.
- Campaign.php: константа STATUS_MODERATING + переходы running→moderating,
moderating→launched|rejected, rejected→queued (пересдача, задача 3.6).
running→launched оставлен ради обратной совместимости. Миграция НЕ нужна:
на колонке status нет CHECK-ограничения (машина статусов — в модели),
`moderating` влезает в varchar(16).
- RunTelegramCampaignJob.finalize(): живой успех (launched=true) → moderating
вместо launched; песочница (черновик) по-прежнему → draft_ready.
Тест-инфра (побочно, но необходимо для проверки): tests/TestCase.php получил
`protected $dropTypes = true`. RefreshDatabase's migrate:fresh дропал таблицы,
но НЕ типы Postgres; при заблокированном дропе таблицы её composite row-type
переживал db:wipe, и перезагрузка db/schema.sql падала на дубле типа
(«legal_entities … уже существует»), оставляя ЧАСТИЧНУЮ схему — давний
интермиттентный флак «migrate:fresh иногда прерывается» (site_events/
client_tg_tariffs случайно отсутствовали). Дроп типов на каждом refresh резко
снизил флак (было 15–46/130 → стало 93–126/130). Только для APP_ENV=testing,
на прод не влияет. Остаточный редкий обрыв — отдельная пред-существующая
проблема, не этой задачи.
TDD, робот замокан, песочница. Приёмка (моя область, чистый прогон):
ModeratingStatusTest 8/8 + StatusMachineTest/RunCampaignJobTest/ExternalIdTest/
RobotRunnerTest 35/35 суммарно; phpstan (Campaign+Job) 0, deptrac 0, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.1b. Робот создаёт реальный
черновик в кабинете уже на шаге «Аудитория»; упавший прогон плодит осиротевшие
черновики (27.07 чистили руками, 2 шт.). Узаконенный инструмент вместо ручного.
- src/cleanup-drafts.js — чистые предикаты (покрыты юнит-тестом) + браузерная
обёртка cleanupDrafts(). Предикаты: isDeletableDraftRow (только строка-черновик
«Telegram по своей базе», НЕ «на модерации»/«отклонена»/«активна», НЕ шапка),
isHeaderOrAggregateRow, selectionIsSafe (гейт перед удалением: ≥1 выбран, ВСЕ
черновики, select-all снят — иначе аварийный стоп).
- bin/cleanup-drafts.js — CLI: read-only по умолчанию (показывает, что БЫ удалил);
удаление только по явному флагу --delete, через сверку selectionIsSafe.
- Логика перенесена с проверенного живьём 27.07 ручного скрипта; критический гейт
проходит через Node-предикат selectionIsSafe (а не только браузерный скан).
- Процедура задокументирована в FLOW-FINDINGS.md.
Приёмка: юнит cleanup-drafts 17/17, весь Node-набор 65/65 (npm test). Без БД,
без денег, без выхода в кабинет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Этап 3 «Жизненный цикл и модерация», задача 3.1. Плюс закрыта задача 3.0
(живая разведка экрана отказа) — вердикт МТС по кампании «займ» (2231134)
пришёл: «Отклонена». Разведка read-only, деньги не тронуты.
Задача 3.1 — колонка mts_campaign_id + РАННЕЕ и надёжное сохранение:
- Миграция client_tg_campaigns.mts_campaign_id (varchar(32) NULL, после
status_reason) + запись CHANGELOG_schema v8.89 (предварит., ветка). RLS не
меняется; rls-reviewer не требуется (nullable-колонка данных).
- Робот (Node): чистый хелпер parseCampaignId(url) в cabinet.js (покрыт
тестом); runner.js захватывает id СРАЗУ после создания черновика (шаг
аудитории) и печатает маркер MTS_CAMPAIGN_ID=<id> в stderr; id теперь
идёт и в ветке ОТКАЗА (раньше терялся).
- Обёртка (PHP): TelegramRobotRunner восстанавливает id из stderr-маркера во
всех путях (таймаут/непарсабельный вывод/JSON без id); RobotResult::failed
принимает id.
- Джоб: finalize сохраняет mts_campaign_id при ЛЮБОМ исходе (успех/отказ), не
затирая ранее сохранённый id. Метод failed() не трогали — туда результат не
доходит (осознанный residual, закроют уборщик 3.1b и sweeper 3.3).
Разведка отказа (3.0) записана в bots/mts-telegram-ads/FLOW-FINDINGS.md:
причина показана текстом в слайд-модалке «Причины отклонения кампании»
(кнопка «Причины»); поля загрузки файла на экране отказа нет — документ
грузится через «Исправить» → шаг «Сообщение» → «Комментарий для модератора»;
кнопка пересдачи — «Исправить».
TDD, робот замокан, тесты на liderra_testing (номера 7999…).
Приёмка: Node 48/48 (npm test), Pest ExternalIdTest 2/2 + регрессия ClientTg
122/122, phpstan (4 боевых файла) 0, deptrac 0 нарушений, pint чисто.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Задача 2.0 (разведка) — РЕШЕНА без входа в кабинет: ответ уже был в
находках Фазы 2. Билинг МТС за «показы своей базе» — НАКОПИТЕЛЬНЫЙ
(резерв → списание по факту показов → возврат остатка, экран /payment).
Следствие: списание «по факту» (была задача 2.4) на успехе отправки честно
сделать нельзя — показов ещё нет; перенесено в Этап 3 (опрос завершения).
Этап 2 закрыт составом 2.1 (отмена) + 2.3 (возврат брони) + 2.2.
- 2.2 PHP: RobotResult несёт `actualCostRub` (nullable string) — заготовка,
чтобы позже (Этап 3) прочитать фактическую стоимость из кабинета и списать
её с кошелька клиента. Проброшено в fromRobotJson (робот начнёт класть поле
позже; нет поля → null). Мёртвых фабрик launched()/draftReady() не добавлял.
- 2.2 Node: чистая утилита `parseCost(text)` в cabinet.js — «Стоимость
кампании от 201,6 ₽» → «201.60» (запятая→точка, разделители тысяч включая
неразрывный пробел код 160, дробь до 2 знаков без округления, нет числа →
null). Вынесена отдельной покрытой функцией; к DOM-потоку НЕ подключена
(селектор строки стоимости подтвердим живьём в Этапе 3).
- Разведка билинга и решение по 2.4 зафиксированы в FLOW-FINDINGS.md; план
Этапа 2 обновлён (2.0 решён, 2.4 → Этап 3, Этап 2 закрыт).
TDD, робот замокан, тесты на liderra_testing.
Приёмка: Node 40/40 (npm test), Pest RobotResultTest 5/5 + RobotRunnerTest
(потребитель) зелёный, регрессия ClientTg 120/120, phpstan RobotResult.php 0,
pint чисто, deptrac 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрыты денежно-независимые куски Этапа 2 (находки #1, часть). Списание
по факту (2.2/2.4) ждёт подтверждения билинг-модели МТС.
- 2.1 Статус кампании «cancelled»: константа STATUS_CANCELLED, переходы
draft→cancelled и queued→cancelled (отмена только ДО запуска); cancelled —
терминальный. Из running/терминальных отмена запрещена. Миграция не нужна —
status это string(16) без CHECK-констрейнта.
- 2.3 Возврат брони (release), иначе резерв кошелька залипал (находка #1):
• при отказе робота (finalize) — release после записи FAILED, отдельным
tenantTx, чтобы сбой возврата не откатил статус;
• при перманентном сбое джоба (failed) — тоже release;
• новый endpoint POST /api/telegram/campaigns/{id}/cancel: draft/queued →
cancelled с возвратом брони; из running/терминальных — 422.
Возврат гейтится !sandbox (в песочнице заморозки не было). Ключи release
совпадают с freeze в launch ('telegram','campaign',id). RLS: денежная
операция в джобе идёт под SET LOCAL app.current_tenant_id (образец
ChargeTgNameFeeJob).
Возврат при rejected здесь НЕ трогаем — его выставляет опросчик модерации
(Этап 3, задача 3.4).
TDD, робот в тестах замокан, тесты на liderra_testing (7999… номера).
Приёмка: Pest ClientTg RefundOnFail 5/5 + регрессия соседей (32/32 суммарно),
deptrac 0, pint чисто, phpstan по боевым файлам (джоб/контроллер) 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Закрыты 5 находок аудита (безопасность и валидация входа, деньги не задеты):
- #11 ПДн-скрины робота: полноэкранный скриншот кабинета МТС (мог содержать
телефоны базы, 152-ФЗ) больше не снимается и не уходит письмом по умолчанию —
только по явному TG_DEBUG_SHOTS. Чистый хелпер src/shots.js.
- #12 стоп-лист opt-out теперь нормализуется при сравнении (8XXXX / 10-значные
формы вычищаются), сравнение по голому 7XXXXXXXXXX.
- #13 верхние пределы входа: phones max:200000, phones.* max:32, audience_days
max:365; store возвращает dropped_count (сколько номеров не распозналось).
- #14 идемпотентность запуска: Фаза A джоба читает кампанию с lockForUpdate —
два воркера сериализуются на блокировке строки (защита от гонки).
- #2 предстартовый гейт аудитории: в боевом режиме launch НЕ бронирует деньги
под кампанию с <367 / пустой аудиторией (иначе бронь залипала бы). Порог —
config client_tg.auto_batch_threshold (367). В песочнице гейта нет.
TDD, робот в тестах замокан, тесты на liderra_testing (7999… номера).
Приёмка: Node 28/28, Pest ClientTg 105/105, phpstan 0, pint чисто, deptrac 0.
Обновлён CampaignApiTest (тест «денег не хватает → 409» засеян ≥367 контактами,
чтобы дойти до проверки денег после нового гейта).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Разбор клиентского модуля «Реклама в Телеграме по своей базе» на дыры
(4 параллельных код-ревью + личная перепроверка) и план их закрытия.
- findings: 14 находок, сгруппированы по серьёзности; главное — вся денежная
часть латентна под песочницей, но капканы (залипшая бронь, минимум 367 после
freeze, потерянный внешний id, зависшие статусы) сработают при go-live.
- spec: решения владельца (бронь→возврат/списание по факту; «своё имя» и
авторассылка доделываем; начинаем с безопасности), границы, приёмка по областям.
- plan: 5 этапов в порядке 1→2→5→3→4, каждая задача в TDD; этап 3 ждёт живого
отказа МТС (Part B).
Ревизия 27.07 (разбор самих спеки/плана на дыры):
- билинг-модель МТС («от X ₽» = нижняя граница, трата по показам) —
выяснить ДО реализации списания (предусловие этапа 2, задача 2.0);
- внешний id кампании МТС сохранять РАНО (робот плодит реальные черновики уже
на шаге аудитории) — иначе осиротевшие черновики и слепой рефанд;
- уборка брошенных черновиков (сегодня чистили руками) — узаконить (задача 3.1b);
- уборщик зависших консервативен: при известном mts_campaign_id не рефандит вслепую;
- оговорка: тест идемпотентности слабый (dispatchSync последователен).
Только документы (docs/superpowers/*). Кода не трогает.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Клиентский модуль «Реклама в Телеграме по своей базе» (робот-в-браузере, МТС Маркетолог).
Задача 5.2 — обратная связь по отказу:
- миграция client_tg_campaigns.status_reason (varchar 500, nullable; RLS без изменений — колонка на существующей таблице), запись v8.88 в CHANGELOG_schema
- Campaign: status_reason в fillable
- NotificationService::notifyTelegramCampaignRejected — in-app уведомление всем активным
пользователям тенанта без pref-гейта (важное операционное сообщение доходит всегда)
- RunTelegramCampaignJob: при провале робота сохраняет причину в status_reason и шлёт уведомление
(Tenant грузится внутри tenantTx для RLS, notify — после транзакции)
- фронт: telegram.ts (+status_reason), AdvertisingTelegramView показывает причину отказа/ошибки
- тесты: RejectNotifyTest (4 Pest) + 2 Vitest на экран
Закалка робота против частых глюков кабинета МТС:
- browser.js: gotoStable() — терпеливая загрузка с перезагрузкой (кабинет виснет на пустой крутилке)
- session.js: isLoggedIn через gotoStable — больше нет ложного «вход слетел»
- cabinet.js: знакомство пропускается если его нет (только для новичков); оферта по #isOfferAccepted;
осознан шаг /payment (денежная развилка «оплатить/без оплаты» — Сессия 6)
Живая разведка кабинета зафиксирована в FLOW-FINDINGS.md (селекторы, шаг /payment, поле файла модератору).
Проверки: Pest ClientTg 93/93, Vitest экрана 7/7, node-тесты робота 20/20.
Робот в тестах замокан, тесты на liderra_testing. Реальных ПДн нет (номера фейковые 7999…).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.
4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
(audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).
4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.
4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).
4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).
Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сессия 3 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Клиент сам собирает объявление + аудиторию и жмёт «Запустить»; робот кабинета МТС
(в тестах замокан) доводит до черновика. Всё в песочнице: деньги/живой запуск выключены.
- Клиентский API (Api\ClientTg\CampaignController + маршруты /api/telegram/*):
создание (store) и запуск (launch) РАЗДЕЛЕНЫ. store создаёт ЧЕРНОВИК со сметой и
числом кандидатов (деньги/робот не трогаются); launch draft→queued, в бою бронирует
лимит на объявление (budget_cap_rub) и ставит RunTelegramCampaignJob. Нехватка денег
→ 409 (кампания остаётся черновиком); повторный запуск не-черновика → 422; изоляция
тенантов (чужой show → 404). Валидация текст/ссылка/аудитория/бюджет; deals без срока → 422.
- Фронт-API telegram.ts (зеркало client-sms.ts, уже): fetch/create/get/launch, cookie-сессия.
- Экран AdvertisingTelegramView.vue (/advertising/telegram): форма (объявление, ссылка,
3 способа аудитории, лимит), кнопка «Запустить» (create→launch), баннер песочницы,
список кампаний с человеческим ярлыком статуса, сообщение при нехватке денег.
- Канал «Реклама Телеграм» АКТИВИРОВАН: advertisingChannels.ts route, провод в AppSidebar
и AppMoreDrawer (route → реальный экран, остальные каналы — прежняя заглушка), маршрут
в router. Тесты каналов обновлены (телеграм больше не заглушка).
- Ручной сквозной сценарий (песочница): создать → запустить → джоб (робот замокан, draft)
→ draft_ready + matched виден в API.
Отличие от СМС-близнеца: отдельного эндпоинта «предпросчёт до создания» нет — смету и
кандидатов клиент видит в самом черновике (создание черновик денег не тратит).
Проверки: 60/60 Pest ClientTg зелёные (+10 новых), 1512 Vitest зелёные (0 регрессий),
phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений. Робот в тестах замокан
(кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сессия 2 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Сердце модуля: по кампании запускаем браузер-робота кабинета МТС (у МТС нет API),
парсим его JSON и ведём статус-машину. В тестах робот замокан node-скриптами.
- Статус-машина Campaign: draft → queued → running → (draft_ready | launched |
failed | rejected); недопустимый переход — DomainException, статус не меняется
(глобальное исключение, а не доменный класс: deptrac Model: [], ADR-005).
Терминальные статусы без исходящих (resubmit — Сессия 5).
- TelegramRobotRunner: формирует task.json и запускает `node bin/run.js --task <file>`
через Symfony Process, разбирает {ok,matched,launched,campaignId}. Таймаут и
непарсабельный вывод → аккуратный RobotResult::failed, воркер не падает.
- RunTelegramCampaignJob: queued → running, собирает кандидатов, пишет временный
файл номеров (ПДн, чистится в finally), зовёт робота ВНЕ транзакции, итог пишет
под tenant-контекстом (SET LOCAL). ok+draft → draft_ready + matched; ошибка → failed
(причина в журнал — колонки нет, как у СМС-близнеца; показ клиенту — Сессия 5).
Идемпотентно: работает только со статусом queued. Денег в песочнице не трогает.
- Песочница config/client_tg.php + .env.example: TG_SANDBOX (по умолчанию ВКЛ) —
робот только draft, деньги/запуск выключены. Живой режим — осознанно в Сессии 6.
Реальная точка входа робота — bin/run.js --task (в плане текст «src/runner.js» —
вольная стенография). Живое списание отложено на Сессию 6: робот пока не возвращает
фактическую стоимость.
Проверки: 50 из 50 Pest зелёные (24 новых), phpstan 0 по своему коду, робот в тестах
замокан (кабинет МТС не тронут).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сессия 1 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Бэкенд-ядро — зеркало готового СМС-модуля. Робота ещё нет — он в Сессии 2.
- 8 таблиц client_tg_* с RLS tenant_isolation и GRANT crm_app_user; справочные
tariffs/settings без RLS с guarded-GRANT. rls-reviewer PASS 8 из 8.
- 7 моделей ClientTg + связи campaign->phones.
- Ступенчатая цена TelegramTariffService — ₽ за показ по объёму; тариф = потолок,
точную стоимость считает МТС.
- Сборка аудитории TelegramAudienceService — сделки за период / своя база / свой
список, нормализация телефона, стоп-лист и дедуп; отдаёт кандидатов, реальный
охват узнаёт робот после загрузки в МТС.
- Канал кошелька telegram: перенесён общий AdWallet со свежей версии портала
— модели, сервис, миграции; списание и заморозка по каналу telegram под тестами,
charge не бросает и не уходит в минус, нехватка средств ловится freeze до списания.
Проверки: 26 из 26 Pest зелёные, larastan 0, gitleaks чисто.
db/CHANGELOG_schema.md v8.86 — номер предварительный, ветка отстаёт от main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Модуль-близнец СМС-рассылки, но через робота-в-браузере (у МТС нет API).
Спека: цель, что берём из СМС, ключевое отличие (нет провода → робот, пачки ≥367,
реальные деньги, модерация), клиентский экран, два режима (ручной + авто с лимитом
на объявление), обратная связь по отказу, что требует живой разведки, текст для клиента.
План: 6 сессий по ≤250–300k токенов с логичными остановками под /compact, каждая
оставляет код рабочим; Сессия 5 (отказ) гейтится живой разведкой Сессии 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Живой прогон runner в режиме draft отработал целиком: визард -> аудитория
(matched=437) -> объявление -> стоимость -> подтверждение -> скрин; кнопку
отправки не жмём, баланс 5010 РУБ не тронут. Юнит-тесты 20/20.
- обход коуч-марка: нативный .click() по DOM-кнопке (domClickButton/continueTo),
т.к. оверлей перехватывает мышиный клик даже с force
- фикс чтения аудитории «Не МТС» (неразрывный пробел, код 160) в readNonMts
+ поллинг завершения асинхронного матчинга
- надёжная категория ОРД (combobox: fill -> pressSequentially -> role=option)
- шаги submitAd->/budget, submitBudget->/confirmation; finalize на /confirmation
(draft=скрин без клика; live=оферта+отправка, за FLOW-CONFIRM)
- fillAd: убраны adTitle/ordCompanyName (полей в кабинете нет, рекламодатель авто)
- task.js/тесты/example: убраны adTitle/ordCompanyName/cpmRub; budgetRub оставлен
как потолок стоимости для боевого запуска
- FLOW-FINDINGS.md: полная карта флоу + две ключевые грабли и их решения
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Копия клиентского Яндекс-показы (ветка feat/reklama-yandex-pokazy) для канала ВК:
общий рекламный кошелёк, зеркало guard аудитории с числом ВК (2000), программная
загрузка креатива (проверено живым API content/static.json → операторский шаг для ВК
убираем). Доступ к VK Ads API получен и проверен вживую, ключи на бою, канал под
рубильником services.vk_ads.enabled до go-live. Код не пишем — решение владельца.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
(fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
(разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
снимаются на первом реальном черновике после разового логина владельца.
Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>