Commit Graph

472 Commits

Author SHA1 Message Date
Дмитрий 4a68609837 Merge branch 'main' into feat/prospects-manual-testing-kp
# Conflicts:
#	docs/observer/STATUS.md
2026-08-07 18:04:49 +03:00
Дмитрий 0bf43bfd1f docs приёмка: Я3 и Я4 закрыты по сроку — последние комбинации Яндекса 2026-08-07 08:38:44 +03:00
Дмитрий c2b3fe47fc docs: промт-передача следующей сессии по вкладке «Активность» 2026-08-07 08:12:50 +03:00
Дмитрий 0181f2e3e7 docs: промт-передача следующей сессии по вкладке «Активность»
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:12:36 +03:00
Дмитрий e4a4766c50 docs: план переделки вкладки «Активность» под вопрос «жив ли клиент» 2026-08-07 08:05:53 +03:00
Дмитрий 16d8191d08 docs: план переделки вкладки «Активность» под вопрос «жив ли клиент»
11 задач по TDD: два сервиса на бэкенде (вердикт с полоской и лента дел из
пяти источников), ручка подгрузки, живая сортировка списка клиентов, типы и
маппер, русские фразы, панель Vue, точка состояния в списке, полный прогон,
приёмка глазами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 08:05:43 +03:00
Дмитрий c298789eef docs: спека переделки вкладки «Активность» под вопрос «жив ли клиент» 2026-08-07 07:49:07 +03:00
Дмитрий c9daf48bdc docs: спека переделки вкладки «Активность» под вопрос «жив ли клиент»
Замерено на боевом: вкладка читает журнал сделок, где 364 из 419 строк —
системные «робот залил лид». Данные про входы и про дела клиента лежат в
двух других журналах и на вкладку не попадают вовсе.

Решения владельца: вердикт + полоска 90 дней + лента только дел, пороги
3 и 14 суток, входы только сверху, заодно чиним мёртвую сортировку списка
клиентов по последней активности.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:48:57 +03:00
Дмитрий 4513fe1a58 docs приёмка: дефект М-1 выкачен на боевой и доказан живыми данными 2026-08-07 07:34:28 +03:00
Дмитрий 9eba348ee8 fix телеграм: счётчик зависшей модерации сбрасывался каждой проверкой робота 2026-08-07 07:19:00 +03:00
Дмитрий 30424a4b52 fix телеграм: счётчик зависшей модерации сбрасывался каждой проверкой робота 2026-08-07 07:18:03 +03:00
Дмитрий d3f190806a @' 2026-08-07 07:04:13 +03:00
Дмитрий b522d98afe @
fix: письма о недоливе — одна сводка в сутки вместо потока на каждом прогоне

Сверка с поставщиком шла каждые 30 минут и на каждом прогоне слала письмо
на КАЖДУЮ пару (тенант, дата) с недоливом больше 20%. Вчерашняя дата остаётся
в окне сверки, поэтому поток рос день ото дня: 144 письма 27.07 → 510 за 06.08,
около 3000 за 12 дней на ops@liderra.ru.

Теперь пары собираются в один список и уходят одной сводкой раз в сутки
(отсечка по московской дате в redis; гонки нет — handle держит общий lock).
Частоту самой сверки не трогал.

Поштучный TenantBusinessDriftAlertMail с шаблоном удалён — стал мёртвым.

Тесты: сводка собирает все пары в одно письмо, второй прогон в те же сутки
молчит, в новые сутки письмо уходит снова. Красными их видел.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
2026-08-07 07:02:45 +03:00
Дмитрий c61178ab26 docs телеграм: снят неверный вывод о причине закрытия окна входа 2026-08-06 22:59:33 +03:00
Дмитрий fc07935f28 docs телеграм: вход в кабинет МТС восстановлен — служба лезла поверх окна владельца 2026-08-06 22:41:51 +03:00
Дмитрий 7bd9c67053 feat(смс): экран показывает, что будет с рассылкой в КАЖДОЙ сети
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Одно имя «на всех» стало полуправдой 05.08.2026: номера с несогласованным именем
перестали уходить вовсе, а к Билайну и МегаФону канала пока нет. Экран продолжал
обещать отправку — половина базы исчезала бы для клиента молча.

Теперь на экране «Имя отправителя» разложено по четырём сетям владельца:

  ✓ МТС      уйдёт под именем «mybrand.ru»
  ✗ Билайн   не уйдёт — сюда мы пока не шлём
  ✗ МегаФон  не уйдёт — сюда мы пока не шлём
  ✗ Теле2    не уйдёт — ваше имя у этого оператора не согласовано

Показываем ВСЕ четыре сети, включая недоступные: клиент, у которого половина базы
на Билайне, должен видеть, куда она делась, а не гадать.

