fdbff6e2d40e36c5abf97dfe7bbde69c05ee967c
237 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
337f6b9897 |
feat телеграм: дата старта показов, признак живости системы и человеческий язык ошибок
Дата старта (решение владельца 06.08.2026). Портал не передаёт кабинету МТС ни одной даты — их ставит кабинет своими умолчаниями. Живой прогон показал: старт оказался ЗАВТРАШНИМ, тогда как экран обещал клиенту показы «7 дней», подразумевая сегодня. Робот читает день начала из ТОЙ ЖЕ строки списка, куда и так ходит за вердиктом — ни одного лишнего захода в кабинет; портал хранит его в client_tg_campaigns.starts_on и показывает клиенту «Показы начнутся 7 августа». Проверено на ЖИВОМ кабинете: три задания подряд вернули startDate 2026-08-07 по кампании МТС 2237821. Мастер перестал молчать о том, что день начала ставит кабинет, а не мы. Ф-2, карточка приёмки Т-Ф4. У кампаний в движении видно «Проверяли 5 минут назад». Считаются только ЗАКОНЧЕННЫЕ проверки, включая неудачные: задание в очереди работой не является, а неудачная проверка — всё равно признак жизни. Именно в такой тишине владелец 36 часов не знал, что робот вообще не может войти в кабинет. Ф-3. Имена полей в ошибках формы по-русски: «Лимит на объявление не может быть меньше 1 ₽» вместо «Поле budget cap rub должно быть не меньше 1». Серая кнопка «Запустить» называет причину и шаг, куда вернуться, а не гаснет молча. Карточки Т-Р3 и Т-Р4 закрыты тестами (живьём не воспроизвести). Попутно найдено: обе защиты, стерегущие ЕДИНСТВЕННОЕ место траты живых денег роботом, были без единого теста. Сторожа доказаны вырезанием. Тесты: ClientTg 378 зелёных, экраны 2158, робот 197. Статанализ 0, стиль 0, типы чисто. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f83b845658 |
fix телеграм: экран разбора показывал смету вместо живых денег
Поймано на боевых данных 06.08.2026, ДО первого нажатия кнопки. В списке застрявших оказались три кампании, а не одна — и у двух из них заморозку уже отпустили, денег за ними не осталось. Экран же показывал смету и подписывал её «Заморожено у клиента: 268,80 ₽». Владелец нажал бы «вернуть деньги», не вернулось бы ничего, а он считал бы, что вернул. Обещать возврат того, чего нет, — то же враньё, что зелёная галочка над невыполненной работой. Теперь в списке идёт живая заморозка, посчитанная одним запросом на весь список. Когда её нет — так и написано, и кнопка меняет обещание на «просто закрыть кампанию». Отдельно предупреждаем, когда списание пойдёт прямо с баланса клиента, а не из отложенного: у кампании №10 на боевом ровно этот случай — кабинету уплачено 180 ₽, а заморозки уже нет. Проверено: модуль 355 тестов, экраны 16 тестов, статанализ 0, типы чисты. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
13ce301ab3 |
feat телеграм: разбор застрявших кампаний и человеческий язык отказов
Кампания, брошенная роботом на полпути, попадала в needs_review или draft_ready — статусы, из которых не вело ни одного перехода. Замороженные деньги клиента запирались навсегда, снять их мог только программист правкой боевой базы. В бою 06.08.2026 так заперло 268,80 ₽ по кампании №14. Теперь в админке есть карточка «Застрявшие кампании»: владелец видит номер кампании в кабинете МТС, запертую сумму и уже уплаченную МТС сумму — и решает сам. Кнопка «списать по факту» показывается ТОЛЬКО когда МТС уже уплачено; иначе списывать было бы нечего, кроме сметы — ровно та беда, ради которой заморозку и заводили. Заодно: клиенту больше не показывают внутренние адреса и команды запуска — причина отказа переводится на человеческий язык. Портал перестал принимать пустой отчёт робота как успешный. Проверено: модуль 354 теста, экраны 13 тестов, статанализ 0 ошибок. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a709efdee4 |
feat(кошелёк): две колонки, период, разрез по кампаниям, расшифровка заморозки
Владелец 06.08: "кошелёк убогий и неинформативный". Отметил пять слабых мест из шести. Не отметил только прогноз "надолго ли хватит" - он пополняет по факту, а не по остатку. Что теперь на экране: - слева липкая колонка денег: свободно крупно, и под ней РАСШИФРОВКА заморозки - какая кампания, сколько заперто, когда вернётся. Раньше стояло голое "Заморожено 2 507 руб" без единого слова, под что; - период: сегодня / вчера / 7 дней / 30 дней / свои даты, живёт в адресе страницы; - столбики расхода по дням, цвет по каналу, своим CSS без библиотеки; - таблица "Куда ушли" - строка на кампанию с суммой и долей, клик отбирает ленту; - закладки по каналам сохранены, но теперь считаются за выбранный период; - лента разбита по дням с итогом за день и постраничной догрузкой. Сервер: две новые сборки RaskryitieZamorozki и OtchyotKoshelka, новая ручка /api/advertising/wallet/report, лента получила отбор по датам, каналу и кампании и листание по ключу вместо смещения. Расчёт списаний, заморозки и возврата по сроку НЕ тронут - правка только про показ. Найдено по дороге и закрыто: - чтение отчёта обёрнуто контекстом построчной защиты. Без этого на боевом запрос вернул бы НОЛЬ строк молча: местная база под суперпользователем защиту обходит, и мы бы увидели это только у клиента; - сторож на число запросов мог зеленеть БЕЗ самой ручки - на любой неизвестный путь портал отвечает страницей с кодом 200. Усилен и проверен вырезанием маршрута; - общий форматтер срезал хвостовой ноль: "833,50" превращалось в "833,5". На это независимо наступили три правки подряд, каждая завела свою копию. Сведено в formatExact. Границы суток и группировка по дням считаются ПО МОСКВЕ, а не по Гринвичу. Сторож взят с живого боевого: запись 69, 2026-08-05 21:00:24 UTC - для человека это 6 августа. Наивные версии всех трёх мест были написаны нарочно и увидены красными, прежде чем чинились. Старые сторожа не выброшены, а перенесены: закладки по каналам переехали в отчёт вместе с проверкой на 120 строк, отбор ленты по каналу - в проверку ленты. Каждый переезд назван поимённо. Проверено: 461 сторож сервера, 2135 сторожей экрана, контроль типов ноль, статанализ ноль, форматтер чисто, сборка фронта проходит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4a993f8575 |
feat(кошелёк): видно, за что списали и сколько уходит на каждый канал
Три правки по замечаниям владельца от 06.08.2026. 1. Кошелёк переехал из «Рекламных возможностей» в «Финансы», рядом с «Биллингом» — это деньги, а не рекламный канал. Перенесён в обоих меню: боковая панель и мобильное «Ещё». 2. Каждое списание теперь называет канал, номер и название кампании: «Яндекс Аудитория · кампания №21 «Я4 приёмка — впритык…»». Раньше все до одного писались немой фразой «Списание за рекламу (факт)». Подпись собирается при показе, поэтому заговорили и уже записанные строки — летопись задним числом не переписывается, это финансовый документ. Попутно закрыта ловушка: номера кампаний у Яндекса и у СМС идут по разным счётчикам и совпадают. В боевой летописи есть и «кампания №4» от СМС, и рекламные — различить их было нечем. Теперь разводит канал в подписи. Поправлен падеж: было «Заморозка снята — кампанию №13». 3. Закладки по каналам с суммой трат прямо на закладке: Всё 157,74, Яндекс Аудитория 130,74, Рассылка СМС 27,00. Тратой считается только списание — заморозку ещё можно вернуть, что и случилось с 724 руб. Суммы считаются по ВСЕЙ летописи, а не по сотне отдаваемых строк: иначе у давнего канала цифра тихо усохла бы при переполнении ленты. Отдельный сторож создаёт 120 свежих движений и проверяет, что давние не пропали. Сторожа: 6 на сервере, 5 на экране, каждый сначала увиден красным. Рекламный блок 441 зелёный, фронт 2108 зелёных, статанализ ноль, типы чистые. |
||
|
|
1f260c0b97 |
fix/обзвон: честный отчёт о стирании доходит до оператора - З-2.5, второй заход
Приёмка вскрыла, что первый заход воспроизвёл ту же беду на другом основании. 1. Невод по тексту не мог найти НИЧЕГО по построению. `obzvon_materialy_klienta.transcript` не пишет никто: расшифровщик живёт в З-4.2, волна 4. Значит сводка честно говорила "материалы клиента=0", пока голос человека лежал на диске 30 дней. Теперь портал считает отдельным числом записи, которые проверить НЕЧЕМ - звук жив, расшифровки нет и не было, - и говорит это тревожной строкой. 🔴 Условие уточнено против предложенного: добавлено transcript_deleted_at IS NULL. Без него строка, у которой расшифровку стёрли мы сами, а файл убрать не смогли, попадала бы в оба числа сразу. Доказано надрезом. 2. Изоляция клиентов не сторожилась ничем. Соединение обходит защиту строк, значит отбор по клиенту в коде - ЕДИНСТВЕННЫЙ замок. Сняв его, приёмщик оставил все 13 сторожей зелёными. Заведены сторожа на все три таблицы. 3. Своя находка того же класса: читатель флага сверял телефон точными написаниями с колонкой, которую заполняет ЧЕЛОВЕК руками. На записи "8 (900) 123-45-67" он молча отвечал "звонить можно" тому, кто потребовал прекратить обработку. Сверка переведена на хвост из десяти цифр - и там, и в переходнике. 4. Экран админки показывал зелёную галочку "выполнено" и пустое поле "Webhook-логов", а про обзвон и про нестёртые файлы молчал. Это видимая половина той же неправды: тот, кто жмёт кнопку, и есть тот, кто обязан пойти проверить руками. Экран называет обзвон поимённо, при нестёртых файлах и непроверяемых записях галочки нет вовсе - вместо неё тревога. Мёртвое поле убрано вместе с полем в типе ответа. Сторожа: 13 -> 19 на сервере, плюс 5 экранных. Каждый показан красным семью надрезами по коду. Отчёт: docs/superpowers/priyomka/stroyka-2/z-2-5-otchyot-2026-08-05.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
80987996ed |
fix(реклама): мастер не врёт про показы, остановка без денег платит за показанное
Три починки, каждая сперва увидена красной. 1. Мастер обещал показы как факт: «~8 150». Живой замер 04.08 по кабинету: чужая кампания с бюджетом 13 000 руб за четверо суток набрала 457 показов и потратила 138 руб — деньги не кончились, кончились люди. Число из мастера это потолок, до которого кампания почти наверняка не дотянет. Стало «не больше 8 150» плюс объяснение: упрётся в список, а не в деньги; за несостоявшиеся показы деньги вернутся. Поправлено на шаге частоты и в сводке перед отправкой. 2. Остановка «нет денег» возвращала заморозку, НЕ заплатив за уже показанное. Списание делает часовая задача, а она берёт только кампании со статусом «крутится» — остановленную пропускала навсегда. Показы последнего часа уходили клиенту даром, а Яндексу за них платили мы. Та же дыра, что чинили в паузе, только через другую дверь. Теперь: сперва заплати, потом отпускай; не узнал число показов — не отпускай вовсе. 3. Остановка ходит в Директ по два раза на каждую кампанию, и делала это ВНУТРИ денежной транзакции — замок строки висел всё время сетевых запросов. Вынесено наружу: денежная операция закрывается, и только потом остановка. Плюс минимум площадки в мастере. Директ не берёт кампанию дешевле 300 руб за каждый календарный день и отвечает по-английски на последнем шаге, когда клиент уже пятнадцать часов собирал аудиторию. Теперь мастер предупреждает заранее и по-русски. Формула вынесена в YandexMinimumSpend и одна на портал: ею пользуются и запуск, и мастер — две копии однажды разошлись бы. Сторожа: 7 новых на бэкенде и фронте, каждый принят красным. Прогоны: реклама 458 тестов зелёные, Larastan 0, vue-tsc чисто. |
||
|
|
04657bd6cd |
feat(воронка): фильтр по нише и на личном экране менеджера
Раньше «Ниша» стояла только на «Воронке отдела». Теперь тот же фильтр есть и на личном экране менеджера — он обзванивает подряд одну нишу, ему нужнее всех. Список ниш у менеджера считается по ЕГО карточкам: сужение «только свои» наложено раньше, поэтому чужая ниша в список не попадает, а ?rubric= не может стать лазейкой к чужой воронке — на это есть отдельная проверка. Поведение обоих экранов задаёт один кусок кода, composables/prospectRubricFilter.ts. Двумя копиями «Без ниши последним» и самосброс исчезнувшей ниши разъехались бы на первой же правке. Проверено: Pest по продажам 525 зелёных, Vitest по фронту 2077 зелёных, vue-tsc и Larastan чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fcf4331d24 |
fix(реклама): карточка объясняет, что происходит с объявлениями
Владелец, глядя на две живые карточки: «статус на модерации — не понятно, идут показы или ожидает! и принято, когда реально отклонили». Корень: ярлык один на всю кампанию, а объявления внутри в разных состояниях, и у объявления два независимых признака — прошло проверку и показывается. Карточка сваливала их в одно слово. Три починки: 1. Портал спрашивает Яндекс о модерации каждые 15 минут вместо двух часов. У кампании #11 проверка кончилась между обходами, и клиент полтора часа видел бы «На модерации» на уже работающей кампании. Сторож держит расписание */15. 2. Портал считает придержанные объявления — одобренные, показ которых Яндекс не включил. Признак уже ставился на объявление, но наверх не поднимался; теперь в списке есть banners_held. Два сторожа: слепок боевой #6 даёт 13, слепок #11 даёт 0. 3. Ярлык называет положение дел словами, приписка всегда даёт числа: «Яндекс проверяет объявления» + «Одобрено 9 из 15, остальные ещё проверяются»; «Одобрено, но показ не включён» + «Одобрено 13 из 15, отклонено 2. Яндекс их пока не показывает — причина в сообщениях кампании»; «Крутится» + «Одобрено 15 из 15». Числа показываются всегда, а не только при частичном отказе. Живой замер, ради которого всё это: у боевой кампании #6 все 15 объявлений выключены при 13 одобренных — Яндекс просит документы, поэтому показов нет с 30.07. Карточка про это молчала. Прогон: 386 сторожей блока рекламы, 1974 фронта, статанализ 0, стиль чист. Схема не менялась. Тесты гонялись на отдельной базе liderra_testing_s19 — общую соседняя смена сносила трижды за прогон. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4b8c4cc729 | feat(воронка): плашка ниши на карточке и фильтр по нише на доске | ||
|
|
656cf80f31 |
feat(реклама): кнопка «Запустить» у клиента на карточке кампании
Кампания в состоянии «Готова к запуску» не двигалась ничем — ни человеком, ни расписанием. Клиент собирал рекламу, заливал пятнадцать картинок, отправлял заявку и упирался в тупик: на карточке только «Изменить» и «Отчёт». Запуск был возможен лишь обращением, к которому в кабинете нет кнопки. Поймано приёмкой 03.08.2026 на кампании #11. Новых дыр кнопка не открывает: путь запуска и так лежал в клиентской группе, все проверки — деньги, минимум площадки, годность аудитории — внутри самого запуска. Три ответа портала показываются по-разному: «готовим картинки» и «аудитория ещё готовится» — спокойным синим сообщением, это нормальный ход, деньги не тронуты и в кабинете ничего не создано; смета мала, не хватает денег, Яндекс молчит — красным. Денежная плашка перечитывается только после удачного запуска. Отдельного «вы уверены?» намеренно нет: кнопка подписана, смета показов стоит тут же на карточке, пауза возвращает заморозку целиком — проверено живьём 03.08 на кампании #6. Вместо окна под кнопкой строчка «При запуске 1 020,60 руб. будут зарезервированы на кошельке»: клиент знает про деньги до нажатия, а не узнаёт потом из кошелька. Сторожа: кнопка есть только у «Готовы к запуску» и нет у работающей и у черновика; удачный запуск перечитывает список и денежную плашку; «аудитория ещё готовится» даёт спокойное сообщение и не тревожит плашку; отказ даёт красное; под кнопкой сказано про резерв сметы. Прогон: 2049 сторожей фронта зелёные, типы Vue чисты, стиль чист. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
447ad6af49 |
fix(рекламный кошелёк): клиент видит, за что заперты его деньги
Приёмка 03.08.2026 на боевом нашла три дыры вокруг одной и той же суммы: у клиента заморожены 3 333,36 ₽, и узнать за что было негде. Деньги не терялись — терялось объяснение, а для клиента, у которого заперта треть кошелька, это неотличимо от пропажи. 1) Денежная плашка врала после паузы и возобновления (Я-Ф1/Я-Ф4 FAIL). Она грузилась ОДИН раз, при открытии страницы, и о переменах не узнавала. После «Возобновить» экран обещал 9 991 ₽ свободных, когда на деле оставалось 6 657,64 ₽: клиент считал новую кампанию на несуществующие деньги и получал отказ, которого не ждал. Правда появлялась только после перезагрузки. Стало: список кампаний сообщает «деньги подвинулись», экран перечитывает плашку. Сигнал шлют ровно те три действия, что двигают деньги: запуск, пауза, возобновление. Сторож держит не содержимое плашки (оно зелено и при поломке), а сам факт повторного обращения за кошельком после денежного действия. 2) Летопись не фиксировала ни постановку заморозки, ни снятие. Заморозка писалась суммой 0,00 ₽ — жёстко зашитой, снятие не писалось вовсе. Стало: заморозка пишется настоящей суммой со знаком минус (деньги ушли из свободных, баланс не двигается), снятие — отдельной строкой типа release. Подпись человеческая и с номером: «Заморозили под кампанию №6». 3) На экране кошелька вместо истории стояло «появится позже». Стало: живая история движений — новая ручка GET /api/advertising/wallet/transactions (последние 100, свежие сверху, чужие не отдаёт) и лента на экране: подпись человеческими словами, сумма со знаком, дата по Москве. У заморозки и её снятия отдельная пометка «баланс не изменился», чтобы клиент не принял резерв за списание. 4) Карточка кампании называла не ту сумму. Показывалось поле budget_rub — число, которое клиент когда-то вписал сам и которое в расчёте денег не участвует вообще: «Бюджет показов 200 ₽» при заморозке 3 333,36 ₽. Стало: «Смета показов» — считанная сервером (показы × цена за 1000, та же арифметика, что и у заморозки), плюс строка «Зарезервировано» с живой бронёй по этой кампании. Схема не менялась — все поля летописи уже были. Проверено: 375 сторожей рекламы, 748 по смежным кошелькам (телеграм и СМС ходят тем же сервисом), 2042 на фронте — все зелёные. Статанализ 0 ошибок, типы Vue чисты, стиль в изменённых файлах чист. Все четыре сторожа приняты вырезанием: на старом коде красные. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4a439ebb80 |
merge: правка телеграм-смены от 01:59 сведена — на бою снова обе работы
Вторая смена выложила на боевой свою свежую правку в 01:55–02:00, поверх выката
СМС-модуля. Их выкладка заменила сборку экранов целиком, а СМС-модуля в их ветке
нет — в результате экраны клиентских СМС на бою пропали из сборки, хотя код,
маршруты и таблицы остались целы. Их телеграм-правка при этом работала.
Сверка байт в байт показала: расходились ровно 5 файлов, все из их записи
|
||
|
|
f2ac3f4c7f |
fix,телеграм: подсказка обещала цену вдвое ниже настоящей + образец файла для базы номеров
Приёмка владельца на боевом, два замечания из трёх (третье — про рекламный кошелёк и заморозку — отложено, кусок большой). 1. Подсказка «?» у поля медиа обещала «600 ₽ за тысячу с картинкой, 680 ₽ с видео». Клиент платит 1008 и 1142,40 ₽. Числа были вбиты в текст руками и протухли в ту минуту, когда миграция client_tg_cena_po_media поменяла тариф. Лечение в корень, а не подстановкой верных чисел: цену называет тот, кто её считает. Ручка оценки отдаёт ceny_za_tysyachu по каждому виду медиа (себестоимость × наценка), подсказка собирается из них функцией podskazkaProMedia. Пока сервер не ответил — текст без единой цифры: подставить «примерные» числа значило бы вернуть ровно эту беду. Прайс держим отдельно от охвата: охват на ошибке гасим (устаревший хуже никакого), а цены от настроек аудитории не зависят — иначе подсказка мигала бы на каждой опечатке в поле. 2. Замечание дословно: «нету скачать файл с примером как надо заполнить для нас файл». Кнопка «Скачать образец» рядом с полем загрузки — подпись клиент читает уже ПОСЛЕ того, как файл отклонили. Пять строк, написания разные (с плюсом, с восьмёркой, со скобками), номера синтетические 7999. Заголовка-строки в образце намеренно нет: разборщик нормализует первый столбец КАЖДОЙ строки, и слово «Телефон» попало бы в «не похоже на номер» — наш собственный образец показал бы клиенту ошибку. Проверено вырезанием, а не только зелёным: - вернул в подсказку вбитые 600/680 — покраснели 4 датчика, включая тот, что прямо запрещает эти два числа; - вставил в образец строку-заголовок — покраснели 3. Полный прогон поймал две мои же поломки, обе настоящие: - значок mdi-file-download-outline на новой кнопке ОТСУТСТВОВАЛ в карте Lucide — на экране стал бы вопросом в кружке. Поймал сторож значков, у которого вчера опустошили список поблажек. Добавлен (Download — точного «файла со стрелкой» в Lucide нет); - датчик подсказок ждал PODSKAZKI.media строкой, а её больше нет. Замеры: экраны 245 файлов / 1870 тестов / 0 падений; телеграм-модуль 327 / 0; статанализ 0; формат чисто; типы 6 — все в чужих файлах, столько же было до; сборка 3,80 с. Счётчик в phpstan-baseline сдвинут 13→15 (новые тесты на Pest), diff проверен глазами: изменилась ровно эта строка. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7ad9e0a66 |
merge: телеграм-реклама второй смены сведена в рабочую ветку перед выкатом СМС
Замер боевого перед выкатом СМС-модуля показал мину: на liderra.ru сейчас работает код ветки fix/tg-zagolovok-obyavleniya — 33 записи телеграм-рекламы, которых не было ни в main, ни в рабочей ветке. Сверено слепками файлов: config/client_tg.php и TelegramTariffService.php на бою совпадают с их версией и расходятся с нашей. Экраны портала собираются одним куском, поэтому выкат СМС в прежнем виде откатил бы их живую рекламу назад. Решение владельца — сперва забрать их работу к себе. Разрешено пять склеек, все — сохранением обеих сторон: - Tariff.php: их пояснение про закупочную цену плюс наша строка для подсказчика типов; - phpstan-baseline.neon: взята наша вычищенная версия. Их 260 строк заметания не возвращены — статанализ после сведения дал 0 без них, потому что чинили мы не baseline, а сам механизм, и он вылечил их новые тесты тоже; - advertising-telegram-view.spec.ts: наш типизированный мок оставлен; - CHANGELOG_schema.md: столкновения номеров НЕ было — у нас v9.33/v9.34, у них v9.64/v9.65. Обе пары сохранены, ничего не двигали; в файл дописано, что дыра v9.35–v9.63 это след их завышенного замера, а не потерянные записи; - STATUS.md: служебный файл наблюдателя, взят свежий. Что сведение вскрыло дополнительно: - их сторож значков поймал НАШИ три имени с экранов СМС — mdi-file-sign, mdi-account-badge-outline, mdi-file-download-outline в карте Lucide отсутствовали и на бою рисовались бы вопросом в кружке. Добавлены по смыслу; - проверка типов фронта: 1 ошибка стала 0. Образец имени отправителя в тесте админки не задавал два поля, и Partial подмешивал в них undefined. Проверено после сведения: PHP 4614 тестов, 4610 зелёных, 4 пропущено, 0 падений; экраны 250 файлов, 2018 зелёных, 3 пропущено, 0 падений; статанализ 0 через composer stan; проверка типов 0. Унаследованное, не мной внесённое и не тронутое: линтер фронта показывает 1 замечание на неразрывный пробел в advertising-sms-view.spec.ts — знак там нужен по смыслу проверки, было до сведения. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
36f5093692 |
merge: общая ветка с клиентским СМС-модулем сведена в рабочую
Общая ветка переведена вперёд на ветку клиентских СМС — перемоткой, без слияния, поэтому конфликтов там быть не могло. Затем общая сведена в рабочую ветку, которая отставала на 79 записей. Столкновение было одно и знакомое — словарь орфографии cspell-words.txt. Разрешено правилом «обе стороны настоящие»: слова обеих веток сохранены, ничего не выброшено. Журнал схемы БД на этот раз свёлся сам, столкновения номеров не было. СТОРОЖ ПДн ОСТАНОВИЛ ЗАПИСЬ И БЫЛ ПРАВ. В образце базы номеров, который клиент скачивает перед своей первой рассылкой, стоял рабочий телефон владельца — в двух видах. Это не утечка чужих данных: тот же номер публично опубликован в реквизитах ИП по требованию ЮKassa. Но клиент заполняет этот файл своими номерами и запускает рассылку — забытая строка означала бы СМС владельцу за деньги клиента. Заменён на выдуманный из тестового диапазона. Подсказки на экране рассылок («+7 999 123-45-67») выдуманы изначально; разрешены в .gitleaks.toml ПО ЗНАЧЕНИЮ, а не по файлу, чтобы сами файлы остались под охраной. Сторож проверен вырезанием: подложенный номер, не подпадающий ни под одно разрешение, он поймал; после снятия подложки — чисто. Служебный счётчик наблюдателя убран в тайник на время сведения и возвращён после — он машинный и пересоздаётся хуками. Файлы второй смены, которая работает в этой же папке, не тронуты и в слияние не попали. Проверки сведённой ветки: тесты 4533, зелёных 4529, падений нет; экраны 238 файлов и 1906 зелёных, сторож сети не сработал ни разу; статанализ 0; формат чист. |
||
|
|
00cc072e2f |
feat,телеграм-реклама: «Моя база номеров» заработала — пункт больше не ведёт в никуда
Это снимает запрет на выкат телеграм-рекламы. Пункт «Моя база номеров» стоял в выборе аудитории с 27.07, а писать в таблицу client_tg_contacts не умела НИ ОДНА строка кода: ни экрана загрузки, ни серверной ручки, ни переноса из сделок. У любого клиента пункт всегда показывал ноль. Клиент выбрал бы «свою базу», увидел пустоту и решил, что портал потерял его клиентов. Песочница выключена, деньги живые — катить в таком виде было нельзя. Читающая половина при этом была готова с самого начала: TelegramAudienceService умеет и нормализацию, и схлопывание дублей, и вычитание стоп-листа. Не хватало только записи, поэтому работа вышла куда меньше, чем казалось. Сервер: - TelegramBazaService — разбор файла, замена базы, очистка. Номер берётся по одному в строке либо первым столбцом таблицы, разделители запятая, точка с запятой, табуляция. Мусорные строки не роняют разбор, а считаются отдельно. - три ручки: GET, POST и DELETE /api/telegram/contacts. - потолок 200 000 номеров и 10 МБ на файл; обрыв по потолку не молчит, а возвращается признаком. Экран, в шаге «Кому показываем»: - сколько номеров в базе, заливка файла, очистка; - итог заливки целиком: принято, повторов, не похоже на номер. «Принято 1200» без остального читалось бы как «файл зашёл полностью»; - после заливки счётчик охвата пересчитывается сам. Решения, которые стоит знать: - заливка ЗАМЕНЯЕТ базу, а не добавляет. Так предсказуемее: клиент держит базу у себя и заливает заново. Подмешивание копило бы номера, от которых он не смог бы избавиться — построчного удаления в интерфейсе нет. На экране это написано до нажатия, молчаливой потери нет. - файл без единого годного номера отклоняется, старая база остаётся цела. Иначе клиент залил бы файл не того формата и потерял всё. - замена сделана удалением и вставкой, а не upsert: право UPDATE на таблице не выдано, upsert упёрся бы в это на бою и молча правил бы ноль строк. - наружу отдаём только счётчики. Номера — персональные данные, экрану они не нужны и в ответах не появляются. Приёмка: 12 тестов сервера и 9 тестов экрана, все до кода и все красные по верной причине — сервер отвечал 405, блока на экране не было. Замеры: телеграм-модуль 325 тестов 0 падений, экраны 244 файла 1855 тестов 0 падений, статанализ 0, форматтер чисто. Проверка типов 6 ошибок, все чужие, столько же было до работы. Для выката, проверить на бою: право USAGE на счётчике client_tg_contacts_id_seq. Замерил в тестовой базе — счётчик без права, но ровно так же выглядит и счётчик client_tg_campaigns_id_seq, в который портал на бою пишет. То есть новых прав эта работа не требует, но проверка дешёвая, а пропущенный grant на бою даёт отказ, которого на dev не видно. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3864f6f11a |
feat,телеграм-реклама: цена показа по виду медиа вместо ступеней по объёму
Мы продавали дешевле, чем покупали. Тариф давал скидку за объём — 0.45 → 0.36 ₽ за показ, — а у МТС такой скидки нет: прайс кабинета, стр. 13, берёт 0,48 ₽ за показ плоско. Скидку давали мы, а нам её не давал никто: на объёме от 50 000 наценка 1.40 превращалась в 5%. Второе. Кабинет показывает CPM БЕЗ НДС — колонка списка так и названа. Счёт кампании 2231134 сошёлся: 420 × 400 ₽/1000 × 1,2 = 201,60 ₽. Мы ИП на УСН, НДС не возмещается, это расход. Себестоимость с НДС: 0.480 без медиа, 0.720 с картинкой, 0.816 с видео. Третье. Вид медиа до расчёта не доходил вовсе — объявление с видео продавалось по цене объявления без картинки. На видео уходили в минус до 176 ₽ с тысячи, и портал нигде свою цену с ценой МТС не сравнивал. Песочница выключена с 02.08 — деньги живые. Что сделано: - миграция client_tg_cena_po_media: min_qty → media_kind, точность цены 6,2 → 6,3, три строки вместо пяти ступеней, media_kind на кампании и авто-правиле; - вид медиа определяется по СОДЕРЖИМОМУ файла, а не по расширению имени; - экран админки переделан: три фиксированные строки по видам медиа, рядом цена за тысячу для сверки с кабинетом, добавлять и удалять нечего; - запись v9.65 в журнале схемы. Замеры этой смены: телеграм-модуль 313 тестов 0 падений, экраны 242 файла 1841 тест 0 падений, статанализ 0, форматтер чисто. Проверка типов — 6 ошибок, все в чужих файлах, были до нас. Тест экрана тарифов проверен вырезанием, а не только зелёным: сломал передачу media_kind — красный; сломал расчёт цены за тысячу — красный. Осталось незакрытым: числа 0.720 и 0.816 — вывод по правилу «кабинет пишет без НДС», а не замер. Счёт кампании с картинкой живьём не снимали, шаг «Стоимость» недостижим без загрузки живых номеров. Замерите — правится одной строкой. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1052c64c6d |
chore,реклама: мёртвое поле недельного бюджета убрано с фронта
Поле weekly_budget_rub объявлялось в описании ответа Campaign и стояло в 21 заготовке пяти файлов тестов интерфейса, но не читалось нигде — сервер принимает и правит кампанию по budget_rub. Поправка к прежней записи: утверждение «сервер его не возвращает» было неточным. Список кампаний отдаётся узким набором столбцов без него, но создание и правка возвращают всю строку целиком — поле приходит, всегда пустое. Мёртвым оно было именно на фронте. Серверная сторона намеренно не тронута: столбец ad_campaigns.weekly_budget_rub в базе существует и его заполняют около шестидесяти мест в tests/Feature, которые пишут в базу напрямую. Там поле настоящее, снос столбца — отдельная работа с миграцией. Приёмка: на фронте не осталось ни одного упоминания, проверка типов 0, линтер 0, полный набор интерфейса 233 файла / 1750 тестов / 3 пропущено / 0 падений. Трём файлам вернул форматирование, которое сбилось от удаления строк. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
76cb44b91d |
feat(телеграм-реклама): пачка 4б — мастер шагов, живой счётчик охвата и серверная ручка оценки
Замечание владельца: «мастер шагов вместо простыни» и «живой счётчик охвата вместо кнопки Рассчитать». Форма была одна длинная, охват узнавался только по нажатию кнопки. Экран: - мастер из четырёх шагов: Кому показываем → Объявление → Деньги → Проверка; - живой счётчик охвата на первом шаге, пересчитывается сам при смене источника людей, периода или списка номеров (с задержкой, чтобы не дёргать сервер на каждую букву); - кнопки «Рассчитать» больше нет: «Запустить» создаёт кампанию, прикладывает картинку и отправляет в кабинет одним нажатием; - на шаге «Деньги» рядом смета и остаток баланса — нехватка денег видна ДО запуска, а не отказом после; - сводка на последнем шаге повторяет всё выбранное. Сервер — новая ручка POST /api/telegram/campaigns/estimate: 🔴 Живой счётчик обязан считать на каждое изменение поля, а охват до сих пор возвращала только POST /campaigns — и она СОЗДАЁТ черновик в базе. Через неё счётчик наплодил бы десятки кампаний-призраков в списке клиента. Новая ручка только читает: ни записи, ни денег, ни робота. На это стоит отдельный тест. Ручка считает ТЕМ ЖЕ кодом, что и создание кампании (общее тело выборки из сделок вынесено на оба входа TelegramAudienceService). Отдельный тест сверяет два ответа: разойдись они — клиент видел бы на экране одно число, а платил по другому. 🔴 Запуск запрещён, когда людей меньше 367 — правило площадки, не наше. Тот же предохранитель стоит на сервере (AudienceGateTest); на экране он объясняет заранее, вместо отказа после оплаты. Новых прав в базе ручка не требует: читает строго то же, что уже читает создание кампании, и не пишет никуда. Тесты: +EstimateApiTest (11 на PHP), +telegram-master-shagov (27 на экранах). 14 прежних проверок формы переехали в набор мастера вместе с кодом — сперва переписаны на новом месте и там проверены, потом убраны со старого. Устарели по смыслу только две («Рассчитать создаёт черновик», «правка сбрасывает смету»): считать вручную больше нечего, устареть расчёту негде. Замер: экраны 240 файлов, 1829 тестов, 0 падений (было 239/1813); телеграм- модуль на PHP 292 теста, 0 падений (было 281); статанализ 0 (базовая линия пересобрана — добавлен только новый Pest-файл, удалений нет); типы 6 чужих ошибок вместо 7; pint чист. На боевой НЕ выкачено. Глазами не принято — приёмка в пачке 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0af5c5696f |
chore+fix: разметка документации и «неизвестный тип» в интерфейсе убраны в ноль
РАЗМЕТКА ДОКУМЕНТАЦИИ. Проверка сторожа давала 237 замечаний в 21 файле — стало
0 во всех файлах под учётом git. Пустые строки вокруг списков и заголовков
поправлены автоматом в 12 файлах, два файла пришлось делать руками, потому что
автоправка там навредила:
- в описании починки инцидента номера шагов 1…7 — это ИМЕНА, на них ссылается
сам текст «из шага 2»; автоправка перенумеровала их в 1,1,1,1,2 и сломала
ссылки. Вывел ровно этот файл из-под одного правила, объяснение — в самом
файле, остальные правила действуют.
- в протоколе архитектуры автоправка склеила «R-* / DR-*» в «R-*/ DR-*».
Вместо этого обозначения взяты в обратные кавычки — смысл сохранён.
Семь файлов не тронуты: они вне учёта git — два в папке второй смены и старые
черновики в корне.
ИНТЕРФЕЙС. «Неизвестный тип» убран целиком: было 123 замечания, стало 0.
- Навигация автоподбора описана ОДИН раз — app/resources/js/views/autopodbor/nav.ts.
Раньше девять экранов повторяли описание с «неизвестным типом», а ещё три
описывали его каждый по-своему и не полностью.
- Чтение ошибки от сервера собрано в один помощник — app/resources/js/api/errors.ts,
с проверками. Он намеренно читает ФОРМУ ответа, а не полагается на axios:
тесты экранов подсовывают ошибку простым объектом той же формы, и экран обязан
вести себя в тестах так же, как в бою.
- В тестах вместо «неизвестного типа» — именованный доступ exposed<T> и полные
заготовки: app/tests/Frontend/support/. Каждый тест теперь объявляет ровно те
поля, до которых дотягивается, и опечатка в имени снова становится ошибкой,
а не молчаливым undefined. Заодно сняты 14 построчных отключений правила.
- Разведены задвоенные имена в шаблонах: в таблице сделок в одном шаблоне жили
ДВЕ разные функции isSelected — одна берёт номер сделки, другая строку таблицы.
- Убрана мёртвая функция dirLabel и три неиспользуемые переменные.
ПРОВЕРКА ТИПОВ: было 7 ошибок, стало 5. Две унаследованные закрылись попутно —
более строгие заготовки вскрыли нехватку обязательного поля elements. Оставшиеся
пять были до меня и не в моих строках.
ПРИЁМКА. Разметка проверена вырезанием: подложил поломку обратно — сторож её
поймал, вернул — снова чисто. Полный набор тестов интерфейса: 233 файла,
1750 тестов, 3 пропущено, 0 падений. Рабочий код на PHP не тронут.
NB: LEFTHOOK_EXCLUDE=cspell — тем же приёмом и по той же причине, что и в записи
|
||
|
|
a2496dfe61 |
feat(реклама): честные статусы кампаний — экран больше не зовёт «Крутится» рекламу с нулём показов
Замечание владельца З-6: «почему статус крутится, когда она отклонена?» Живой замер кампании #6 на бою: статус running, зелёное «Крутится», показов доставлено НОЛЬ, потрачено 0 ₽, заморожено 3 333,36 ₽ — третьи сутки. Главное, что вскрылось: экран физически не мог показать правду. Список кампаний отдавал estimated_impressions («сколько обещали») и не отдавал delivered_impressions («сколько было»), хотя колонка есть. Чисел принятых/отклонённых объявлений тоже не было. Поэтому чинили с сервера, а не с подписей. - сервер отдаёт факты: delivered_impressions + счётчики объявлений (всего/принято/ отклонено), ОДНИМ запросом на весь список — на N+1 поставлен отдельный датчик; те же счётчики доезжают и в отчёт по кампании; - считаются только включённые в показ: снятое галочкой в Яндекс не уезжает и в знаменателе «2 из 15» ему не место; - ярлык по фактам: running при нуле показов → «Принято, показов пока нет» нейтральным цветом; пошли показы → зелёное «Крутится»; часть отклонена → приписка «часть объявлений отклонена — 2 из 15»; все → красное «Отклонено»; - правило перехода статусов НЕ тронуто: на rejected висит возврат заморозки (AdWalletService::release, «ВЫХОД 2»). Чинили то, что видит человек; - подписи собраны в один файл (composables/campaignStatusMeta.ts). Их было ДВЕ копии — в списке и в отчёте — и они уже разъехались; разъехавшиеся копии и есть та разница между экранами, на которую жалуется владелец; - телеграм-экран: подписи вынесены отдельно и приведены к тем же словам («На модерации в МТС» ↔ «На модерации в Яндексе», «Отклонено» на обоих); голое «Запущена» → «Запущена в кабинете МТС». Граница честности: у телеграм-модуля нет счётчика показов ВООБЩЕ — actual_cost_rub в таблице есть, но её не пишет ни одна строка кода. Поэтому там нельзя сказать ни «крутится», ни «показов пока нет»: это была бы выдумка того же сорта. Записано открытым вопросом владельцу в файле замечаний. Проверено: 435 тестов рекламы Яндекса, 281 телеграма, 1771 тест экранов (235 файлов, 3 пропущено), статанализ 0, формат чист. Новых тестов 18. Глазами НЕ принято (приёмка — пачка 5), на боевой НЕ выкачено. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7922bffb6d |
feat(телеграм-реклама): заголовок и картинка в авто-режиме + честная проверка медиа по требованиям МТС
Пачка 1 замечаний владельца о едином виде рекламных экранов. Песочница выключена 02.08.2026 — каждая дыра ниже стоила живых денег. 1. Заголовок объявления в АВТО-правиле. Кабинет МТС требует его для рекламы сайта; утром 02.08 поле довезли до разовой формы, а в авто его не было вовсе — накопитель создавал кампанию без ad_headline, и она сгорела бы в кабинете уже после списания. Колонка client_tg_auto_rule.ad_headline, приём в контроллере (обязателен только при включённом авто и не-телеграмной ссылке), перенос в кампанию, поле на экране. 2. Картинка/видео в авто-правиле. Колонка media_path, отдельная загрузка POST /api/telegram/auto-rule/media, перенос в кампанию, поле на экране. 3. Проверка медиа переписана по настоящим требованиям кабинета, снятым глазами 02.08. Было mimes:png,jpg,jpeg,gif,mp4|max:51200 — врало по пяти пунктам: принимало GIF, пропускало вдвое больший вес, не смотрело пиксели и длительность, зря отказывало в mov/webm. Стало правило App\Rules\ClientTg\MtsMedia: картинка JPEG/PNG до 25 МБ и 640x360...5120x2880, видео до 20 МБ, 3-55 секунд, от 640x360. Формат картинки определяется по содержимому, длительность и кадр видео читает App\Support\Mp4Probe из контейнера — без внешних программ. Отказ человеческий: «Картинка слишком маленькая: 300x200. Нужна не меньше 640x360». 4. Цена показа с медиа — 600 руб. с картинкой, 680 с видео — теперь видна ДО загрузки файла, на обоих экранах. Сверх плана, найдено по дороге: 5. Предохранитель накопителя: правило, сохранённое до 02.08 со ссылкой на сайт и пустым заголовком, всё равно ушло бы в кабинет. Теперь такая пачка держится черновиком, в журнал пишется причина no_headline. 6. Отказ сервера доходит до клиента его словами. Оба экрана глушили ответ общей фразой «Не удалось рассчитать кампанию», и человек не понимал, что не так с файлом. Границы честности: длительность и размер кадра читаются только у mp4/mov/m4v; у webm/mkv/mpeg/wmv проверяются формат и вес — так и записано в Mp4Probe. Проверено: 281 тест телеграм-модуля, 1753 фронтенд-теста, статанализ 0, Pint чист. Глазами НЕ принимали — приёмка живьём в пачке 5. На боевой не выкачено. Журнал схемы: v9.64 — номер взят как максимум по всем веткам плюс один, чтобы не повторить столкновения 29.07 и 01.08. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b6a15c5bbf |
merge: подтянул общую ветку в клиентские СМС — 227 записей отставания закрыты
Ветка шла отдельно почти неделю и отставала на 227 записей, отставание росло
каждый день. Направление сведения — общая В ветку: перевод main владелец
отклонил, значит вливать в него нечего.
Девять столкновений, каждое разобрано по существу.
Журнал схемы: столкнулись НЕ три номера, как ожидалось, а ВСЕ - обе ветки
независимо заняли v8.96-v9.25 и v9.32 разным содержимым. Обе стороны
настоящие, выбросить нельзя ни одну, поэтому перенумерована ветка, а не
общая: 31 запись уехала в свободный диапазон v9.33-v9.63. Содержание не
тронуто - доказано сверкой с исходной версией через git, посимвольно.
Соответствие старых номеров новым вписано в сам журнал, чтобы старые
документы ветки оставались читаемыми. Прежняя пометка про "запас v9.32"
заменена: запас не спас, v9.32 в общей ветке тоже был занят.
Сборка тестовой базы: взята версия общей ветки. Она позже и доказана
замером - двумя шагами вместо migrate:fresh, который спотыкался на
типе-призраке и оставлял схему неполной.
Список слов орфографии сведён объединением: 2106 наших + 2169 общих дали
2173, ни одно слово ни с одной стороны не потеряно - проверено сравнением.
Расписание работ, маршруты экранов и админский слой: обе стороны добавляли
своё в одно место, оставлены обе.
Витрина рекламных каналов: каждая ветка сделала настоящим СВОЙ канал -
ветка СМС свой, общая Телеграм. После сведения настоящих три, заглушки
исключают все три. Сторож витрины принят вырезанием: убрал СМС из списка
настоящих - покраснел, вернул - позеленел.
СТОЛКНОВЕНИЕ ИМЁН, созданное самим сведением. Оба набора тестов объявляли
глобального помощника pollCampaign - свой в СМС (один довод) и свой в
Телеграме (от двух до четырёх). Две функции с одним именем в одном языке
не живут: пока ветки шли врозь, этого не видел никто. Помощник СМС
переименован в pollSmsCampaign. Проверено, что других таких пар в PHP-тестах
нет ни одной.
Статанализ ветки доведён с 674 замечаний до НУЛЯ, уровень не понижен и в
baseline не заметено ничего.
- 616 из 674 - ложный класс Pest, закрытый тремя узкими правилами; правила
перенесены из рабочей ветки, где владелец их уже принял;
- остальные 42 - свои, в новом коде ветки, и починены по существу:
задвоенный ключ массива в трёх тестах (след копирования - комментарий
оторвался от своей строки), врущие описания двух помощников (PHP сам
делает из ключа-номера число), сужение типа возврата, прятавшее от
анализатора свойства подставного отправителя, лишний знак вопроса и
четыре бесполезных перенумерования списка.
- Приёмка вырезанием: подложил несуществующий метод - анализатор назвал его
поимённо и покраснел; убрал - ноль.
Шапки 59 моделей обновлены пересборкой подсказчика и ОСТАВЛЕНЫ намеренно
(правка только в комментариях, проверено): без них анализатор не связывает
модель с описанием и не знает, что дата - это дата, а не строка. Откатил их
сперва по привычке - получил 15 замечаний про даты, вернул - ноль.
Орфография: 15 файлов проверено, 0 замечаний (смотрел и на число
проверенных файлов, не только на число ошибок). Добавлены три слова из имён
миграций ветки.
Заодно: алиас ruflo-core в списке имён сторожа реестра - плагин описан в
реестре групповым именем, сторож видел только машинное. Мостится ТОЛЬКО имя;
🔴 содержательный долг остаётся - реестр до сих пор зовёт ruflo изолированным,
хотя его разморозили 28.07. Это чинить отдельно, через claude-md-management.
Проверено: три фронтовых сторожа рекламы 18/18, статанализ 0, разметка 0,
орфография 0, синтаксис PHP чист. Полный прогон тестов ветки - отдельным
шагом, он ещё ни разу не делался.
NB: в журнале схемы есть задвоенные номера v8.26 (пять раз) и v8.64 (два) -
это досталось по наследству из общей ветки, ровно столько же их там и было.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
62d81e33f7 |
Merge remote-tracking branch 'gitea/main' into fix/tg-zagolovok-obyavleniya
# Conflicts: # docs/observer/STATUS.md |
||
|
|
9ef688fa8d |
merge: подтянул main после выката починок модерации Яндекса
Мой коммит уехал в main отдельно — пересадкой на актуальную вершину и оттуда на боевой. Возвращаю main к себе, чтобы ветка не разъезжалась. Пришло 9 коммитов, из них главное — работа соседней сессии по воронке продаж, уже влитая в main и запушенная. Конфликта два, оба в общих машинных файлах: - cspell-words.txt — сторону не выбирал, объединил. Все слова обеих сторон на месте; убрана ровно одна строка — мой же дубль «админский», который прошлый коммит добавил дважды. Проверено сравнением с версией до слияния: другого отличия нет; - docs/observer/STATUS.md — машинный файл наблюдателя со столбиком часов процессов, взята своя версия, его всё равно переписывает хук. Замечание на будущее: патч «минимум площадки считается по длине периода» живёт в двух коммитах — |
||
|
|
947cb3403a |
fix(воронка-продаж): сроки фильтра по датам — «Просроченные» отдельным пунктом, планы смотрят вперёд
Просьба владельца 01.08.2026: «убери вчера, наверх создай просроченные,
сегодня, завтра и т.д. и проверь что они правильно привязаны и реально
работают»; во втором режиме «убери завтра — завтра у тебя не может быть».
У каждого режима теперь СВОЙ список сроков, они не пересекаются:
Что надо сделать (вперёд): Просроченные / Сегодня / Завтра /
Ближайшие 7 дней / Ближайшие 30 дней / Произвольный
Что менялось (назад): Сегодня / Вчера / 7 дней / 30 дней / Произвольный
Каждый пункт показывает ровно то, что на нём написано. Раньше просроченное
подмешивалось в ЛЮБОЙ выбранный период, и «Сегодня» показывало не только
сегодняшнее — теперь это отдельный первый пункт (period=overdue), а подпись
«плюс всё просроченное» убрана за ненадобностью.
Вторая, невидимая глазом поломка: «7/30 дней» в режиме планов считались
НАЗАД (d7/d30) — «что надо сделать за прошедшую неделю». Добавлены зеркала
next7/next30 в SalesPeriodResolver.
Срок из чужого набора («что менялось завтра») сервер отвергает с 422, а не
подменяет молча текущим месяцем, как делал прежний резолвер по умолчанию.
Проверено:
- сервер: 30/30 (фильтр + резолвер), весь отдел продаж 498/498;
- фронт: 1739/1739 весь набор;
- приёмка вырезанием — подложил поломку в оба места, оба набора покраснели;
- глазами в браузере 1920×1080: списки сроков в обоих режимах, «Просроченные»
дают ровно забытые карточки, «Завтра» — ровно завтрашнюю (скрины 08–12).
Попутно починен чужой протухший тест advertising-channels: Телеграм давно
стал живым роутом, а тест продолжал считать его заглушкой и был красным.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
19d9eda368 |
fix сверка: не кричим про расхождение, пока оператор просто ещё не ответил
Находка приёмки Этапа 5. Сверка считала счёт оператора «известным», если отчиталось хотя бы ОДНО сообщение из всей рассылки, а разницу брала против нашего расхода по ВСЕМ. Живой замер на стенде: оператор отчитался по 10 сообщениям из 120 — экран написал «наш расход 360.00 руб, счёт оператора 60.00 руб, разница минус 300.00 руб». Человек прочитает это как «МТС недосчитал 300 рублей». Правда другая: МТС ещё не отчитался по 110 сообщениям. Отчёты приходят постепенно, опрос ходит раз в десять минут — значит такое состояние было бы у КАЖДОЙ свежей рассылки. Это тот же класс, что уже ловили на вебхуке поставщика: сторож, не умеющий отличить «потеряно» от «ещё в пути», штампует ложные тревоги и заставляет чинить несломанное. Что сделано: - разница считается от нашего расхода ПО ОТЧИТАННОМУ, то есть сравнимое со сравнимым. Для этого запрос отдельно складывает наши части только по тем сообщениям, за которые оператор назвал цену; - «наш расход» слева остался прежним — это сколько мы должны за всю рассылку, и число полезное. Рядом с разницей экран пишет охват: «оператор отчитался по 10 из 120». Без охвата разница непонятна: не видно, по всей ли рассылке она; - охват пишется ТОЛЬКО когда отчитались не по всем, иначе строка шумела бы всегда; - настоящее расхождение по деньгам видно и при неполном отчёте — ждать полного отчёта, чтобы заметить, что оператор считает дороже, было бы хуже исходной беды. Порядок работы соблюдён. Четыре теста сервера и два теста экрана написаны ДО правки и покраснели на отсутствующих полях. Каждый доказан вырезом, вырезов четыре: вернул разницу к расходу по всей рассылке - покраснели 2; убрал охват из ответа - покраснели 3; убрал строку охвата с экрана - покраснел 1; показал охват всегда - покраснел другой 1. Это пара: один тест стережёт «видно», второй «не шумит». Прогоны: модуль 387 из 387, фронт 222 файла и 1722 зелёных при 3 намеренно пропущенных, формат чист, типы - 5 ошибок и до правки, и после, все в чужих файлах. Статанализ добавил 4 замечания одного ложного класса: анализатор не понимает $this внутри тестов Pest и не видит getJson. Доказательство ложности прямое - эти самые тесты проходят, не будь метода, они бы падали. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1acbaf9383 |
Merge remote-tracking branch 'gitea/main' into feat/prospects-manual-testing-kp
# Conflicts: # docs/observer/STATUS.md |
||
|
|
9ee4e9a79f |
feat(воронка продаж): корзина, дробь у «Отказа», фильтр по датам — экраны
- 12-я колонка «Корзина», результат «В корзину» просит только причину; - в шапке «Отказа» дробь «4/2» с подсказкой «из них 2 после ручного тестирования»; при нуле дробь не рисуется — «69/0» это шум; - у «Выслано КП» поле даты подписано «если договорились» и необязательно; - орган фильтра по датам на ОБОИХ экранах, два режима + период. Приёмка глазами пройдена живьём по всем семи пунктам, включая «Воронку отдела» (снимки в docs/superpowers/screens/2026-08-01-korzina-filtry/priemka/). Браузер поймал то, чего не видели тесты: при смене режима оставался прежний период, и «что менялось за завтра» давало пустую доску — теперь период возвращается к «Сегодня», на это заведён отдельный тест. Прогон: сервер 1245/1245, фронт 1567/1567. Журнал схемы — v9.31. |
||
|
|
7aa54773c1 |
merge: свёл заголовок объявления с веткой робота — воронка продаж и опрос в одной ветке
Влил fix/robot-yandex-zamok (26 коммитов: сведение с основной, стадии воронки «Тестирование ручное» и «Выслано КП», чтение вердикта и пересдача по опросу) в ветку заголовка объявления. Конфликт был один — docs/observer/STATUS.md, машинный файл наблюдателя со столбиком часов процессов. Взята своя, более свежая версия; файл всё равно перезаписывается хуком. Замер после слияния (своя тестовая база liderra_testing_zag, прогон в тишине): портал 4069 тестов, 4029 прошло, 16 упало; робот 130/130. До слияния было 4063/4018/17. Тестов больше, падений меньше — стык чистый. Падают шесть давних классов, не связанных с этой работой: CreativeRobotEndpoint (11 — лезет в живой Директ и получает «недействительный ключ»), CreativeJobService, InAppNotification, PhoneRegionSmoke, ProjectExtensions, SalesOverview. Проверено отдельно: таблица заданий роботу не ограничивает список режимов (обычная строка), поэтому новые «read-status» и «resubmit» миграции не требуют. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
32df332901 |
fix(телеграм-реклама): заголовок объявления для рекламы сайта — кампания больше не встаёт
Приёмка глазами вскрыла: кабинет МТС требует «Заголовок объявления» (до 40 знаков), когда в объявлении ссылка на САЙТ, а не на телеграм-канал. Робот про это поле не знал, «Продолжить» молча не срабатывало, кампания вставала на шаге «Объявление» — в бою уже ПОСЛЕ списания денег. Проверено живьём: 2234454 (сайт — встала) против 2234462 (канал — дошла до подтверждения) и 2234490 (сайт с заголовком — дошла). Портал спрашивает заголовок заранее, на создании черновика: обязателен только для не-телеграмной ссылки (App\Support\TelegramLink), колонка ad_headline varchar(40), поле на экране появляется по той же развилке. Робот заполняет его в кабинете. Три ловушки, добытые живыми прогонами (описаны в коде): - поле дорисовывается в ОТВЕТ на ссылку, с задержкой — надо ждать, а не спрашивать; - под описание подходит несколько элементов — нужен .first(); - серая надпись внутри поля НЕ placeholder, а нарисованная подпись: поиск по атрибуту давал ноль совпадений при видимом на снимке поле. Опознаём по видимой надписи. Тесты: робот 130/130, ClientTg 250/250, экран 23/23. Полный прогон бэкенда — те же 13 падающих классов до и после правки (ни одного в телеграм-части). В baseline статанализа добавлен известный ложный класс Pest для нового файла тестов. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
22ac6e4f13 |
feat(смс-клиент): журнал рассылки открывается страницами по 50, а не одним куском
Решение владельца В-203 — «делай». Это не строка приёмочного листа, а мина, найденная разведкой: журнал отдавал ВСЕ сообщения рассылки одним ответом. На рассылке в двадцать тысяч номеров это двадцать тысяч строк за раз и подвисший экран — ровно то, что Этап 4 уже вынул из базы номеров. Этап 5 сделал мину горячее: в журнал добавилась судьба каждого номера, и человек стал открывать его чаще. Теперь по 50 строк, внизу подпись «Всего строк: 120 · страница 2 из 3» и переключатель. На рассылке в одну страницу переключателя нет — не шуметь там, где листать нечего. Размер страницы адресом не задерёшь: потолок 200, иначе страницы обходятся одним параметром и мы возвращаемся туда, откуда ушли. Номер страницы передаётся серверу явно. Сам по себе постраничный вывод берёт его из общего запроса приложения — и вторая страница выходит неотличимой от первой; эту дыру мы уже ловили живьём 30 июля на базе номеров. Главное решение здесь про доверие к числам: итог по судьбам и предложение досыла считаются по ВСЕЙ рассылке, а не по видимой странице. Иначе человек, листнув, увидел бы другой итог и не понял, какому верить. А досыл — это ещё и деньги: считать не дошедших по видимой странице значило бы называть заниженное число и брать не ту сумму. Стерегут это отдельные тесты — и на сервере, и на экране. Восемь тестов на сервере и пять на экране, все доказаны вырезом; вырезов вышло одиннадцать. Но главным прибором тут был браузер: вырез «убрать номер страницы» проверка запросом не видит вовсе — это записано в самом уроке. Живьём пройдены все три страницы: пятьдесят номеров, потом другие пятьдесят, потом остаток в двадцать; подпись менялась, а итог «доставлено 80, не доставлено 40» и кнопка «Дослать не дошедшим (40)» на всех трёх остались прежними. Стенд возвращён. Попутно поймана болезнь измерителя, уже второй раз за смену: он считал только «не сошлось» и не видел «рухнуло», отчего доложил два красных вместо семи. Прибор обязан читать весь отчёт, а не то поле, которое ты ждал. Один существующий тест пришлось поправить — он закреплял прежний вызов без номера страницы. Поправлен так, чтобы стеречь новое поведение, и к нему добавлен второй: страницу не назвали — просим первую. |
||
|
|
65f3785bf3 |
feat(смс-клиент): сверка нашего расчёта с расчётом оператора — в админке у владельца
Строка листа 5.7. В админке «СМС» появился блок «Сверка расчётов с оператором»: по каждой отправленной рассылке видно, сколько частей насчитали мы и сколько оператор, сколько сообщений разошлись, наш расход, счёт оператора и разница. Клиенту это не показывается вовсе (решение В-201) — он платит по своему тарифу, наш расход перед оператором не его дело. Главное решение здесь — отсутствие числа и ноль это РАЗНЫЕ состояния. Экран пишет «цена канала не задана», «оператор цену не сообщил», «не спрашивали», «сверять нечем» и никогда не рисует 0 ₽ вместо неизвестного. Иначе владелец видел бы идеальную сходимость там, где сверять нечем — ровно тот молчаливый сбой, что стоил четырёх поломок 20.07. Разница считается только когда известны оба числа. Порядок работы задала живая проба, а не код. Шесть живых сообщений с боевого (разрешение и номера дал владелец) с перебором по одному признаку: МТС-номер и Т2-номер, одна часть и две, три разных имени отправителя, смешанный и чисто русский текст. Все шесть — «не отправлено», цена 0, отказ в ту же секунду, что и приём. Владелец проверил кабинет: баланс 5010 ₽, имя живое. Значит вывод из памяти проекта «пустой счёт» опровергнут, причина на стороне оператора (В-235, В-237) и требует разбора с их поддержкой. Попутно вторично и жёстче подтвердилось В-212: оператор принял даже номер Теле2, которого наш канал не обслуживает. «Принял» не значит ничего — по-старому портал записал бы «отправлено» и списал бы деньги за сообщения, которых нет. Что проба дала положительного: оператор считает ЧАСТИ ровно как мы — 1 на короткое, 2 на длинное в 118 знаков. На этом сверка по частям и построена, деньги ей не нужны. Строка 5.7 сужена честно и не молча (В-236): денежная половина построена, но живьём не доказана — цены нет с обеих сторон. У оператора 0, а у нас цена канала на бою не задана вовсе (В-234). Дозакрыть можно двумя вещами: числом из договора и одним реально отправленным сообщением с ненулевой ценой. План велел править AdminSmsController — такого файла нет вовсе (В-231, четвёртый раз за проект). Сверка живёт отдельным распорядителем: она ни ценам, ни отказам, ни именам отправителя не родня. Тесты: 9 на сервере, 6 на экране. Все проверены вырезом — 15 вырезов, каждый покрасил именно свои тесты. Один вырез не сработал, и опять ошибалось моё ожидание, а не код (В-239): база сама считает сравнение с пустотой «неизвестным», поэтому страж оказался лишним; заменён на вырез с настоящей ошибкой этого места. Живой прогон в браузере парный: без заданной цены канала — «цена канала не задана» и «сверять нечем»; с ценой 3 ₽ за часть — 9.00 ₽ против 12.00 ₽, разница 3.00 ₽, а расхождение по частям (3 против 4) выделено красным. Стенд возвращён. Статанализ поймал настоящую ошибку: сводные числа читались как поля модели сообщения, которых у неё нет. Починено — итоги берутся голыми строками, а не моделями. |
||
|
|
a57a70268d |
merge: воронка продаж — стадии «Тестирование ручное» и «Выслано КП» в main
Конфликты только в документах: журнал «мозга» взят из main, запись v9.28 переставлена поверх боевого журнала схемы. Код не пересекался. Сторож слоёв проверен отдельно в рабочей папке: 0 нарушений. Сторож секретов gitleaks отработал по-настоящему. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c65856ec73 |
feat(смс-клиент): досыл не дошедшим — отдельной рассылкой «Досыл к рассылке №N»
Строка листа 5.6. В журнале рассылки появилась кнопка «Дослать не дошедшим (N)».
Нажатие спрашивает отдельным окном: сколько человек получат сообщение ещё раз и что
это отдельная рассылка с отдельной оплатой. По подтверждению заводится рассылка
«Досыл к рассылке №N» — у неё свои деньги и свой итог, а первая остаётся целой.
Почему отдельной рассылкой, а не второй попыткой в старой (решение владельца В-200):
переписав старые строки, мы стёрли бы «не доставлено», и человек больше не увидел бы,
что первая попытка не дошла. Да и база второй попытки не даст: на журнале сообщений
уникальный ключ по клиенту, рассылке и номеру.
Досылаем только по ОКОНЧАТЕЛЬНОМУ итогу (моё решение В-216): пока кто-то «ещё в пути»
или пока сама рассылка идёт — кнопки нет, а сервер отказывает человеческими словами.
Правило живёт в одном месте на сервере, экран получает готовый признак и условие не
пересчитывает: разойдись они — человек нажимал бы кнопку и получал отказ.
Деньги. Досыл списывается как обычная отправка (решение В-198) и своей денежной ветки
не имеет — считает, замораживает и списывает тот же общий код заказа. Повторное
нажатие второй рассылки НЕ заводит: ключ заказа постоянный, «resend:{номер}». Второй
клик означал бы вторую оплаченную рассылку тем же людям.
Живой прогон нашёл то, чего не видели тесты (В-227): досыл забывал оператора, которого
портал уже знал. Номер уходил как «вписанный руками», получал пометку «оператора не
знаем», а такую строку обогащение регионом уже не чинит — на бою, где универсального
канала может не быть, досыл не ушёл бы никому при созданной и оплаченной рассылке.
Теперь досыл вспоминает оператора из журнала сообщений первой рассылки: журнал хранится
без срока, в отличие от снимка получателей, который чистится через 90 дней.
Ещё две дыры, которых не было в плане. Первая: «в пути» считается только по
отправленному, поэтому у рассылки в работе он равен нулю — досыл разрешался бы посреди
работы (В-229). Вторая: защита от случайного повтора отбивала бы досыл молча, ведь текст
у него тот же, что у первой рассылки (В-228).
Схема v9.25: колонка resend_of_campaign_id и индекс у client_sms_campaigns. Прав не
требует — это колонка существующей таблицы, а не новая таблица.
Тесты: 8 на сервере, 4 на экране. Все проверены вырезом — двенадцать вырезов, каждый
покрасил именно свои тесты. Живой прогон в браузере: кнопка с числом, окно про отдельную
оплату, рассылка на два нужных номера с сохранённым оператором, первая рассылка цела,
второе нажатие ничего не завело. Стенд возвращён.
Движение денег живьём НЕ проверялось: на стенде включена песочница, а выключить её —
значит слать настоящие СМС за настоящие деньги.
|
||
|
|
f4ed24e3cc |
feat(воронка продаж): два новых результата разговора в карточке
Выбор канала + поле значения, подпись поля меняется по выбору. Дата созвона обязательна у обоих. Платящему клиенту результаты не предлагаются. «Зарегистрировался» остаётся доступен с новых стадий — иначе из них не выйти. Статанализ и сторож журналов «мозга» пропущены с разрешения владельца: первый даёт тот же фон замечаний, что и до правки, второй падает из-за обрезанных корневых библиотек — оба к этой работе отношения не имеют. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2d865d3fe5 |
feat(смс-клиент): в журнале рассылки видна судьба каждого номера и честное объяснение про трое суток
Строки листа 5.1, 5.3 и 5.4. До этого человек видел только «отправлено» — то есть что мы отдали сообщение оператору. Дошло ли оно до людей, экран не говорил вовсе, хотя портал это уже знал: судьбу собирает Task 3, а итог считает Task 5. Оказалось, до экрана эта работа не доезжала: открытие журнала рассылки БРАЛО из ответа сервера только сообщения, а присланный итог по судьбам молча выбрасывало (В-225). Теперь не выбрасывает. Что видит человек: колонку «Судьба» рядом со «Статусом», над таблицей итог «Доставлено · Не доставлено · Ещё в пути», а под ним — объяснение, что оператор досылает до трёх суток. Объяснение показывается только пока кто-то правда в пути; когда итог стал окончательным, оно исчезает само. Словарь судеб — ОТДЕЛЬНЫЙ от словаря статусов отправки (В-70). Тот отвечает на вопрос «ушло ли от нас», этот — «дошло ли до человека». Смешать их значило бы написать на экране «отправлено, не доставлено» одним словом. Числа экран не складывает сам — берёт готовые с сервера: второй счётчик однажды разошёлся бы с первым, а речь про деньги. Тесты: 4 новых, каждый проверен вырезом — шесть вырезов, шесть красных, и каждый покрасил именно свой тест. Проверка идёт ПОСТРОЧНО: «не доставлено» содержит внутри себя «доставлено», и проверка по всей странице зеленела бы, даже если экран напишет всем номерам одно и то же. Живой прогон в браузере парный: пока двое в пути — итог 1/1/2/1 и объяснение видно; довёл судьбы до окончательных — итог 3/1/0/1, объяснение исчезло. Стенд возвращён. Отдельная запись про приборы (В-224): скрипт вырезов показал 47 красных из 85 на чистом коде. Виновата одна буква — папка задавалась как «c:» вместо «C:», и vite грузил модули дважды, отчего подмены в тестах не срабатывали. Поймал это базовый прогон без вырезов — тот самый, что появился после прошлого вранья прибора. |
||
|
|
97a93cb763 |
feat(смс-клиент): кнопка «Продолжить рассылку» — доводим до конца тех, на кого не хватило денег
Строка листа 4.13, решение владельца В-49 «надо обязательно». Рассылка встала из-за денег → клиент пополнил кошелёк → нажал «Продолжить», и она идёт дальше с того места, где остановилась. 🔴 Главная мина (В-151, подтверждена чтением кода): джоб отправки считает обработанным ЛЮБОЙ номер, у которого есть запись в журнале этой рассылки, без разбора статуса. А номер, на котором кончились деньги, записан как «не хватило денег». Значит простой повторный запуск молча пропустил бы его: человек заплатил бы за продолжение, а получатель не получил бы ничего. 🔴 И вторая находка, которой в плане не было (В-184): «оставить строку и просто не считать номер обработанным» НЕВОЗМОЖНО физически — на журнале сообщений уникальный ключ по (клиент, рассылка, номер), вторая запись не вставится. Поэтому продолжение эти строки УДАЛЯЕТ, и по смыслу это верно: «не отправили из-за денег» фиксирует НЕслучившееся и после пополнения перестаёт быть правдой. Заодно замерен масштаб: при остановке пишется РОВНО ОДНА такая строка, у остальных номеров записей нет вовсе. Что сделано: · POST /api/sms/campaigns/{id}/resume — убирает строки «не хватило денег», ставит рассылку в очередь, гасит причину остановки (иначе экран продолжал бы объяснять человеку прошлую остановку у работающей рассылки); · продолжаем ТОЛЬКО остановленную ИЗ-ЗА ДЕНЕГ (В-187). Остановленную человеком продолжать — значит отменить его решение; сорванную сторожем — наступить на ту же поломку. У каждой причины свой человеческий отказ, и текст под тестом; · правило живёт в ОДНОМ месте на сервере (как enableDecision в Task 4): список рассылок отдаёт готовый признак can_resume и число «сколько ещё уйдёт». Счёт остатка — в читателе снимка, где уже живёт «кто ждёт» (одно место, В-96); · кнопка «Продолжить» в таблице рассылок + подтверждение: сколько осталось отправить, что повторно никому не пойдёт, что цена прежняя; · цену НЕ пересчитываем, снимок НЕ пересобираем, заново НЕ замораживаем (В-39, В-188): списание идёт поштучно с проверкой перед каждым номером, в минус рассылка не уйдёт — кончатся деньги, встанет снова с той же причиной. 🔴 Мой же тест поймал расхождение с моим же решением (В-189): проверку денег я впихнул в правило «можно ли продолжить» — и кнопка исчезла бы именно у того, кому она нужна. Разделил: состояние — признак для экрана, деньги — проверка по нажатию с числами («свободно 0.00 ₽, одно сообщение 8.50 ₽»). Право DELETE на журнал сообщений рабочей роли — миграция 2026_08_01_101000, схема v9.22 (В-186, вторая половина мины В-152): без него продолжение на бою падало бы «нет доступа». Сторож MigrationGrantsTest расширен. Проверено: ClientSms 320/320 (13 пачек, все с первой попытки), приём лидов 17/17, фронт 1700 + 3 пропущенных (одна чужая давняя ошибка, В-57), phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Шесть вырезов, каждый покраснел ровно там, где вырезан. 🔴 Живой прогон ДЕНЕЖНЫЙ и парный под боевой ролью crm_app_user: рассылка на 5 номеров при деньгах на 2 встала (ушло 2, списано 17.00 ₽, одна строка «не хватило денег»); продолжение без пополнения — отказ с числами; БЕЗ пометки клиента рассылка не видна вовсе (изоляция держит); после пополнения с пометкой — ушло 5 из 5, списано 42.50 ₽, в журнале ровно 5 записей по одной на номер, «не хватило денег» не осталось. В браузере: кнопка видна глазами, после нажатия и прогона очереди рассылка «завершена, 5 из 5», кнопка исчезла, у нетронутой рассылки осталась. Заодно поправлена речь: было «отправим ещё 3 сообщений». Стенд возвращён и сверен; намеренное изменение одно — миграция накатана на локальную dev-базу. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4784382d49 |
feat(смс-клиент): журнал автоматических СМС за 30 дней на экране клиента
Строка листа 4.11, решение владельца В-102 «обязательно нужен». Причины по авто-СМС писались в базу, но человеку их не показывал НИ ОДИН адрес портала: у авто-СМС нет кампании, а единственный список сообщений отбирает их по номеру кампании. Это и была суть задачи, а не мелочь на потом. Что сделано: · GET /api/sms/auto-log отбирает ровно авто-СМС (кампания пуста) за 30 дней: «ушло», «не ушло» и разбивка по причинам — АГРЕГАТАМИ по всему окну, из базы приезжают числа, а не тысячи строк; · поимённый список — последние 100 событий, потолок жёсткий, параметра у клиента нет вовсе; если событий больше, экран честно говорит «Показаны последние 100 событий из N. Числа выше — за все 30 дней» (В-170, класс В-166: обязательство держится запретом на сервере, а не вежливостью экрана); · блок на вкладке «Авто-СМС»: числа, причины человеческими словами по уже заведённому словарю подписей, таблица событий, а на пустом журнале — фраза «За 30 дней автоматических СМС не было» вместо пустой таблицы. Попутно починены две неправды экрана — обе внутри обязательства «человеческими словами», а не сверх него: · подпись «отписались раньше» переписана на «номер в вашем стоп-листе» (В-169): возможности отказаться у получателя НЕТ вовсе (решение В-30), номер в этот список вносит сам клиент. Подпись пережила отмену целой функции и объясняла человеку то, чего в портале нет — класс В-154; · пробное сообщение считается НЕ ушедшим (В-172). План велел обратное, но денежный счётчик модуля считает только настоящее «отправлено», и подпись экрана говорит «пробный режим — не отправлено». Это четырнадцатая ошибка плана, найденная тем же приёмом: открыть код и посмотреть, как это же состояние трактуют соседи. Статанализ поймал то, чего не увидели ни четыре зелёных теста, ни живой прогон, ни браузер (В-174): разбивка читала у модели поля, которых у неё нет. Переписано на пару «причина → сколько». У каждого прибора своя слепая зона. Проверено: ClientSms 309/309, приём лидов 17/17, фронт 1697 + 3 пропущенных, phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Восемь вырезов, каждый покраснел там, где вырезан. Живой прогон под боевой ролью crm_app_user, парно: события писал НАСТОЯЩИЙ джоб авто-СМС — с пометкой клиента «ушло 1 (списано 9.00 ₽), не ушло 2: пробный режим 1, стоп-лист 1», без пометки тот же вызов дал ноль. На 363 событиях — ровно 100 строк в таблице при полных числах. ДаДата не тронута: клиент подменён заглушкой. Стенд возвращён и сверен. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
284163de5a |
feat(смс-клиент): своя база листается страницами, а не грузится целиком
Строка листа 4.7. База клиента бывает на десятки тысяч номеров, и экран получал её одним куском: замер на 20 000 номерах — 5.09 с и 3.60 МБ в одном ответе. Стало 0.04 с и 0.01 МБ на страницу. Что сделано: · GET /api/sms/contacts отдаёт страницу по 50 номеров и поля страницы (total / page / per_page / last_page) в том же объекте, где уже жил потолок загрузки — форму ответа меняли ОДИН раз, в прошлой задаче (В-160); · больше 200 номеров за один ответ не отдаётся НИКОМУ (В-166): иначе постраничность обходится параметром ?per_page=100000 и обязательство «не грузится целиком» остаётся невыполненным; · экран показывает «Всего номеров в базе: N» и листалку, страницу берёт у сервера, а не режет список у себя. Живой прогон под боевой ролью нашёл то, чего не видел ни один из шести зелёных тестов (В-167): вторая страница отдавала ТЕ ЖЕ номера, что первая — paginate() брал номер страницы из глобального запроса приложения, а не из переданного в метод. По HTTP это одно и то же, поэтому тесты через getJson врали хором. Номер страницы читается явно; прибор — тест, зовущий метод так же, как живой прогон. Двух мест план не касался вовсе, оба про враньё экрана: · удаление номера убирало строку только на экране — при страницах осталось бы 49 из 50, «всего» не менялось бы, а номер со следующей страницы не подтягивался (В-164); теперь страница перечитывается, а опустевшая последняя отступает на шаг назад; · после загрузки человек оставался на прежней странице и не видел ни одного своего нового номера при надписи «Добавлено: N» (В-165) — теперь возвращаем на первую страницу, где эти номера и лежат. Проверено: ClientSms 305/305, приём лидов 17/17, фронт 1694 + 3 пропущенных, phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Восемь вырезов, каждый покраснел там, где вырезан. В браузере: 50 строк, «Всего номеров в базе: 20 000», листалка 1…400, переход на вторую страницу уходит запросом ?page=2 (ответ 9.6 КБ). Стенд возвращён и сверен со снимком до работы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d292082ab |
feat(смс-клиент): потолок загрузки базы и пачечная запись вместо построчной
Строка листа 4.6 состояла из двух половин, и вторая («десятки тысяч
проходят и не падают») не выполнялась вовсе: каждый номер писался
отдельным запросом — 20 000 номеров стоили 25 278 запросов и 41 секунду.
Теперь пишем пачками по 1000 одним upsert: 23 запроса и 3.5 секунды.
Оплаченный ДаДатой оператор при повторной загрузке не стирается, дубли
внутри одной загрузки не роняют её, база не задваивается.
Потолок: колонка client_sms_settings.max_upload_phones (миграция
2026_08_01_100800, схема v9.20), по умолчанию 50 000, правится владельцем
в админке в границах 1 000…100 000. Сверх потолка загрузка отклоняется
целиком — частично загруженная база хуже незагруженной — и человек видит
оба числа. Экран говорит потолок ДО загрузки, числом с сервера.
Потолок спрашивается ПЕРЕД построчной проверкой номеров: иначе отказ на
50 001 номере занимал 20 секунд (замерено живым прогоном), а при верхней
границе запрос успел бы умереть по сроку жизни.
Ответ GET /api/sms/contacts стал объектом {items, max_upload_phones};
мёртвое поле contacts из ответа загрузки убрано.
Проверено: 14 серверных тестов, 2 фронтовых, 8 вырезов (каждый покраснел
там, где вырезан), живой прогон под боевой ролью crm_app_user и в браузере.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
77f61fb1d1 |
merge: сведение ветки «Реклама Телеграм» с боевым main
Слияние feat/client-telegram-ads с main
|
||
|
|
1122c4ddc9 |
feat(смс-клиент): номер, вписанный руками, принимается в любом виде
Строка листа 4.8. Портал теперь понимает «+79991234567», «8 999 000 00 02», «(999) 000-00-03» и «7999-000-00-04» — раньше первый же такой номер отбивался ошибкой 422. Вся строка оказалась одной лишней проверкой в правилах заказа: 'phones.*' => 'string|size:11' требовала ровно 11 знаков и срабатывала ДО разборщика номеров, который читает все эти виды прекрасно. Умение было — ему мешала проверка. Разбор номеров живёт в одном доме (normalizeManualPhones), куда ходят и предпросмотр, и заказ: второй такой дом означал бы, что смета и факт считаются по разным спискам. Непонятые номера не пропадают молча (строка листа так и требует — «возвращаются образцами»): сервер отдаёт их число и до двадцати образцов, а экран говорит «Не поняли 1 номер: абвгд — проверьте их и впишите заново. Остальные уйдут как обычно». Отдаём и в предпросмотре, и в ответе на заказ: между ними список могли дописать. Доказательства: 3 серверных теста и 2 фронтовых (второй парный — когда всё разобрано, экран молчит), 2 выреза, живой прогон в браузере. ClientSms 284/284, приём лидов 17/17, фронт 1687, phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
65e270583c |
feat(смс-клиент): шлём только абонентам МТС, Билайна, Мегафона и Теле2
Строка листа 4.14, решение владельца В-149 (вариант Б). Номер мелкого или виртуального оператора в рассылку не берётся, и человек видит честную причину, а не молчаливую пропажу. Каналы отправки НЕ тронуты: МТС возит своих, остальных троих — универсальный канал СМС-центра. Ограничиваем, КОГО берём, а не КЕМ везём. Список — настройкой, а не в коде: client_sms_settings.allowed_operators (схема v9.19), галочки «Кому шлём» в админке, пусто = четвёрка по умолчанию. Правило живёт в одном месте — AllowedSmsOperators. Решение принимается ДВАЖДЫ, и второй раз — единственная возможность: у сделок и своей базы оператор известен в момент заказа, у номеров, вписанных руками, его нет вовсе, и приговор выносится в момент ответа ДаДаты — в снимок ложится уже канонический ключ, где «Тинькофф Мобайл» неотличим от «ещё не спрашивали». Плата за имя не тронута (В-150): в коде два похожих списка операторов, и связать их значило бы поднять плату всем клиентам с 2500 до 10 000 рублей. Заодно починена давняя неправда на экране (В-154): «номер не из МТС (пока шлём только по МТС)» — универсальный канал возит всех. Доказательства: 8 новых тестов (в т.ч. сторож длины слага причины — колонка 24 знака), 6 вырезов, живой прогон с выключенной песочницей и пара на момент ответа ДаДаты, живой прогон в браузере со снятием галочки «Билайн». ClientSms 281/281, приём лидов 17/17, фронт 1685, phpstan 2 чужие давние, vue-tsc 5 чужих давних, pint чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6d52f390a0 |
feat(смс-клиент): кнопка «включить имя обратно» — списывает и включает сразу
Строка листа 4.5, Этап 4 Task 4. Раньше имя, которое клиент выключил сам или которое отключили за долг, можно было вернуть только одним способом: заказать заново, снова приложив документы и снова дождавшись согласования у МТС. Теперь у клиента есть кнопка: она берёт плату за месяц вперёд и включает имя сразу — решение владельца. Денег не хватает — портал говорит, СКОЛЬКО именно не хватает, и не трогает ни имя, ни кошелёк. Документы заново не запрашиваются: заявка уже согласована, сканы на месте. Ключ списания у кнопки тот же, что у ночного работника, поэтому за один календарный месяц имя оплачивается один раз — кто бы его ни включал. Сумма нигде не зашита, берётся из карточки имени и вырастет сама, когда подключим остальных операторов. Плана оказалось мало ЧЕТЫРЕЖДЫ, и все четыре раза — про деньги. В-143: план не смотрел, КТО отключил имя. Имя отключает и владелец руками, и оно у МТС погашено; клиент нажал бы кнопку, портал взял бы с него плату, а имя не работало бы. Это ровно то, что работнику запретили в прошлой задаче. Кнопка теперь смотрит в ту же графу «почему отключено» и при включении её гасит. В-144: слово «отменено» стоит и у имени, которое когда-то работало, и у заявки, которую клиент отозвал ещё на согласовании. Вторую план включил бы как рабочее имя и списал за неё деньги — за имя, которого у оператора нет. Отличаю по отметке согласования: её ставит ровно одно место, кнопка одобрения у владельца. В-145: имя, оплаченное вперёд, план списал бы второй раз — а при пустом кошельке ещё и отказался бы включить имя, за которое уже заплачено. Смотрю на «оплачено до»: срок в будущем — включаю без списания и срок не двигаю. В-146: план отдавал экрану решение «показывать ли кнопку». После первых двух правок правило перестало быть про один признак, и оно переехало на сервер: адрес имени отдаёт готовые «можно ли включить» и «сколько спишется», экран не считает. Отдельно В-148 — его поймал только браузер, мимо прошли 12 серверных и 66 фронтовых тестов. Экран писал «плата за текущий срок уже внесена» у имени, отключённого за долг с апреля. Ноль в сумме приходил не от оплаты, а от песочницы — одно значение с двумя разными причинами. Развёл: сумма считается только по сроку оплаты, песочница гасит сам денежный шаг. Проверено вырезанием, восемь вырезов, все вернуты, каждый покраснел там, где вырезан: проверка денег, отбор по причине отключения, отметка согласования, ветка «оплачено вперёд», ключ месяца, песочница в решении о сумме, кнопка на экране, учёт «можно ли» на экране. Живой прогон под боевой ролью crm_app_user, семь нажатий: 50 ₽ при плате 100 — отказ «Не хватает 50.00 ₽», не тронуто ничего; пополнил до 550 — имя работает, списано ровно 100 ₽, оплачено до 29.08; имя владельца при 5000 ₽ — отказ, деньги целы; неодобренная заявка при 5000 ₽ — отказ, деньги целы; второе нажатие — «имя уже работает», в движениях одно списание. Пара на пометку клиента: без неё то же нажатие не делает ничего, с ней — списывает и включает. Живой прогон в браузере: у имени с долгом видна кнопка и строка «Спишется 2500.00 ₽ — плата за имя за месяц»; нажал — карточка стала «Ваше имя: MYSHOP (активно)». У имени, отключённого владельцем, кнопки нет вовсе. Прогоны: клиентские СМС 275/275 (11 пачек, все с первой попытки), приём лидов 17/17, фронт 1683 + 3 пропущенных, phpstan ровно 2 чужие давние, pint чисто. Проверка типов фронта: 5 ошибок в 5 файлах вместо прежних 8 в 6 — задал явный тип ответа в спеке, который и так правил, и три давние ошибки ушли вместе с моими. Схема базы не менялась, миграций нет. |
||
|
|
ba8e936848 |
feat(смс-клиент): владелец сам задаёт, через сколько считать рассылку зависшей
Строка листа 4.3, Этап 4 Task 2. Срок, после которого сторож считает рассылку зависшей, переехал из кода в админку «СМС» — рядом с границами окна отправки. Поле подписано человеческими словами и объясняет последствие: портал сперва попробует рассылку дожать, потом остановит и вернёт клиенту замороженные деньги, а ту, что честно ждёт утра получателей, не тронет. Границы 5…1440 минут — не вкусовщина. Ниже пяти сторож срывал бы рассылки, которые просто идут медленно: проверка средств и запись в журнал на каждый номер занимают время. Выше суток чужие деньги висели бы замороженными дольше, чем клиент вообще помнит про эту рассылку. Отказы написаны словами, а не кодами. Поле НЕобязательное — как и границы окна: этот адрес зовут и те, кому нужны только настройки имени отправителя, и ломать им запросы права не имеем. (План велел сделать поле обязательным — это была ошибка плана, журнал В-139.) Проверено вырезанием, два выреза, оба вернуты: — убрал границы на сервере → покраснели оба теста про границы; — убрал отправку срока с экрана → покраснели три фронтовых, включая новый. Живой прогон в браузере: выставил 120 → сохранилось → пережило перезагрузку страницы; ввёл 2 минуты → отказ «Слишком мало: рассылка может идти медленно, и сторож срывал бы живые. Ставьте хотя бы 5 минут», в базу не легло. Вернул 60. Прогоны: затронутые тесты 32/32, фронт целиком 1678 + 3 намеренно пропущенных (было 1676, мои +2; одна ошибка — та же чужая давняя), vue-tsc ровно 8 чужих давних в 6 файлах, pint чисто. |
||
|
|
dcf9706703 | Merge branch 'main' into feat/reklama-yandex-pokazy | ||
|
|
6f469fb13d |
feat(телеграм-реклама): деньги на общем балансе + чистка СМС-фантазий + медиа объявления
Денежная модель (куски 1-2): реклама в Телеграме (МТС Маркетолог, робот-в-браузере)
оплачивается с ОБЩЕГО баланса тенанта (как СМС), наценка 40%. Списание при запуске,
возврат при отказе модерации/сбое/отмене (сальдо-идемпотентность tg_ad_charge/tg_ad_refund).
Старый рекламный кошелёк-заморозка убран из потока кампаний (таблицы ad_wallets НЕ удалены).
Новый TelegramCampaignChargeService (зеркало SmsChargeService); F5 money-safety сохранён
(сбой с mts_campaign_id → needs_review без возврата). Уборщик добивает зависшие queued с возвратом.
Чистка СМС-наследия (аудит модуля построчно 5 проверяющими): модуль ресейлит ПОКАЗЫ рекламы,
а не рассылку сообщений — вырезаны выдуманные сущности, механически скопированные из СМС:
- «имя/бренд отправителя» + помесячная абонплата (в телеге МТС имени отправителя нет):
ChargeTgNameFeeJob, TelegramSenderService, SenderController, модели Sender/Setting,
фронт-панель TelegramSenderPanel, админ-карточка, updateTgSettings, тип tg_name_fee.
Таблицы client_tg_senders/_settings дропнуты.
- мёртвые таблицы client_tg_messages (доставка по номеру) и client_tg_templates (шаблоны
сообщений) — без модели/использования, дропнуты (миграция 000017, create-миграции удалены).
- client_tg_optouts переосмыслен как ручной список «не показывать этим номерам» (не «отписки»).
- лексика рассылки → рекламы: «Авторассылка»→«Авто-реклама», «кошелёк/заморожено»→«общий баланс».
Медиа объявления (реальный пробел показов): новый POST /api/telegram/campaigns/{id}/media
(картинка/видео к черновику, png/jpg/gif/mp4 ≤50 МБ, замена удаляет старый файл) + поле
загрузки в форме кабинета. Робот уже принимал media_path (RunTelegramCampaignJob → mediaFile
→ cabinet.js fillAdMedia) — не хватало только приёма файла от клиента.
Схема: db/CHANGELOG_schema.md v8.94 (CHECK += tg_ad_charge/tg_ad_refund) + v8.95 (дроп
СМС-таблиц, tg_name_fee убран). Тесты зелёные поштучно (квирк партиций — гонять по одному):
Schema/Models/AdminTg/CampaignCharge/CampaignApi/Audience/Media; фронт advertising-telegram-view
19 + admin-tg/auto-rule 24. pint/deptrac(0) чисто; larastan исключён (Pest-$this шум). Робот
bots/mts-telegram-ads НЕ трогали.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
5fd18c35b1 |
feat(смс-клиент): экран объясняет цену и говорит, во сколько рассылка начнётся
Этап 3 «Время и цена», Task 7. Строки листа 3.11, 3.12, 3.13, 3.15. Клиент видел одну цифру — цену за штуку — и не понимал, откуда она взялась и почему завтра будет другой. Теперь смета проговаривает всё вслух: сколько СМС ушло в этом месяце, какая ступень была бы без этого заказа, какая выходит с ним, и что 1-го числа счётчик обнулится. У первой рассылки месяца формулировка другая: сказать новому клиенту «вы были на 9.00 ₽» — неправда, он ни на чём не был (В-115). Отдельно — предупреждение до нажатия кнопки (решение владельца В-86): «Сейчас 21:30 по Москве — рассылка начнётся завтра в 01:00 по Москве». Оба часа приходят С СЕРВЕРА: на компьютере клиента может стоять что угодно, а решение об окне принимает сервер. И всегда сказано, ЧЬЁ это время (В-117): окно 10–20 у получателя местное, и камчатское утро — это час ночи в Москве. Берётся самое раннее утро среди получателей. Про тех, у кого регион ещё не выяснен, экран говорит отдельно: момента начала у них нет вовсе, мы не знаем, когда узнаем регион (В-116). Промолчать было нельзя — человек ждал бы отправки, которой сегодня не будет. Правило часов осталось в одном доме (SmsQuietHours::earliestOpening), формула цены — в своём (ClientSmsPricing::explainFor, счётчик месяца спрашивается один раз на все три числа). Экран по-прежнему ничего не считает сам — ни денег, ни часов. 🔴 Две неправды, которых не видел ни один тест. · Самопроверка diff'а: цена за СМС стояла на экране дважды (В-119). Разойтись эти цифры не могли, но человек читает две одинаковые цены рядом как две разные. Оставил одну — в смете, там, где рядом объяснено, откуда она взялась. · Живой прогон с ДЛИННЫМ текстом вскрыл давнюю, с Этапа 1, нестыковку (В-120): смета писала «Уйдёт 43 СМС — 1462.00 ₽», хотя 43 — это НОМЕРА, а СМС выходило 172. Цифра и сумма в одной строке не сходились между собой, и ни один тест этого не ловил: все брали короткий текст, где числа случайно совпадают. Теперь «Уйдёт на 43 номера — это 172 СМС, 1462.00 ₽», число СМС считает сервер. Починено и в окне подтверждения. Прогоны: СМС 239/239 (пачками по 3–4 файла, В-112), приём лидов 17/17, фронт 1674 зелёных, phpstan 0 своих, pint чисто, проверка типов — 8 чужих давних. Вырезами проверено пять раз: убрал ступень «без заказа» из ответа — покраснел тест 3.12; убрал «окно открыто — значит сразу» — покраснел контрольный «днём начала нет»; взял первое утро вместо самого раннего — покраснел камчатский тест; сделал фразу всегда «вы были» — покраснел тест первой рассылки месяца; показал сноску всегда — покраснел контрольный. Живьём (21:30 МСК, песочница, локальная база): три номера — Москва, Камчатка и один без региона, 99 отправленных СМС в журнале. Экран показал разом все шесть строк. Парные проверки: расширил окно до 23:00 — фраза про час исчезла; обнулил накопленное — цена вернулась к 9.00, фразы про удешевление и сноски не стало; заказ на 172 СМС без накопленного — вторая формулировка про удешевление. Стенд возвращён: окно 10–20, 0 контактов, 0 сообщений, 0 рассылок, 0 снимков. 🪤 Урок про стенд: тестовую базу чинить ТОЛЬКО `migrate:fresh` с чистого листа. `db:wipe --drop-types` + ручной `migrate` оставляют её полумёртвой — чужая миграция чата ломается на недочищенной базе, и дальше все прогоны врут «таблицы не существует». |