По правке владельца со скрина: блок фильтра по датам переехал из отдельной
строки под заголовком в общий ряд справа — к «Менеджер» и «Происхождение».
Порядок внутри блока перевёрнут: САМ ФИЛЬТР крайний справа, СРОК левее него.
Читается справа налево: «что менялось» → «вчера».
Ряд получил flex-wrap: на узком экране переносится, а не уезжает за край.
На экране менеджера тот же ряд — экраны не должны разъезжаться.
Порядок закреплён тестом, иначе его снова переставят.
- 12-я колонка «Корзина», результат «В корзину» просит только причину;
- в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного
тестирования»; при нуле дробь не рисуется — «69/0» это шум;
- у «Выслано КП» поле даты подписано «если договорились» и необязательно;
- орган фильтра по датам на ОБОИХ экранах, два режима + период.
Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела»
(снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/).
Браузер поймал то, чего не видели тесты: при смене режима оставался прежний
период, и «что менялось за завтра» давало пустую доску — теперь период
возвращается к «Сегодня», на это заведён отдельный тест.
Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31.
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28
переставлена поверх боевого журнала схемы. Код не пересекался.
Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений.
Сторож секретов 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>
Слияние feat/client-telegram-ads с main 8bdd58e8. Десять швов разобраны вручную:
денежный файл AdWalletService взят из main целиком — проверено поимённо, что все три
починки на месте: свой контекст клиента, оживление брони, таяние заморозки. В расписании
объединены оба набора заданий: телеграмные два и рекламные четыре. В боковом меню и в
мобильном «Ещё» сохранён пункт «Рекламный кошелёк», подписи поправлены — на реальные
экраны ведут ОБА канала. Словарь, пример настроек и журнал схемы объединены.
Сверх самого слияния:
- Журнал схемы: телеграмные записи v8.86-v8.95 перенумерованы в v9.18-v9.27, блок
переставлен наверх, пометки «номер предварительный» сняты и заменены одной врезкой
о перенумерации. Задвоенных номеров не осталось. Врезка шапки теперь называет и
телеграмные таблицы: их DDL, как и рекламный, живёт только в дельта-миграциях.
- Новый сторож денег tests/Feature/ClientTg/TgMoneyUnderRealRoleTest.php: списание и
возврат под боевой ролью crm_app_user. С контекстом клиента деньги двигаются, без
контекста возврат падает громко. Обычные тесты ходят суперюзером и этот класс дыр
увидеть не могут.
- Помощник rejectedCampaign переименован в tgRejectedCampaign: одноимённый помощник
есть у рекламного модуля, помощники Pest глобальные, полный прогон падал фаталом.
Каждая ветка по отдельности этого увидеть не могла.
- Два теста уведомлений считали ВСЮ таблицу целиком вместо строк своего пользователя:
в одиночку зелёные, в полном прогоне красные. Счёт сужен до конкретного пользователя.
- Убраны две проверки отменённой сущности «своё имя отправителя» — сама сущность
дропнута в v9.27 как СМС-фантазия, её адрес отдаёт 405.
Прогоны: телеграм 193/193, реклама 336/336 при 1029 проверках, вместе 532/532,
экраны 1704/1708, сборка фронта чисто, полный Unit+Feature 3923/3960. Шестнадцать
падений полного прогона совпадают построчно с прогоном ветки без телеграма — слияние
не добавило ни одного. Статанализ в свежем каталоге запустить не удалось: он требует
сгенерированного файла-подсказки, которого нет в репозитории, и без него молча падает
на обеих ветках.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Яндекс на отказ отдаёт машине «Отклонено на модерации.» и больше ничего. Из этого
выходило три беды сразу: в переписке клиент видел одну бесполезную фразу, в списке
кампаний под ярлыком «Отклонено» не было вообще ничего — подпись берётся как первая
строка, а первым знаком у настоящего ответа идёт перенос, — и то же самое уезжало
клиенту письмом.
Новый ModerationReason — единственное место, где ответ модерации превращается в текст
для клиента. Отписку заменяем честным «Яндекс отклонил рекламу, но причину не назвал.
Выясняем — как только узнаем, напишем здесь». Первая строка нарочно короткая: она идёт
подписью под ярлыком. Настоящую причину принесёт разведка, задача 15.
Ловушка, пойманная до написания: подмену нельзя звать на все объявления подряд —
у принятого пустое пояснение это норма, и подмена приписала бы принятой рекламе отказ.
Зовём только при отказе, на ловушку стоит отдельный тест-сторож.
Второй слой на экране: firstLine обрезает края до разбора на строки, а не после.
Слои не подпирают друг друга — сервер решает, что сказать клиенту, экран следит,
чтобы сказанное не потерялось. Письмо починилось само, оно берёт текст из переписки.
Проверено вырезанием: без подмены два теста краснеют именно на возврате отписки,
а тест про обрезку переносов остаётся зелёным.
Портал 361 из 361, экраны рекламы 37 из 37, робот 60 из 60, мест разморозки денег
по-прежнему четыре.
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.
Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.
Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обязательно только название фирмы — вывеска. ИНН по желанию: заполнили
проверяем контрольную сумму и ловим дубль, оставили пустым — карточка
заводится без ИНН. Пустая строка приравнена к «не знаю» и уходит как null.
Проверка на дубль по ИНН теперь срабатывает только когда ИНН указан:
раньше вторая карточка без ИНН у того же менеджера ложно ловилась бы как
«эта фирма уже есть в вашей воронке». Схему БД менять не потребовалось —
колонка inn была nullable, уникальный индекс частичный.
Тесты: сервер 58/58, экран 9/9. Спека §16 обновлена.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Р3 хвост. Адрес файла баннера стал /api/creative-robot/jobs/{jobId}/banners/{bannerId}/file.
Раньше портал подставлял «какое-нибудь задание в работе» — защита выдачи чужих картинок
держалась на внешнем условии, а не на самом запросе. Робота править не пришлось: он берёт
адрес из ответа портала как есть. Запись v9.08 в журнал схемы про частичный уникальный
индекс; rls-reviewer по миграции — GO.
Р4. Замок на наборе баннеров после заведения кампании в Яндексе: перезаливка, включение
и удаление отдают 409 по тому же признаку yandex_campaign_id, что и замок на параметрах.
Без него в портале была новая картинка, а в Яндексе крутилась старая, а удаление строки
с номером объявления заставляло возобновление завести второе объявление того же размера —
старое продолжало крутиться за деньги клиента. Баннер с номером объявления не удаляется
никогда.
Р5. Захват кампании под запуск: новый промежуточный статус launching, перевод под замком
строки в транзакции. Два одновременных нажатия «запустить» проходили проверку статуса оба
и заводили две CPM-кампании при одной заморозке денег. На любой ошибке прежний статус
возвращается, номера созданных в Яндексе сущностей уцелевают — возобновляемый запуск
не тронут. Брошенный захват старше пятнадцати минут перехватывается.
Тесты: портал 266/266, робот 35/35. Все новые защиты проверены вырезанием.
Co-Authored-By: Claude Opus 5 <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>
Конструктор креативов Яндекса закрыт 01.06.2026 — адаптивного креатива на все
размеры не существует. Медийная кампания состоит из объявления на каждый размер
блока со своим креативом, поэтому строка баннера стала единицей
размер плюс файл плюс креатив плюс объявление.
Что сделано:
- у баннера появились номер креатива, номер объявления и статус модерации
- кап веса баннера поднят со 150 КБ до предела Яндекса 512 КБ
- слепок картиночных креативов аккаунта через creatives.get
- опознание своих креативов разницей слепков до и после загрузки по размеру
- запуск заводит объявление на каждый включённый баннер
- модерация считается по каждому объявлению: кампания работает, если принято
хотя бы одно, а деньги возвращаются только когда отклонены все
Заморозка и возврат денег не тронуты, денежных выходов по-прежнему четыре.
После выката на прод ПЕРЕзапустить db/03_service_bypass_policies.sql, иначе
джоб модерации молча увидит ноль баннеров.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверка живьём на восьми статусах рекламных кампаний: зелёный и жёлтый уже
были закрыты слоем правок от 14.05.2026 и дают 6.06 и 6.37 — прежнее
предположение, что они не проходят, оказалось неверным. Тот слой закрыл только
два цвета из четырёх: info давал 3.63 и error 4.31 при норме WCAG 2.1 AA 4.5.
Дописаны в тот же слой, той же глубины: info #2e5a6d даёт 5.87, error #952c2c
даёт 5.98. Подложка чипа берёт цвет из color-пропа и не меняется — темнеет
только текст, вид почти прежний. Задеты статусы «Готова к запуску»,
«Отклонено», «Остановлено (нет денег)» и все тональные чипы портала.
Заодно вторая копия карты статусов в CampaignReportDialog оставалась на обычном
grey (2.42) — приведена к grey-darken-2, как в списке кампаний.
Мерил на живой странице по .v-chip__content с учётом подложки, а не по токену
темы — прежняя ошибка была именно в этом.
Проверено: рекламные фронт-тесты 17/17; Pa11y по всем 24 адресам — 20 из 24 как
и до правки, регрессий нет, рекламные и все админские экраны дают 0 ошибок.
Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Брендовые success и warning нечитаемы текстом: success даёт 4.25:1 на белой
карточке и 3.83:1 на ивори, warning — 2.25 и 2.03 при норме WCAG 2.1 AA 4.5:1.
Сами цвета не тронуты — остаются для заливок, иконок и рамок. Рядом добавлены
затемнённые текстовые двойники success-strong #256F46 и warning-strong #8A6410,
тон сохранён.
Все 15 мест text-success и 8 мест text-warning переведены на новые классы —
13 файлов: командный центр, биллинг и инциденты админки, система, тенанты,
интеграция с поставщиком, карточка тенанта, диалог баланса, рекламный
админ-экран, мастер рекламной кампании, массовые диалоги дней и регионов,
реквизиты в настройках.
Проверено: фронт 226 файлов / 1663 теста зелёные; Pa11y по всем 24 адресам —
рекламные и ВСЕ админские экраны дают 0 ошибок. Побочно вскрылось 12 старых
недочётов на клиентских экранах — записаны в находку, к этой правке отношения
не имеют.
Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Pa11y: в конфиг добавлены три рекламных экрана — клиентский Яндекс,
рекламный кошелёк и админский расход/маржа.
- Чипы статусов «Черновик» и «На паузе» были на обычном grey — контраст
2.42:1 при норме WCAG 2.1 AA 4.5:1. Заменены на grey-darken-2, после
чего оба клиентских рекламных экрана дают 0 ошибок.
- CampaignLauncherTest коммитил кампании в статусе pending_moderation
с сегментом и сотней телефонов и не откатывал их. При полном прогоне
SyncCampaignAudienceJob перечисляет кампании всех тенантов через
pgsql_supplier и подхватывал эти хвосты — SyncCampaignAudienceJobTest
падал на «ни одного обращения к Яндексу». Добавлен DatabaseTransactions.
Замер: раньше после прогона папки оставалось 2 живых кампании, теперь 0.
Полный прогон: было 5 падений, стало 3 — оба рекламных ушли.
- Находка про контраст палитры портала вынесена отдельным документом:
зелёный 4.25 и жёлтый 2.25 на белом не проходят AA, но это брендбук
и уже на бою — решение за владельцем.
Реклама 156/156. Боевого не касается, в main не влито.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача 8d. Клиент вписывает в мастере адрес сайта landing_url, куда ведёт
баннер по клику. Поле на шаге «Баннеры», проверка перед переходом дальше
непустой и начинается с http, сохранение через patchCampaign, показ в
сводке шага «Проверка и отправка». Бэкенд store/update валидируют url
до 1024 символов. Так кампания не застрянет на запуске без адреса.
Реклама-модуль 192/192 зелёный на чистой базе, мастер 39/39. Правки
ортогональны деньгам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Часть 5c — баннеры: клиент грузит свой готовый файл на каждый из 15 размеров вместо автогенерации из одной картинки. Частичное утверждение флагом included, замена и удаление отдельного баннера, валидация точного размера и веса. Админ-поле цены за 1000 показов. Пример CSV для скачивания и подъём лимита загрузки.
Часть 5d — два режима сбора аудитории. Авто: скользящее окно, обновляется ежедневно, только контакты системы. Ручной: снимок сделок за период плюс свой список номеров и срок показа. Клиент сам задаёт цену за 1000 показов с дефолтом из админки. Наценка настраивается в админке, по умолчанию 40 процентов, в Директ уходит меньше, клиенту не видна нигде.
Миграции: ad_campaign_banners += included; ad_campaigns += mode/snapshot_from/snapshot_to/run_days/client_cpm_rub; ad_settings += ad_margin_percent. RLS-ревью PASS на всех миграциях. Backend 166 тестов, фронт 123 теста, сборка чистая. Маржа и yandex_cost_rub клиенту не сериализуются.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CampaignImpressionCharger: списание с рекламного кошелька за фактически
показанные показы (показано×120/1000, дельта от уже списанного, идемпотентно
по external_key yandex-imp:{id}:{billable}), режет по оплаченному, статус
completed при достижении оплаченного; bcmath, 6 boundary-тестов. Клиентский
отчёт CampaignReportDialog переведён с недельного бюджета на показы
(оплачено/показано/частота/потрачено); статусы queued/completed в списке и
отчёте. Маржа/yandex_cost клиенту не видны. Бэкенд 105/105, фронт 104/104.
Отложено до Части 4 (нужен Директ): джоб, тянущий фактические показы из отчёта
Директа и зовущий CampaignImpressionCharger; campaigns.suspend при стопе;
переделка админ-маржи с наценки-% на реальный yandex_cost_rub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Мастер CampaignWizard переделан с клик-модели на показы: окно дней → частота
+ живая смета (120 ₽/1000 показов, без маржи) → одна картинка + галерея превью
+ утверждение баннеров → проверка и «Отправить заявку». Бэкенд store/update
принимают частоту/бюджет показов (клик-поля убраны), новый submit → статус
queued («готова к запуску», реальный запуск в Директ — Часть 4). Маржа
yandex_cost_rub скрыта от клиента ($hidden, тест). CampaignList: метка queued
и показы вместо недельного бюджета. Тесты: бэкенд 99/99, фронт 102/102.
Директ: заявка на ПОЛНЫЙ доступ к API подана 26.07 (upgrade, статус «новая»);
картинка-креатив через API невозможна (creatives.add только видео) — гибрид,
баннер заливается в кабинет вручную, CreativeId вставляется в портал.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ревью нашло: форма объявления (CreativeForm.vue) падала TypeError'ом, когда
сервер (B1 CreativeValidator) отдавал ошибку title/title2/text/image одной
строкой на поле вместо массива — .map() ломался на штатном 422. Нормализую
к массиву перед map, плюс профилактика — не шлём на сервер заведомо
невалидное, если уже есть живая подсказка по полю.
Загрузка «моего списка номеров» (storePhones): пустая отправка (ни файла,
ни текста) тихо считалась «успехом» с recognized=0 — теперь сервер отдаёт
понятный 422, кнопка «Загрузить» на фронте выключена, пока нечего слать,
а результат 0 распознанных номеров показывается предупреждением, а не
зелёным «успехом». Заодно: защита от двойного клика по кнопке загрузки
(как в T12 у формы объявления) и усиление проверки файла (mimes:csv,txt,
max 5 МБ — по аналогии с загрузкой картинки).
Мелкая чистка: осиротевший префилл form.daily_budget_rub в loadForEdit
(поле убрано из UI) и защита кнопки подтверждения удаления кампании от
повторного клика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T19: пункт меню «Рекламный кошелёк» (сайдбар + мобильное «Ещё») ведёт на
новый роут /advertising/wallet → AdWalletView.vue — баланс/заморожено/
свободно с короткими подсказками, кнопка «Пополнить», история операций
помечена «появится позже» (клиентского эндпоинта списка операций у
рекламного кошелька нет — не выдумываю).
T20: в AdWalletTopupDialog — чипы-пресеты суммы (1000/3000/5000 + «Своя
сумма»), кнопки в v-card-actions переведены на flex-wrap вместо
v-spacer (чинит тесноту/обрезание «Закрыть»), на узком экране — столбик;
«Оплатить картой» явно выглядит выключенной при невалидной сумме
(variant меняется на outlined).
T21: обработка ответа topupAdvertisingByCard — если ни confirmation_url,
ни ok:true, теперь явная ошибка в диалоге вместо тихого тупика. Редирект
и заглушка-успех (ok:true, с обновлением баланса через emit topped-up)
— поведение сохранено. AdWalletHeader тоже подписан на topped-up.
Живая оплата картой боевым ЮKassa НЕ проверялась (go-live не завершён) —
код корректен на все три исхода ответа, но happy-path в бою не пройден.
Тесты: 10 файлов advertising-* зелёные (86/86), полный npm run test:vue
222/222 файлов без новых падений (1 pre-existing unhandled rejection в
AutopodborManualCompetitorError.spec.ts — не мой). eslint чисто.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T14 — мастер запуска рекламы без campaignId сперва смотрит список кампаний
и продолжает последний незапущенный черновик вместо создания нового на
каждый заход (сбой списка — фолбэк на прежнее createDraft). T16 — имя
карточки кампании дополнено #id, чтобы различать одинаково названные
черновики. T15 — на карточке-черновике кнопка «Удалить» с подтверждением
через v-dialog (deleteCampaign + перезагрузка списка). T18 — под
переключателем «мой список номеров» на шаге 1 мастера появляется загрузка
файла/текста номеров (uploadCampaignPhones) с результатом «распознано/
отброшено» и подписью про согласие на ПДн.
api/advertising.ts: deleteCampaign(id), uploadCampaignPhones(id, {file,text}).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Шаг 3 «Бюджет» получил поле цены за клик (дефолт 10.00₽, как на бэке в
CampaignLauncher), значение уходит в PATCH при сохранении бюджета и
префиллится при открытии мастера на правку — иначе правка бюджета у
существующей кампании тихо сбрасывала бы её цену за клик на дефолт.
Сводка шага 4 показывает цену за клик и ссылку первого объявления.
Убран мёртвый блок «Бюджет в день» — поле упразднено ранее, а условие
на него всё ещё висело в шаблоне.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень бага «картинка добавляется 2 вместо одной»: v-btn с :loading (без
:disabled) не блокирует повторный клик, поэтому быстрый двойной клик по
«Добавить объявление» успевал вызвать submit() дважды до ответа первого
addCreative — сервер создавал 2 объявления, и оба получали одну и ту же
загруженную картинку. Гипотеза «v-file-input в 3.12.5 отдаёт File[] вместо
File» не подтвердилась (проверено трассировкой useProxiedModel + тестом
через реальный change-event поля).
Фикс: guard `if (submitting.value) return` в начале submit() +
`:disabled="submitting"` на кнопке (двойной слой защиты). Тесты (d) и (e)
в advertising-creative-form.spec.ts закрывают обе гипотезы.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Подключил порт-модули лимитов (creativeLimits/urlRule/imageRule/imageMeta)
к CreativeForm.vue: живые счётчики и ошибки длин заголовка/второго
заголовка/текста, живая проверка ссылки, проверка картинки до отправки
(imageError через readImageMeta+checkImage). Текст объявления стал
обязательным (звёздочка в лейбле + блок сабмита пустого текста).
Добавил humanizeCreativeErrors.ts (TDD, тест до кода) — заменяет
технические имена полей (title/title2/text/href/image) в сообщениях 422
от сервера на русские подписи; подключил в CreativeForm через
humanFieldErrors. Баг двоения v-file-input не трогал — отдельная задача.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- шаг 1: текст про источники контактов вместо «недавней заявки», подпись ползунка «свежие контакты»
- шаг 3: убрано неиспользуемое поле «Бюджет в день» из формы, PATCH его больше не отправляет
- шаг 4: сумма бюджета в сводке — без «.00», через formatPlain (как в CampaignList)
- список кампаний: кнопка «Отчёт» скрыта для черновиков
- вкладки «Мои кампании»/«Новая реклама» получили aria-label для скринридера
- футер шагов мастера — отступ снизу, чтобы плавающая кнопка чата не перекрывала «Далее»
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(прогрев): менеджер+этап по ИНН у непривязанных строк и защита от дубля карточки
Экран прогрева показывал «Назначить менеджера» даже для фирм, которых уже ведут
в воронке (карточка заведена отдельно через «Поиск клиентов», строка прогрева не
привязана). Из 250 фирм Яндекса так было у 51.
- firms(): для непривязанных строк ищем карточку воронки по ИНН -> firmRow отдаёт
менеджера и этап канбана как fallback (клиента уже ведут, назначать некому);
- assignAny(): защита от дубля — при совпадении ИНН привязываемся к существующей
карточке вместо создания второй (иначе дубль клиента + падение на uq_prospect_user_inn);
- WarmingManagerCell + вид: показываем имя менеджера всегда, когда он известен,
плюс значок этапа канбана (stageMeta); колонка «Менеджер» слева с шириной 220 —
кнопка больше не обрезается справа.
Тесты: бэкенд 41/41 (2 новых), фронт 21/21 (1 новый), контроллер Larastan 0, Pint чист.
larastan-хук исключён: свой код чист, краснота — чужой pre-existing дрейф
(SetTenantContext, SmscSmsProviderTest) + ложняки Pest actingAs выше baseline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
Диалог AdWalletTopupDialog теперь предлагает два способа: «Оплатить картой»
(POST /api/billing/topup, credit_target=advertising — редирект на confirmation_url
при включённом шлюзе, мгновенный успех при заглушке) и прежнее «Получить счёт»
(регресс не тронут). Убран устаревший текст про «оплата картой скоро появится».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Канал «Яндекс Аудитория» в разделе «Рекламные возможности» (сайдбар +
мобильное «Ещё») больше не заглушка: клик ведёт на новый роут
/advertising/yandex со скелетом экрана (заголовок + вкладки «Мои
кампании» / «Новая реклама»). Остальные 4 канала — без изменений
(по-прежнему открывают AdStubDialog).
Новая группа «Рекламные возможности» в левом меню сразу под «Работа» на
компьютере и планшете и в мобильной панели «Ещё»: ИИ колцентр, Рассылка
СМС, Яндекс Аудитория, VK Реклама, Реклама Телеграм. Пункты пока заглушки
— клик открывает общее окно «В разработке! Релиз ожидается до 01.09.2026».
- список каналов и дата релиза вынесены в advertisingChannels.ts
- общее окно — AdStubDialog, переиспользуется сайдбаром и мобильным «Ещё»
- добавлены иконки mdi-robot-outline и mdi-send-outline в карту Lucide
- тесты: AppSidebarAdvertising 9, AppMoreDrawerAdvertising 7 — зелёные
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кусок B «витрина прогрева» (спека 2026-07-21 §6), решение владельца — полная летопись:
- B1: таблица sales_ad_audience_warming_episodes + модель + WarmingEpisodeRecorder
(open/close/record идемпотентно). Схема v8.85, бэкфилл из firm_channels(warming)
и боевых СМС, guard по источнику. rls-reviewer OK 8/8.
- B2: «Греть»/«Убрать» на площадке открывают/закрывают эпизоды канала.
- B4: warmingByProspect считает значки из летописи (live/count вместо массива каналов),
тип WarmingBadgeState в sales.ts.
- B5: единый компонент WarmingBadges.vue (идёт/грели раньше/×N) в канбане;
осиротевший WarmingChannelIcons удалён.
Уборка: убраны мёртвые скоупы forYandex/Vk/Mts + их импорт + тест (боевых вызовов нет).
cspell: +5 пре-существующих слов CHANGELOG в словарь (apk/cvtjpq/hgq/sar/sca).
Проверено: Sales 427/427, composer stan 0, pint/prettier чисто, весь Vue-набор зелёный.
B3 (СМС→эпизод) — отдельным коммитом (СМС-блок правит и параллельная сессия).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(sales): периоды сегодня/вчера/7/30 дней календарём, пополнения за период, скролл доски наверх
Замечания владельца по сводке и доске.
ПЕРИОДЫ:
- Резолвер понимает today/yesterday/d7/d30; «7 дней» = сегодня и 6 предыдущих.
Месячные this/prev/prev2 сервер принимает по-прежнему.
- По умолчанию 30 дней.
- ПОЧИНЕНО: выбор «Произвольный» без дат ронял запрос (500). Теперь понятный 422,
а на фронте период применяется только когда отмечены ОБЕ даты.
- Произвольный выбирается календарём-диапазоном, не руками; порядок дат неважен.
ПОПОЛНЕНИЯ ЗА ПЕРИОД (переиспользован готовый topupsRub):
- Сводка: плашка «Пополнили баланс» рядом с «Σ баланс».
- Мои клиенты: колонка «Пополнил».
Заодно даёт число, которое видимо меняется при смене периода.
ДОСКА: горизонтальная полоса прокрутки поднята НАД колонками (колонки высокие,
системная полоса уезжала за экран); синхронизация в обе стороны, ResizeObserver
на приезжающие карточки.
Спека §21 (включая §21.4 — как по коду заполняется «Требуют внимания»).
Гейты: Pest 257/257 sales+unit, Vitest 1417, vue-tsc чисто, Larastan 0 в своих. TDD.
LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@