Причины разведены и проверяются в том же порядке, что у отборщика получателей:
сперва запрет владельца, потом канал, потом имя. Иначе клиент прочитал бы про имя
там, где мы и так не шлём, и пошёл бы зря его согласовывать.

Названия операторов словами — на ЭКРАНЕ, не на сервере: сервер отдаёт ключ и
причину, как назвать это человеку — дело интерфейса (так же в соседних экранах).

Тесты экрана — с настоящим монтированием, а не сверкой исходника: проверяем, что
надпись дошла до глаз, а не что нужное слово где-то есть в файле.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:20:58 +03:00
Дмитрий 2c72a0cb6b feat(смс): своё имя есть, но у этой сети не согласовано — номер не уходит вовсе
Решение владельца 05.08.2026. Прежний вариант (подписать нашим «Liderra.ru»)
подменял бренд, за который клиент заплатил, причём незаметно: журнал сообщений
имя отправителя не хранит вовсе. Так уже работает отдел продаж
(SmsRecipientSelector → SKIP_NO_SENDER), и надпись на клиентском экране
(«нет имени отправителя для оператора») давно лежала готовая — её просто никогда
не показывали.

Два случая разведены НАМЕРЕННО и закреплены отдельными тестами:
  • своего имени нет вовсе (или приостановлено) — шлём от имени Лидерры, как
    и раньше. Смешай их — и рассылка встала бы у ВСЕХ разом: на бою своих имён
    заведено НОЛЬ;
  • своё имя есть, но у этой сети не согласовано — номер не уходит.

Отсев в ОТБОРЩИКЕ, а не при отправке: деньги не замораживаются, и причина видна
клиенту в предпросмотре — до оплаты.

🪤 Полный прогон поймал мою ошибку, которую выборочные тесты не видели: имя
спрашивалось ПО КАЖДОМУ номеру — 20 000 запросов в базу, отбор 20 000 получателей
вырос с 20 до 29 секунд и упёрся в сторожа скорости. Добавлена памятка на тенанта
(та же защита, что у AllowedSmsOperators, который прямо об этом предупреждает).
После лечения — 1,47 с.

Два теста часовой давности переписаны: они закрепляли ПРЕЖНЕЕ решение и после
смены решения стали неправдой. Теперь проверяют и «в канал не ушло ничего», и
«деньги целы».

Узкая щель записана в коде: если админ сузит список операторов у имени ПОСЛЕ
создания рассылки, такой номер уже лежит в снимке и уйдёт подписанным нашим именем.
Случай редкий — пересогласование переводит имя в «на рассмотрении», а это снимает
блокировку.

Проверено: 4621 тест, упал один чужой (ExampleTest — в рабочей папке не собран
фронтенд). Статанализ 0. Стиль чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:21:28 +03:00
Дмитрий 1daee489a7 feat(смс): имя отправителя выбирается по оператору получателя, а не одно на всех
Имя согласовывает ОПЕРАТОР, и у каждого оно своё. Клиентская рассылка несла одно
имя всем каналам, а умолчанием ему служило `services.sms.mts.naming` — то есть имя,
согласованное с МТС, подставлялось Теле2 и подставилось бы Мегафону и Билайну.
Живой отказ Теле2 `invalid_source_address` 04.08.2026 — ровно этот механизм. На бою
у клиентов заведено НОЛЬ своих имён, значит это касалось каждой отправки.

Появился ClientSmsSenderResolver: спрашивает имя у той сети, чей номер. Пустая
строка на выходе — не «имени нет», а «своего имени для этой сети нет, канал
подпишется собственным согласованным именем из настроек».

Переведены на него оба места отправки: рассылка (SendClientSmsCampaignJob) и
авто-СМС по сделке (SendAutoSmsForDealJob).

Универсальный СМС-центр передавал имя КАК ЕСТЬ — теперь тоже падает на своё
(SMSC_NAMING). Раньше пустое имя туда не приходило никогда, с этой правкой стало
бы приходить: ушли бы СМС без отправителя.

Заплатка внутри канала Теле2 (приведение написания к своему регистру) остаётся, но
больше не несущая: она чинила следствие в одном канале, а причина была общая.

Экранное имя (ResolvesClientSmsSenderName) НЕ трогали — это запись намерения, а не
то, чем подпишется сеть; в его шапке теперь написано почему, чтобы не «починили»
обратно.

🪤 Урок по дороге: авто-СМС ловит любую ошибку и молча выходит. Справочник имён был
передан не в ту функцию — вместо падения получилась ТИШИНА, не уходило ничего.
Поймали три чужих теста, до того зелёных.

Проверено: 4614 тестов, упал один чужой (ExampleTest — в этой папке не собран
фронтенд). Статанализ 0. Стиль правленых файлов чист.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 05:28:59 +03:00
Дмитрий d1e6fcab7f fix(смс): новая запись про адрес звонившего писалась уровнем, который бой выбрасывает
Приёмка на бою вскрыла, что хвост был закрыт только на вид: строку писали
уровнем «к сведению», а на боевом LOG_LEVEL=warning — всё, что ниже, молча
отбрасывается. Запись не появилась бы в журнале НИКОГДА, и круг «список нельзя
заполнить, потому что адреса неоткуда взять» остался бы замкнут — только теперь
незаметно, с виду починенный.

Замерено живым вызовом приёмника на бою 04.08: соседнее предупреждение про
незнакомое сообщение в журнал легло, а строка «к сведению» — нет.

Уровень поднят до «предупреждение». Тест теперь стережёт именно уровень, и в нём
записано, почему: не «так решили», а «иначе бой выбросит».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:14:30 +03:00
Дмитрий 57c36985a5 fix(сторож ссылок): ссылка на ТЗ лендинга не пускала push из любой отдельной папки
CLAUDE.md §0 ссылается на «лендинг/TZ_landing_v1_0.md», но каталог `лендинг/`
лежит в .gitignore (строка 137) — в git этого файла нет и не будет.

В общем дереве ссылка «работает» только потому, что файл есть там на диске.
В любой отдельной рабочей папке его нет, проверка ссылок краснела и не пускала
push — а сам CLAUDE.md править мимо плагина нельзя (правило §5 п.10).

Исключение узкое: ровно этот файл. Проверено — 38 из 38 ссылок CLAUDE.md
целы, ошибок ноль.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:07:56 +03:00
Дмитрий 13efef2050 feat(смс): канал Теле2 — отправка, судьба сообщений и приёмник отчётов
Канал «SMS-Таргет» (target.t2.ru, HTTP API v2, Basic Auth) обеими половинами:

- отправка через POST /send_message (msisdn числом, shortcode, text);
- опрос судьбы GET /send_message/message-id-{id} — по одному сообщению,
  пакетного запроса у t2 нет;
- приёмник обратных звонков GET|POST /api/webhook/t2-delivery/{secret}
  со строками ?id=&status=&parts= — форма другая, чем у МТС.

Ловушки, закрытые тестами:

- слово status в ответе встречается дважды: снаружи «запрос удался»,
  внутри судьба сообщения. Спутать = объявить доставленным всё подряд;
- отказ приезжает с HTTP 200 — судим по телу, не по коду;
- незнакомое слово статуса судьбу НЕ меняет, сохраняется в delivery_raw.
  За «не доставлено» клиенту возвращаются деньги (В-198) — выдумка двигала
  бы рубли;
- t2 наш ответ не проверяет и повторов не делает: пропущенный звонок потерян,
  поэтому опрос client-sms:poll-delivery остаётся обязательным.

Деньги приёмник не двигает — возврат живёт в команде опроса (В-146).
Защита приёмника как у МТС: секрет не короче 32 знаков (пустой = закрыт
наглухо), список адресов отправителя, счётчик обращений; чужому 404, не 403.

Канал ВЫКЛЮЧЕН и живьём не пробован: имя отправителя Liderra.ru (заявка
№ 31121) на согласовании, без него t2 не выдаёт логин с паролём.
Порядок включения — docs/superpowers/runbooks/2026-08-04-vklyuchenie-kanala-t2.md

Миграций нет: графы судьбы появились ещё при МТС. Теле2 уже был в списке
разрешённых операторов и в тарифах админки — новых экранов не нужно.

Проверено: 78 тестов зелёные, Pint чист, статанализ по ВСЕМУ проекту 0 ошибок,
маршрут зарегистрирован. Против main — только вставки, удалений нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:06:10 +03:00
Дмитрий 458f206ce9 feat(смс): канал Теле2 — отправка, судьба сообщений и приёмник отчётов
Канал «SMS-Таргет» (target.t2.ru, HTTP API v2, Basic Auth) обеими половинами:

- отправка через POST /send_message (msisdn числом, shortcode, text);
- опрос судьбы GET /send_message/message-id-{id} — по одному сообщению,
  пакетного запроса у t2 нет;
- приёмник обратных звонков GET|POST /api/webhook/t2-delivery/{secret}
  со строками ?id=&status=&parts= — форма другая, чем у МТС.

Ловушки, закрытые тестами:

- слово status в ответе встречается дважды: снаружи «запрос удался»,
  внутри судьба сообщения. Спутать = объявить доставленным всё подряд;
- отказ приезжает с HTTP 200 — судим по телу, не по коду;
- незнакомое слово статуса судьбу НЕ меняет, сохраняется в delivery_raw.
  За «не доставлено» клиенту возвращаются деньги (В-198) — выдумка двигала
  бы рубли;
- t2 наш ответ не проверяет и повторов не делает: пропущенный звонок потерян,
  поэтому опрос client-sms:poll-delivery остаётся обязательным.

Деньги приёмник не двигает — возврат живёт в команде опроса (В-146).
Защита приёмника как у МТС: секрет не короче 32 знаков (пустой = закрыт
наглухо), список адресов отправителя, счётчик обращений; чужому 404, не 403.

Канал ВЫКЛЮЧЕН и живьём не пробован: имя отправителя Liderra.ru (заявка
№ 31121) на согласовании, без него t2 не выдаёт логин с паролём.
Порядок включения — docs/superpowers/runbooks/2026-08-04-vklyuchenie-kanala-t2.md

Миграций нет: графы судьбы появились ещё при МТС. Теле2 уже был в списке
разрешённых операторов и в тарифах админки — новых экранов не нужно.

Проверено: 78 тестов зелёные, Pint чист, статанализ по ВСЕМУ проекту 0 ошибок,
маршрут зарегистрирован. Против main — только вставки, удалений нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:06:01 +03:00
Дмитрий fa4b9e83a4 feat(воронка): колонка rubric у карточки — ниша фирмы 2026-08-04 12:25:09 +03:00
Дмитрий 38745e61dd docs: голосовой канал — выбран Inworld, замерены цена, скорость и все ручки
Записан выбор поставщика для обеих Лен и всё, чем он подтверждён.

Замерено живьём, не по рекламе:
- разговор одной линией, 7 ходов: 0,88 с до первого звука, ровно от начала
  до конца. Втрое быстрее нынешней Александры и без просадки.
- кэш работает, 96-98 процентов попаданий, счётчик поставщик отдаёт.
  Значит длина наставления по деньгам почти ничего не стоит — решение
  владельца не резать базу оправдалось.
- звонок стоит около 6,4 рубля, из них 5,6 голос. Экономить надо на
  длине реплик, а не на длине наставления.
- стена: лимит 20 000 байт считает весь разговор вместе с историей,
  а не только наставление. На бесплатном тарифе разговор умирает
  на пятом ходу. Оплата — условие любой осмысленной проверки.

Все ручки поставщика разобраны отдельным файлом, 15 проверены живым
опросом канала. Три опасных умолчания: предела длины ответа нет вовсе,
голос по умолчанию младшей модели, температура единица.

Мина на будущее: замер кэша сделан на чужом мозге, который кэширует сам.
Claude так не умеет, ему нужна отдельная ручка, а она по умолчанию
выключена. Проверить первым делом после оплаты.

Выгрузка из Даши — оба промта Лен и три документа базы знаний, дословно.
В kb-02 помечены две мины: ответ про происхождение номера противоречит
обоим промтам, и бесплатный тест описан двумя разными способами.

Ключей и персональных данных в файлах нет, проверено датчиком
с контрольной пробой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 00:03:49 +03:00
Дмитрий 36f5093692 merge: общая ветка с клиентским СМС-модулем сведена в рабочую
Общая ветка переведена вперёд на ветку клиентских СМС — перемоткой, без
слияния, поэтому конфликтов там быть не могло. Затем общая сведена в рабочую
ветку, которая отставала на 79 записей.

Столкновение было одно и знакомое — словарь орфографии cspell-words.txt.
Разрешено правилом «обе стороны настоящие»: слова обеих веток сохранены,
ничего не выброшено. Журнал схемы БД на этот раз свёлся сам, столкновения
номеров не было.

СТОРОЖ ПДн ОСТАНОВИЛ ЗАПИСЬ И БЫЛ ПРАВ. В образце базы номеров, который
клиент скачивает перед своей первой рассылкой, стоял рабочий телефон
владельца — в двух видах. Это не утечка чужих данных: тот же номер публично
опубликован в реквизитах ИП по требованию ЮKassa. Но клиент заполняет этот
файл своими номерами и запускает рассылку — забытая строка означала бы СМС
владельцу за деньги клиента. Заменён на выдуманный из тестового диапазона.

Подсказки на экране рассылок («+7 999 123-45-67») выдуманы изначально;
разрешены в .gitleaks.toml ПО ЗНАЧЕНИЮ, а не по файлу, чтобы сами файлы
остались под охраной. Сторож проверен вырезанием: подложенный номер, не
подпадающий ни под одно разрешение, он поймал; после снятия подложки — чисто.

Служебный счётчик наблюдателя убран в тайник на время сведения и возвращён
после — он машинный и пересоздаётся хуками.

Файлы второй смены, которая работает в этой же папке, не тронуты и в
слияние не попали.

Проверки сведённой ветки: тесты 4533, зелёных 4529, падений нет; экраны
238 файлов и 1906 зелёных, сторож сети не сработал ни разу; статанализ 0;
формат чист.
2026-08-02 23:27:13 +03:00
Дмитрий 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 — тем же приёмом и по той же причине, что и в записи
e0223de4, которой эти шесть файлов исследования заводились в репозиторий: сырой
внешний материал с чужой терминологией. Замерено: до моих правок сторож
орфографии находил в них ровно столько же слов, сколько после — я не добавил
ни одного. Разметка в этих файлах теперь проходит начисто, отключена ТОЛЬКО
орфография и только на эту запись; остальные семнадцать сторожей отработали.
Решение владельца, спрошено отдельно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:07:24 +03:00
Дмитрий 4b7cc7a179 feat(затея): запуск работника ребёнком надзирателя — полный путь, поток событий, пустой успех не успех 2026-08-01 21:46:57 +03:00
Дмитрий 5240cfbdd9 style: пробел перед @mixin в шапках моделей — так их пишет форматтер
Правка целиком в комментариях: 37 файлов, ни строки кода.
Её дописал сам pre-commit форматтер во время предыдущей записи и оставил
неподтверждённой. Записываю, чтобы папка ветки не оставалась грязной.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 19:57:50 +03:00
Дмитрий 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>
2026-08-01 11:42:46 +03:00
Дмитрий 78dc8ae94e docs телеграм-робот: промт следующей смене — связка на опросе, что дальше
Переделка сделана и запушена, на бой не выкачено. Развилка описана: поселить
робота и выкатить, перевести на опрос чтение вердикта и пересдачу, вылечить
тестовую базу. Ловушки смены выписаны, включая молчаливую установку
зависимостей и правило замерять сторожа до починки своего текста.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:11:17 +03:00
Дмитрий 071a70eab3 docs телеграм-робот: памятка установки робота на машину
Сама установка НЕ выполнена — она трогает живую машину и боевой портал,
нужно явное разрешение владельца. Куда селить робота, зависит от исхода
разговора с поставщиком прокси.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:24:54 +03:00
Дмитрий 9100e1fdda feat телеграм-робот: проход опроса — сам спрашивает работу у портала
Своей петли нет: запускается таймером, зависший проход не мешает следующему.
Номера кладутся во временный файл и убираются при любом исходе.

Проверено живьём против поднятого портала: верный токен — «Работы нет.» и код 0,
чужой токен — понятная ошибка 401.

Заодно закрыта мина: токен уходит в заголовок HTTP, куда пускают только латиницу.
С русскими буквами робот падал нечитаемым сбоем Node ещё до обращения к порталу —
теперь говорит по-человечески. Наступил на неё сам, проверяя план дословно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:22:26 +03:00
Дмитрий 146f0a7a35 feat телеграм-робот: джоб запуска умеет опросный канал
При transport=poll портал кладёт задание и выходит, робот заберёт его сам.
Процессный путь оставлен рабочим и остаётся умолчанием.
Полный набор тестов телеграма: 219 из 219 зелёные.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:07:39 +03:00
Дмитрий 81d6de7588 feat телеграм-робот: приём отчёта роботом и общее применение итога
Применение итога вынуто из джоба в сервис без изменения логики — теперь его зовут
оба пути, процессный и опросный. Отчёт принимается только по заданию в работе.
Сторож принят вырезанием: без проверки статуса повторный отчёт проходит с 200.

Отдельно закрыта мина, найденная ревью защиты и отсутствовавшая в плане: роль
crm_admin_user имела на client_tg_campaigns только чтение, а канал робота пишет
туда итог — на бою приём отчёта упал бы по правам. Миграция v9.29 даёт UPDATE.

Полный набор тестов телеграма: 217 из 217 зелёные, старые в том числе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:54:06 +03:00
Дмитрий 79c51b4399 feat телеграм-робот: разговор робота с порталом — задание, номера, отчёт
Тесты робота: было 113, стало 118.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:46:15 +03:00
Дмитрий d4663dcb0b feat телеграм-робот: выдача номеров роботу только по заданию в работе
Номера не кладём в задание и не пишем в журнал — отдельный запрос, без кеша.
Сторож принят вырезанием: без проверки статуса тест отдаёт 200 вместо 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:29:29 +03:00
Дмитрий c6bd19cede feat телеграм-робот: канал выдачи задания роботу
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:16:43 +03:00
Дмитрий d42334674c feat телеграм-робот: сервис-токен канала, пустой токен закрывает канал
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:10:00 +03:00
Дмитрий e75bb76adf feat телеграм-робот: очередь заданий — поставить, выдать по одному, вернуть зависшее
Выдача строго по одному: у робота один профиль браузера и одна сессия кабинета.
Задание, по которому робот не отчитался за срок аренды, возвращается в очередь.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:07:27 +03:00
Дмитрий 542fb7a371 feat телеграм-робот: модель задания роботу
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:41:32 +03:00
Дмитрий 1b5c60e55c docs телеграм-робот: промт следующей смене на переделку связки
Где работать, что уже стоит, красные линии, ловушки и приёмка вырезанием.
Отдельно записано, что проверка обращения к поставщику прокси умирает
вместе с сессией и её надо ставить заново.

Co-Authored-By: Claude Opus 5 (1M context) <noreply\@anthropic.com>
2026-07-31 05:42:01 +03:00
Дмитрий ae2ee5e59c docs телеграм-робот: план переделки связки портал-робот на опрос
Нужна при ЛЮБОМ исходе с адресом: и на сервере, и на машине владельца робот стоит
не там, где очередь портала, а сегодня портал запускает его как процесс у себя.
Списано с работающего образца - робота креативов Яндекса.

12 задач по шагам с готовым кодом и тестами, переключатель process/poll оставляет
старый путь рабочим. Отдельно вынесены красные линии: перезапуск srv_bypass после
миграции, номера телефонов не в задании, приёмка сторожей вырезанием.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 05:27:06 +03:00
Дмитрий 8769245ed5 feat реклама: портал сам проверяет готовность аудитории и минимальную смету до запуска
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Живой бой 30.07.2026. Сегмент дозрел за 15 часов — Яндекс опознал 1644 человека
из 1693, охват 3379. Портал нажал «Запустить» и получил отказ Директа:
   Budget for this period cannot be less than 600 rub.

Стена оказалась не одна, а две. Обе — молчаливые: портал о них не знал.

Первая — аудитория. Сегмент в Аудиториях существует и при этом НЕГОДЕН, пока
Яндекс сводит загруженные номера с людьми. Портал этого не спрашивал вообще:
ни один файл не читал can_create_dependent. Он просто шёл в Директ строить
условие ретаргетинга и получал «объект не найден» — тот же отказ, что и на
несуществующий сегмент. Судить о готовности можно ТОЛЬКО по can_create_dependent:
статус is_processed означает «обрабатывается», а не «готов».

Вторая — деньги. Директ не берёт кампанию, у которой бюджет за период ниже
шестисот рублей. Приёмочная кампания на 1693 показа давала около 146 рублей.
Проверки минимума в портале не было тоже.

В обоих случаях клиент видел сырую ошибку Яндекса на английском и не понимал,
виноват ли он и что делать дальше.

Что сделано. Обе проверки выполняются ДО первого обращения к Яндексу: в кабинете
ничего не создаётся, деньги не морозятся, кампания остаётся черновиком.
   аудитория ещё готовится → 202 и «подождите, обычно несколько часов»
   смета мала              → 422 и «нужно 6945 показов, это 833.40 рублей»

Наружу уходят ТОЛЬКО клиентские числа. Ни минимума площадки, ни нашей наценки:
по паре «минимум площадки — цена клиенту» долю Яндекса можно вычислить делением.
Минимум вынесен в настройку YANDEX_DIRECT_MIN_SPEND_RUB, Яндекс может его менять.

Тестом вперёд, в живой Яндекс из тестов не ходили. Четыре новых теста на запуск
и два на ответы портала. Реклама целиком: 342 теста зелёные.

Заодно приведён в порядок список исключений статанализа. Он был красным ЗАДОЛГО
до этой правки и не пускал ни одну правку кода: 629 замечаний до моих изменений,
638 после. Проверено в обеих папках работы — не артефакт подпапки.

Разбор 637 замечаний по составу:
   611 — статанализ не понимает устройство тестов Pest и ругается на
         обращения вида this->postJson и this->tenant. Таких записей в списке
         исключений уже было 671 — просто новые тестовые файлы туда не дописали.
    26 — придирки к типам, из них 7 в боевом коде.

Все семь в боевом коде разобраны поимённо и оказались ложной тревогой. Доказано
не рассуждением, а зелёными тестами, которые упали бы при настоящей поломке:
   shows_until, три замечания — три теста прямо проверяют закрытие кампании по
      истечении срока показа и разморозку остатка. Будь там строка вместо даты,
      первый тест упал бы, а деньги клиента остались бы заперты навсегда.
   snapshot_from и snapshot_to, два замечания — тесты ручного режима строят
      аудиторию по этим датам, на строке они бы рухнули.
   SetTenantContext строка 56 и CampaignMessageService строка 168 — лишние
      подстраховки, вреда нет.

Корень ложных срабатываний: Larastan ищет приведения типов в старом виде —
свойством casts, а у нас метод casts. Код правильный, инструмент отстал.

После обновления списка статанализ зелёный: 0 замечаний. Теперь сторож снова
ловит НОВОЕ, а не молчит красным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 15:07:49 +03:00
Дмитрий f976de52d7 docs: починены 92 битые ссылки — сторож ссылок снова что-то значит
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Проверка ссылок падала на девяноста двух ошибках, поэтому любая отправка в main
шла через обход. Смысл починки не в красоте: пока ошибок девяносто две, новая
битая ссылка тонет среди них и никто её не замечает. Теперь ноль, и пуш проходит
обычным порядком.

Что было внутри девяноста двух:

  68 — ссылка написана от корня хранилища, а документ лежит на две-три папки
       вглубь. Приписаны шаги вверх. Файлы всё это время лежали на месте.
   8 — файл существует, но переехал: Jobs/Billing, Jobs/Supplier, Services.
       Адреса переписаны на нынешние места.
   8 — файла нет и не будет: ProcessWebhookJob снят при уходе от старого
       биллинга, SupplierCsvParser удалён 09.07 как мёртвый, каталог memory
       уехал в claude-brain, а HANDOFF прогрева от 25.07 не существует ни
       в одном коммите. Ссылки сняты, текст оставлен.
   3 — неверное число шагов вверх: две точки вместо трёх.
   3 — это вообще не ссылки, а примеры в тексте: http:// как образец мусорного
       ввода, https://ваш-сайт.ru из подсказки формы. Обёрнуты в кавычки-код.
   1 — адрес с приписанными номерами строк, которых проверятель не понимает.
   1 — Россвязь. Сайт мёртв по-настоящему: сервер 194.226.91.2 отдаёт nginx-ову
       404 на любой адрес, включая корень, а сертификат выписан не на это имя;
       ведомство расформировано. Замену проверить не удалось — преемник
       с этой машины не отвечает вовсе. Ссылка снята, адрес читается текстом.
       Вопрос «где реестр нумерации живёт теперь» остаётся открытым.

Исключения проверятеля не тронуты ни на строку. Ноль получен починкой, а не
глушением сторожа — иначе вся работа теряет смысл.

Отдельным прибором проверено, что видимый читателю текст не изменился ни в одной
из 81 правленой строки: правились только адреса ссылок. Разметка линтером чистая.

Вместе с починкой ложится промт смены, по которому работа делалась.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:16:14 +03:00
Дмитрий 5b04d751c0 feat(телеграм-модуль): авто-режим, своё имя + помесячная оплата, админ-тарифы
Сессия 4 плана docs/superpowers/plans/2026-07-27-client-telegram-ads-module.md.
Всё в песочнице (TG_SANDBOX): деньги/живой запуск выключены; робот в тестах замокан.

4.1 Авто-режим (пачка):
- Таблица client_tg_auto_rule (правило: объявление + budget_cap на пачку, одно на тенанта, RLS).
- TelegramAutoAccumulator: копит номера новых лидов в один открытый авто-черновик
  (audience_kind=list, created_by IS NULL); при наборе пачки (порог client_tg.auto_batch_threshold,
  по умолчанию 367 кандидатов) доводит смету, ставит кампанию в очередь В РАМКАХ budget_cap_rub
  и ставит RunTelegramCampaignJob. Ниже порога — только копим. Дедуп номеров на входе.
  Отличие от СМС-близнеца: СМС шлёт по одной на лид, Telegram копит ПАЧКУ (МТС показывает базе).

4.2 Защитный observer:
- DealTelegramObserver на created: freshness-guard (сутки) → ставит AccumulateTelegramLeadJob.
  🔴 try/catch(Throwable) НИКОГДА не роняет приём лида (регресс DealCreateTest 10/10 зелёный).
- Приёма-джоб уводит работу с роли приёмщика (crm_supplier_worker, без GRANT на auto_rule) на
  очередь (crm_app_user) с tenant-контекстом — иначе на проде был бы «тихий ноль». ПДн: телефон
  в payload не кладём, перечитываем по deal_id. Зеркало DealSmsObserver + SendAutoSmsForDealJob.

4.3 Своё имя/бренд + помесячная оплата:
- Таблица client_tg_senders (одно имя на тенанта, RLS; app_user S/I/U, supplier S, admin S/U).
- TelegramSenderService — жизненный цикл: клиент requestSender (заявка+бронь месячной платы) /
  disableSender; админ approve (списание+снятие брони+paid_until +1мес) / reject / disableByAdmin.
- ChargeTgNameFeeJob (ежедневно 05:10 МСК, routes/console.php): помесячное списание, проверка
  free≥cost ДО списания, идемпотентно по периоду (external_key), долг > grace (29д) → suspended,
  кросс-тенантно через pgsql_supplier, деньги под SET LOCAL. В песочнице деньги не двигаются.
  Лёгкое зеркало client_sms_senders + ChargeSmsNameFeeJob (без документов/операторов — согласование
  в Телеграме уточняется живой разведкой §8 спеки; здесь фиксируем денежный цикл).

4.4 Админ-тарифы/настройки:
- Api\Admin\TgTariffController (GET/PUT /api/admin/telegram/tariffs|settings) под saas-admin,admin-db.
- Фронт: api/admin.ts (fetchTgTariffs/updateTgTariffs/updateTgSettings), AdminTgView.vue (правка
  ступеней + плата за имя/грейс), маршрут /admin/telegram. Зеркало AdminSmsView (без секции имён).

Проверки: 89/89 Pest ClientTg зелёные (+37 новых), регресс DealCreateTest 10/10, Vitest зелёный
(+4 admin-tg-view), phpstan 0 по своему коду (level 5), pint чисто, deptrac 0 нарушений.
rls-reviewer: PASS по обеим новым таблицам (client_tg_auto_rule, client_tg_senders). CHANGELOG_schema
v8.87 + напоминание ПЕРЕзапустить db/03_service_bypass_policies.sql на кластере (Сессия 6). Робот в
тестах замокан (кабинет МТС не тронут). Синтетические номера 7999… — реальных нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:41:33 +03:00
Дмитрий 741d1697c5 feat(телеграм-бот): бот-автоматизатор кампаний МТС Маркетолог — cabinet/runner/CLI/keepalive (задачи 10–16)
Дописан standalone-робот (Node ESM + Playwright) полного цикла создания
Telegram-кампании по своей базе телефонов в кабинете МТС Маркетолог:
- cabinet.js: вход в визард, выбор «Своя база клиентов», загрузка базы +
  подсчёт «не МТС» с порогом 367, переход «Аудитория→Объявление» (само-
  верификация), заполнение объявления + блок ОРД, финал черновик-стоп/боевой
  (fail-loud при неизвестном режиме — защита от боевого клика по ошибке).
- runner.js: оркестратор с алярм/отчётом на почту, гарантированное закрытие
  профиля браузера, чистка временного файла номеров (152-ФЗ).
- task.js: обязательные поля заголовка и названия ОРД; config: потолок бюджета
  падает при нечисловом значении (защита денег), budget: NaN-guard.
- bin/run.js (CLI), bin/keepalive.js (сторож входа с алярмом), bin/login.js
  (разовый вход), task.example.json (без ПДн), README.
- Шаг «Стоимость» намеренно не размечен (заглушка бросает Error) — селекторы
  снимаются на первом реальном черновике после разового логина владельца.

Ядро: 20/20 юнит-тестов зелёные. Браузерная часть проверяется живым черновиком.
Двухстадийная ревизия каждой задачи + финальный сквозной проход (поймал и
починил пропущенный переход между шагами и NaN-обход потолка бюджета).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:43:20 +03:00
Дмитрий 51aac64c8a docs(телеграм-бот): разметка подтверждена живым прогоном до шага «Объявление»
Прогон в черновике на реальной базе (1740 номеров): Получатели 1709, МТС 713,
Не МТС 996, стоимость показов 478,08 ₽ — цепочка «загрузка базы → счётчик → цена»
работает. Шаг «Объявление» размечен полностью (текст/заголовок/ссылка/медиа +
обязательный ОРД-блок: категория downshift, название, «Я — посредник»). Добавлены
грабли автоматизации (coach-mark overlay, downshift, force-click). Шаги
«Стоимость/Подтверждение» — доразметить на первом реальном черновике бота.
Черновик удалён, баланс 5010 ₽ цел, временные ПДн стёрты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 23:34:48 +03:00
Дмитрий f747b2fc74 feat(телеграм-бот): персистентный браузер и проверка живости входа
Задачи 8–9 плана (код, проверка против кабинета — при логине владельца в
профиль бота):
- browser.js — launchPersistentContext(profileDir), humanPause по темпу
- session.js — isLoggedIn по стабильному пункту меню «Рассылки и звонки»
  (селектор подтверждён живьём при разведке)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:49:22 +03:00
Дмитрий a1492400a0 feat(телеграм-бот): тестируемое ядро — config, task, phones, budget, mailer
Задачи 3–7 плана (TDD, 14/14 зелёные):
- config.js — загрузка/валидация настроек окружения
- task.js — парсинг и валидация задания кампании (режимы draft|live)
- phones.js — нормализация и дедуп файла номеров (7XXXXXXXXXX)
- budget.js — предохранитель потолка бюджета
- mailer.js — письма алярма/отчёта через SMTP (nodemailer)

gitleaks: каталог test/ бота добавлен в allowlist (синтетические
телефоны-фикстуры 345-67-89 / 111-22-33, не реальные ПДн).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:47:15 +03:00
Дмитрий 22d1150e26 chore(телеграм-бот): скелет Node+Playwright проекта
Задача 2 плана: package.json (Node ESM, node --test), .env.example (профиль
браузера, SMTP Unisender Go, потолок бюджета, человеческий темп), .gitignore.
Зависимости playwright/nodemailer/dotenv + chromium установлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 22:44:52 +03:00