Config: jivo_bot -> bot (убраны неиспользуемые webhook_secret/outbound_url/token
- транспорт Jivo уже удалён), jivosite убран целиком (виджет больше не нужен).
Env: JIVO_BOT_* -> BOT_*; JIVO_WIDGET_ID/VITE_JIVO_WIDGET_ID удалены.
tours_enabled выключен по умолчанию (BOT_TOURS_ENABLED=false) — экскурсии
«Показать на портале» уходили не по теме вопроса (живая проверка 13.07.2026),
чинить релевантность отдельно; сторож-тест не даёт включить незаметно.
Заодно убраны обнаруженные хвосты того же виджета, оставшиеся от прежних
задач: JivoLivenessProbe (класс+тест+регистрация в мониторинге внешних
сервисов), плитка «JivoSite» в админ-дашборде, встроенный скрипт виджета
в welcome.blade.php, осиротевший тест resources/js/.../JivoWidget.vue
(компонент уже был удалён ранее), declaration VITE_JIVO_WIDGET_ID.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прежний сторож проверял только «не file»: без ключа config вернул бы null,
и проверка прошла бы впустую. Теперь ключ обязан быть redis или array,
и сам класс обязан брать именно это хранилище, а не общий кэш приложения.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Файловый Cache::increment() под параллельной нагрузкой (10 x 30) досчитал 77 из 300 —
защита от накрутки счёта (пауза/час/IP/сутки) продавливалась параллельным скриптом.
ChatRateLimiter теперь берёт своё хранилище (services.jivo_bot.limiter_store, по
умолчанию redis, в тестах array) и считает через атомарные add()+increment(), а не
через фасад RateLimiter на общем кэше. Ручная проверка на живом Redis: 300 из 300.
Защита денег для публичного чата (лендинг открыт всему интернету,
каждый ответ бота ~0,44 руб): пауза 2 сек между репликами, лимит
вопросов в час (гость 20 / вошедший 60), потолок реплик на разговор
(40), лимит по IP в час (60) и общий суточный потолок ответов (1500)
с одноразовым письмом-тревогой владельцу при пробитии. Отказ всегда
вежливый — клиент получает текст, а не молчание.
Заменяет JivoBotController+JivoBotClient на ChatController (POST /api/chat/message)
и ProcessJivoMessageJob на ProcessChatMessageJob. Реплику клиента теперь пишет
контроллер и возвращает её номер (message_id); джоба получает этот номер и берёт
историю строго до него, не отправляя ответ во внешний Jivo — он ложится в bot_dialogs,
откуда его заберёт своё окошко (Задача 3). Мозг бота не менялся.
Спека: docs/superpowers/specs/2026-07-13-own-chat-widget-design.md §5
jivo_chat_id -> chat_id + добавлены source/user_id/ip (спека
2026-07-13-own-chat-widget-design §5). Историческая миграция создания
таблицы не тронута.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
В трёх статьях было написано «не берёт трубку → заменим». Это ошибка: замену
мы не делаем, и бот честно обещал её клиентам.
Тон тонкий, тему сглаживаем:
- не обещать замену/возврат («заменим», «вернём деньги») — AnswerGuard::promisesReplacement;
- но и не рубить дверью («замена не полагается», «деньги не возвращаются») — тоже режем;
- мягко: недозвон — обычное дело; совет СМС → звонок позже → мессенджер; спорный
случай на support@liderra.ru, специалист разбирает индивидуально, без обещаний;
- не выдумывать срок ответа поддержки (она 24/7).
Статьи: zamena-zayavki.md переписана, поправлены kak-rabotat-s-nomerom.md и sdelki.md.
Два старых теста охраняли прежнее (неверное) правило — переписаны под новое.
Тесты 130/130. Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- «скидка ЗА ОБЪЁМ» больше не считается просьбой скидки: бот отвечает про
ступени тарифа, а не уводит к специалисту.
- «где посмотреть мою ступень / мой лимит» — вопрос про портал, а не про
личные данные клиента: эскалация снята.
- Цена ↔ ниша/регион: правило по СМЫСЛУ (AnswerGuard::priceTiedToNiche).
Враньё «цена зависит от ниши» возвращалось ПЯТЬЮ формулировками —
регулярка на каждую не работает. Честное «цена НЕ зависит от региона»
сохраняется.
- Вырезано: «на тысячу выйдет от 2 до 20 заявок» (реально максимум 4),
«работаем не со всеми нишами» / «есть направления, с которыми мы не
сотрудничаем» (ограничений по нишам НЕТ), «не берёт трубку — забей»
(такую заявку МЕНЯЮТ), «баланс на паузе не потратится», выдуманные сроки
зачисления по счёту.
- Промпт: цена только от ступени объёма; запретных ниш нет.
- Статьи: тронута ТОЛЬКО строка topics (синонимы поиска), тексты не менялись.
Тесты 126/126. Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прогон 46 диалогами (250 реплик подряд в одном чате) вскрыл то, чего не видел
прогон отдельными вопросами:
- Клиент оставил телефон → бот не отвечал ВООБЩЕ: письмо в поддержку слалось
синхронно внутри ответа, SMTP не отозвался → упала вся задача (tries=1).
Теперь номер в чате = заявка на звонок: принимаем всегда (даже без просьбы
бота), письмо уходит через очередь и не может уронить ответ.
- Бот «забывал» себя: сторож резал цену, взятую из истории чата, ответ пустел
→ эскалация. Числа из своих прошлых ответов теперь разрешены; память чата
3 → 5 пар реплик; в эскалационную ветку тоже передаётся история.
- AnswerGuard: + телефон поддержки (его нет), + юр-гарантии в новых оборотах
(«Да, законно», «Штраф не прилетит»), + враньё «на паузе списаний нет»,
+ выдумки про 18:00/ночь, + «цена зависит от ниши»; снимает висящее «Но…»
и голое «Правильно, не будут» после вырезанной лжи.
- Промпт: не повторять сказанное, не поддакивать клиенту, без нумерованных списков.
- pravilo-18-00.md: 18:00 — граница для ИЗМЕНЕНИЙ, а не конец приёма заявок.
Тесты 120/120. Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прогнал 250 вопросов через настоящий вебхук шесть раз и сравнил четыре модели
вслепую силами судей-агентов. Итоги — в docs findings.
Модель. Появился переключатель services.jivo_bot.llm: yandex (данные не покидают
РФ) либо aitunnel (зарубежные модели через российский шлюз). По слепому сравнению
Claude Haiku 4.5 выиграл финал у Gemini 157:24 и у обеих моделей Яндекса: он
полнее, точнее по кнопкам и цифрам и говорит как человек, а не справочник.
Три замка, потому что живая модель = самоуверенная модель:
- PersonalDataMask — телефоны и почта вырезаются ДО отправки в любую модель
(клиент оставляет номер в чате; за границу ПДн уходить не должны).
- ChatTextCleaner — markdown и смайлики (Jivo их не рисует): было 191 ответ
с разметкой, стало 0.
- AnswerGuard — режет юридические гарантии («вас не оштрафуют», «мы гарантируем»,
«не нарушаем закон») и ЛЮБУЮ цену, которой нет в выданных статьях. Пусто после
чистки → зовём живого специалиста.
Поиск по инструкции переписан: выбрасываются вопросительные слова (из-за них
«какие у вас тарифы» находило выгрузку в Excel), ранжирование сперва по заголовку
и синонимам, выдача — три РАЗНЫЕ статьи и каждая целиком, плюс починка опечаток
(«каг сазадь праэкт») и учёт прошлой реплики в коротких «а это платно?».
Инструкция: 40 → 51 статья. Новые — по фактам владельца: бесплатный тест и
подарок 1000 ₽, «мы канал, а не реклама», СМС → через час звонок, средние
показатели (8–12 из 100 разговаривают, ещё 5–7 по СМС), номер не вернётся
30 дней, замена брака, вывод остатка, поддержка 24/7, удаление данных, ниши.
Вычищено враньё: минус баланса, «списали без заявки», пауза вечером, выходные,
«Вся РФ», статус «Отказ» (его нет — есть «Не реализовано»), amoCRM, платный
повторный сбор, нарастающий итог тарифных ступеней.
Тесты: 106 из 106. Каждая найденная тупость закреплена тестом.
НЕ НА ПРОДЕ. Выкат — только по прямой команде владельца.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. 🔴 МОЙ КОСЯК: на «как получить номер человека, заходящего на сайт» бот отвечал
«нельзя» — то есть отговаривал клиента от того, ЧТО МЫ И ПРОДАЁМ. Владелец:
«как нельзя, а мы чем занимаемся?!». Теперь: «это ровно то, что делает Лидерра —
создайте проект, укажите сайт, контакты пойдут в Сделки».
2. «Все ли номера с источника цепляет система?» — нет, ПРИМЕРНО ПОЛОВИНУ: передавать
можно только абонентов с согласием (требование закона). Формулировка владельца.
3. «Как подписать договор с юрлицом?» — у нас публичная ОФЕРТА, подписывать нечего.
Слово «договор» убрано из стоп-тем; новая статья: оферта (/legal/offer), политика,
возврат + закрывающие документы (счёт → оплата → акт, УСН без НДС).
Деньги/жалобы/претензии — по-прежнему живой специалист.
Все три проверены ЖИВЬЁМ через настоящий вебхук чата (а не в обход очереди).
Инструкция: 41 статья. Pest бот 58/58.
🪤 Урок: после правки кода бота ОБЯЗАТЕЛЬНО перезапускать queue:work — воркер
кэширует бут. Владелец видел старые ответы, пока я радовался зелёным тестам.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Владелец прогнал бота вживую: «там тупежа жуть какая-то». Разобрал журнал —
каждая тупость закрыта тестом.
1. 🔴 «откуда приходят номера телефонов?» — бот плёл про Конкурентное поле вместо
честного ответа (операторы + согласие абонента). Статья усилена.
2. «как получить номер человека, зашедшего на сайт» — теперь прямой ответ: НЕЛЬЗЯ,
мы не ставим счётчик на чужой сайт и не снимаем посетителей.
3. «законно ли брать телефоны с сайтов конкурентов» — да: это контакты фирм
из открытых источников, не персональные данные граждан.
4. Формат сайта и номера в проекте — бот сдавался, хотя это в статье. Дописано явно.
5. Клиентское слово «выгрузка» (= поставка заявок) — новая статья «Когда пойдут
заявки и когда прекратятся».
6. 🔴 «могут ли работать 2-3 человека?» — ЛОЖНАЯ эскалация: голое слово «человек»
стояло в стоп-темах. Стоп-паттерн переписан: ловим просьбу позвать ЖИВОГО
(позовите/соедините/живой человек), а не любое упоминание. Плюс статья про команду.
7. «а что ты вообще знаешь?» — бот нёс чушь про паузу проектов. Новая статья-оглавление.
8. «придут ли СМС?» — теперь прямо: СМС/Телеграм/пуш нет, только почта и колокольчик.
9. 🔴 ИИ начинал ответ с «в инструкции этого нет», а СЛЕДОМ отвечал верно — и мы
глушили весь ответ эскалацией. Теперь отговорку выкидываем; в промпт добавлено:
«нет» и «нельзя» — это ТОЖЕ ответ, а не отсутствие ответа.
Проверено вручную стоп-темы: «позовите живого человека», «какие операторы», «поставщик»
— по-прежнему эскалация. Pest бот 54/54 (12 новых тестов), Vitest каталога 8/8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Вопрос владельца: «а цены в нём надо актуализировать руками или он сам тянет?»
Ответ был неприятный: руками. Цифры (300/50 руб, тарифные ступени) стояли текстом
в статьях — поменяли бы цену в админке, бот продолжил бы врать про деньги.
Теперь в статьях стоят метки {{цена_подбора}}, {{цена_изучения}}, {{тарифы}},
а сервис LivePrices подставляет живые значения из system_settings и pricing_tiers
В МОМЕНТ ОТВЕТА — без переиндексации базы знаний. Нет цены в настройках — метка
не протекает клиенту, бот отсылает к экрану, где цена написана всегда.
Из текстов экскурсий цифры цен тоже убраны (фронт не знает БД): шаг подсвечивает
строку с ценой в самом окне — она всегда актуальная.
Проверено живьём: поменял цену в настройках на 450 — бот сразу назвал 450.
Pest 42/42 (3 новых теста), Vitest каталога 8/8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(bot): всеобъемлющая инструкция (36 статей) + 31 узкая экскурсия, которая открывает окна
Живой урок владельца 12.07.2026: «дубли чистятся в 2-х местах, а бот выдумал третье»,
«стыдно такого бота показывать», «видео показываем полное, а я спрашиваю про одно —
надо резать», «надо не только кнопку показывать, а и что за ней».
База знаний: 17 обзорных статей → 36 узких по темам. Портал обойдён целиком
(5 агентов): все экраны, окна, подсказки «(?)», тексты ошибок, внутренние правила.
Новое, чего бот не знал: дубли в ДВУХ местах (Поле и Предложения) + окно «источник
уже есть»; правило 18:00; почему приходит меньше лимита; оплата по счёту и акты;
списания; безопасность и 2FA; отчёты; массовые действия; регистрация.
Экскурсии: 15 → 31, каждая на свою тему (3–7 шагов). Раннер GuidedTour научен
шагу `open` — сам открывает окно/вкладку и подсвечивает то, что ВНУТРИ
(окно дублей, форму подбора, диалог пополнения, панель тарифов, карточку сделки).
99 новых якорей data-tour расставлено по всему порталу (4 агента).
Поиск: веса в tsvector (заголовок A / синонимы B / текст C) — иначе на «чистка
дублей» первой всплывала статья «Списания» из-за мимоходного упоминания «поля».
Тесты: Pest бот 39/39, Vitest 1222/1222. Новые сторожа: у каждой статьи есть
экскурсия; экскурсия не короче 3 и не длиннее 7 шагов; каждая цель экскурсии
реально существует в разметке; экскурсии про действия обязаны открывать окно.
Живая проверка в браузере: экскурсия про дубли сама открыла окно объединения
и перешла на второе место чистки.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@
Правило владельца 12.07.2026: оператора в чате может не быть, поэтому бот НЕ
переводит диалог (INVITE_AGENT убран из авто-флоу). Не смог ответить (личный
вопрос, стоп-тема, незнание) → просит номер телефона → как только клиент его
оставил, контакт уходит письмом в поддержку, а клиенту — «Спасибо, передал».
- ContactCapture: распознаёт номер в любом формате (+7/8/скобки/дефисы) → 79161234567
- BotContactRequestMail + шаблон: телефон, вопрос-на-котором-спасовал, id чата,
переписка целиком, время → services.support.email
- ProcessJivoMessageJob: ждём контакт, если прошлый ответ бота просил номер;
номера нет (клиент передумал/спросил другое) — отвечаем как обычно
- тексты эскалации переписаны под сбор контакта
TDD: BotContactCaptureTest (4 новых) + обновлены 4 теста прежней механики. 46/46.
Живая проверка на стенде: вопрос → просьба → номер → письмо с полной перепиской.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Владелец 12.07.2026 дал суть: номера передают операторы связи, и только по тем
абонентам, кто дал согласие на передачу данных третьим лицам и на рекламные
предложения. Раньше бот на «а где вы берёте эти номера?» честно эскалировал —
теперь отвечает сам, юридически корректно.
+ otkuda-zayavki.md: источник данных (операторы связи), согласие абонента как
основание, 152-ФЗ, никаких «слитых» баз; практика для клиента (звонить можно
и нужно быстро, вежливо завершать при отказе); честная оговорка «заявка —
интерес, не гарантия продажи»; юрвопросы → живой специалист.
Границы держатся (проверено живьём): «какой конкретно оператор — Билайн? МТС?» →
эскалация; «эти номера только мне достаются?» → эскалация (эксклюзивность —
жёсткая стоп-тема). Поставщик и модель распределения не раскрываются.
TDD: тест на РЕАЛЬНОЙ базе знаний (3 формулировки вопроса → статья про операторов).
42/42 зелёные.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Живой урок владельца 12.07.2026: на «зачем лидерра нужна и чем отличается
от конкурентов» бот понял «конкурентов» буквально и рассказал про раздел
«Конкурентное поле». Ценностного предложения в базе знаний просто не было.
+ o-servise.md: оплата за результат (нет абонплаты), 7 ступеней, прозрачные
списания, управление лимитами/регионами/паузой, никаких обязательств, старт
за минуты + 1000₽ новичку, заявки сразу в CRM, кому подходит, честная оговорка
«заявка — это интерес, а не гарантированная продажа».
TDD: тест ищет на РЕАЛЬНОЙ базе знаний (help:rebuild-knowledge внутри теста) —
топ-1 по вопросу не должен быть «Конкурентное поле». 41/41 зелёные.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Разведка портала 5 агентами (экраны, биллинг/настройки, Конкурентное поле,
все подсказки (?), внутренние правила) → база знаний бота выросла 4 → 15 статей:
начало работы (что такое заявка, подарок 1000₽), проекты (лимиты/регионы/дни,
правило 18:00 МСК), тарифы (7 ступеней, списание по факту), пополнение (100₽,
карта/счёт/акт), баланс и пауза (разморозка «всё-или-ничего»), сделки и канбан
(5 статусов, экспорт), Конкурентное поле (сбор 300₽/изучение 50₽, очередь,
ручной режим, дубли, метки номеров), смена источника, настройки и 2FA,
уведомления, импорт CSV, API/webhook, мобильная версия, дашборд, поддержка.
Защита коммерческой тайны (проверено агентом-цензором — 15/15 статей чисто):
ни слова про поставщика/каналы/закупку и про то, что заявка может уйти не только
одному клиенту; вопросы об эксклюзивности и перепродаже — жёсткая стоп-тема
(бот не отвечает НИ В КАКУЮ сторону, зовёт живого специалиста), внутренние
движки не упоминаются, ложных обещаний (авто-возврат, «Команда») нет.
Стоп-темы разделены: HARD (деньги/скидки/юр/эксклюзив — всегда) и PERSONAL
(«мой баланс») с исключением HOWTO — «перенести мои заявки из амоцрм» больше
не уходит к человеку, бот рассказывает про импорт.
TDD: 40/40 зелёные. Живая проверка 20+ вопросов на стенде.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Живой урок владельца 12.07.2026 («а что такое объединить дубли?»):
- LLM признался «в инструкции этого нет» (слабое ИЛИ-совпадение по общим словам),
а мы лепили кнопку «Показать на портале» → теперь такой ответ = честная
эскалация без кнопок (регэксп-детектор признания)
- статья konkurentnoe-pole: «Найти и объединить дубли» 🧹 — пары-двойники,
подтверждение клиента, бесплатно, счётчик на кнопке
- TDD: тест «не знаю без кнопки», 38/38 зелёные
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Живой урок владельца 12.07.2026 («а сам руками я могу?» — бот талдычил про ИИ за 300₽):
- ProcessJivoMessageJob: последние 6 реплик чата → история для LLM (хвосты
«👉 Показать…» срезаются), только СВОЙ чат
- BotAnswerService/YandexGptClient: история между system и текущим вопросом;
пустой поиск досыпается прошлой репликой клиента
- статья konkurentnoe-pole: ручное добавление конкурента/точек/источников — бесплатно,
кнопка «+ Добавить вручную» (фича существует, бот о ней не знал)
- TDD: 3 новых теста, все 37 зелёные
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Два живых бага демо 12.07.2026 (TDD, 34/34 зелёные):
- KnowledgeSearch: websearch_to_tsquery требует ВСЕ слова вопроса; «зачем он нужен»
ронял поиск в пусто → добавлен мягкий запасной проход через ИЛИ (plainto | )
- BotAnswerService STOP_PATTERN: «какой у меня сейчас баланс» проскакивал стоп-тему
(слова между «у меня» и «баланс») → допускаем до 3 слов разрыва, сч[её]т
- Протокол: журнал 12.07 (аккаунт Jivo, канал, цвет, ключ zofyNxmnRP, стенд-проверка)
- Скриншоты живой проверки виджета в findings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Серверный 3-й слой защиты (SupplierProjectName::strip) в DealController,
V1/DealsController, DealExportController — паритет с фронтом stripChannelPrefix.
На входе префикс и так срезается (RouteSupplierLeadJob/CsvLeadsParser), это
belt-and-suspenders на случай будущих правок парсера. Тесты: API+CSV не палят B<N>_.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Задача 5: RunAutopodborStudyJob::handle() оборачивает существующее тело в
try/finally — после ЛЮБОГО завершения прогона (done/empty/failed, включая
проброшенное исключение) распорядитель AutopodborStudyScheduler::tick()
берёт следующего кандидата с освободившейся дорожки шага 2.
Задача 6: artisan autopodbor:study-tick — крон-страховка раз в минуту на
случай, если дорожка освободилась не через завершение джобы. Без
withoutOverlapping() — требует таблицу cache_locks, которой нет в схеме
(см. комментарий в routes/console.php); overlap уже защищён внутренним
Cache::lock в AutopodborStudyScheduler::tick().
Боевой в batch-режиме: пауза проекта не доходила до поставщика — заказ висел активным
(галочка вкл, лимит>0), поставщик мог лить лиды и списывать за остановленный проект
(подтв. живой базой: omega, 57 заказов, 0 команд после паузы 08.07).
Поставщик фиксирует завтрашнюю заливку снимком в 21:00, поэтому гасить надо в 18:00-прогоне
ДО снимка (мгновенно бессмысленно — сегодня уже зафиксировано). Добавлен проход
deactivateOrphanedOrders в SyncSupplierProjectsJob: заказам источников, которых нет в
активных группах слепка за завтра, шлётся updateProject(status=paused) с ненулевым лимитом,
локально помечается inactive_since. online-режим не тронут (там пауза уже мгновенна).
Активность берётся из слепка (слепок-инвариант), не из live is_active.
Тесты (TDD RED->GREEN): batch pause + стык с delete-хвостом. 19/19 supplier+delete,
238/238 supplier+projects, pint чисто, PHPStan без новых замечаний.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Блок 6: если за окно (10 мин) в supplier_manual_sync_queue упала пачка
(>= threshold-manual-queue, дефолт 10) — вероятное падение кабинета поставщика.
Инцидент дедупится 60 мин, письмо шлётся каждый прогон на kdv1@bk.ru + ops@liderra.ru
(громко при реальной аварии, но одним сводным письмом). Флаг sendMail в createIncident.
phpstan-baseline: +4 TestCall::artisan() (5→9) от новых тестов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 9f0f5cfc59)
Убраны письма manual_required и failover_to_form (по проекту, без троттла) —
источник потопа ~116 писем на ops@ 07.07. Строка в ручной очереди остаётся
(запись видна в админке). Зависимость Mailer удалена из канала и DI-привязки.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 46607d702a)
START_BALANCE_RUB в RegistrationService: '300.00' → '1000.00' — новый тенант
получает 1000 ₽ на баланс при подтверждении почты. Обновлён докблок
RegistrationController и тест RegistrationTest (баланс 1000, 13 тестов зелёные).
Автоподборные 300 ₽ (цена «Собрать конкурентов») НЕ тронуты — это другая сумма.
Миграций нет (константа применяется при регистрации). В UI/лендинге сумма
подарка не отображается — фронт не менялся.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Второй пробел после фикса таймаута рендера: номера, написанные на сайте простым
текстом (не кликабельные tel:/schema, без коллтрекинга), терялись. Пример Центрофинанс:
8 800 200-00-10 («Телефон:» у юр. адреса), +7 931 106-54-50 (подвал) — движок брал
только tel:-ссылки.
- HtmlPhoneScanner: тело сканируем БЕЗ <script>/<style> (меньше шума из конфигов)
- CandidateBuilder: без трекера body-номера (не в code/visible) → kind 'text' «проверьте»
- SourceAggregator + DeepStudyCollector: text-only → phone_kind 'unverified' (не теряем,
но и за 100% настоящий не выдаём); code/справочник перебивают → 'real'
- фронт: бейдж «⚠ на сайте — проверьте» (amber), сортировка ниже настоящих, легенда
- 7 тестов (scanner/builder/aggregator/DeepStudy); вся ветка 493/493, build ок
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Кнопка на актуализации переименована «Удалить»→«Отказаться» (чтобы клиент не путал с удалением конкурента).
- CSV-выгрузка «Списаний» теперь включает автоподбор-строки (отдельным блоком, «Автоподбор конкурентов»);
тест на export. charges 11/11.
- schema.sql canon-sync v8.63: +колонка dismissed_actualize_keys у autopodbor_competitors + CHANGELOG
(заодно в цепочку шапки вписана пропущенная v8.62 CHECK autopodbor_charge).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кнопка «Удалить» на карточках «На актуализацию» рядом с Добавить/Заменить.
Отклоняет ТОЛЬКО находку (новый сайт/адрес): фирма поля остаётся, находка → архив,
её новые ключи запоминаются у фирмы поля (новая колонка dismissed_actualize_keys jsonb).
Классификатор вычитает отклонённые ключи → та же находка больше не всплывает; реально
другой новый сайт даст новый ключ → покажется. Не глушим по имени, конкурента не убираем.
Бэк: миграция + модель-каст + ProposalClassifier (вычитание) + эндпоинт dismissActualize
+ роут. Фронт: api + store + кнопка «Удалить». Тесты: классификатор 8/8, ветка 185/185,
фронт 30/30. Canon-sync schema.sql (v8.62) — follow-up (миграция idempotent-guarded).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- «Списания» (TenantChargesController.index) теперь union lead_charges + autopodbor_charge
(balance_transactions) — клиент видит списания за прогоны, не только в «Истории транзакций».
Автоподбор-строки: kind=autopodbor, метка вместо сделки, «—» для tier. Фильтр charge_source
автоподбор не подмешивает. 2 теста, всего charges 10/10.
- Плашки «Предложений» разъезжались за экран на длинных URL (utm/рекламные хвосты): grid-элементы
min-width:auto не сжимались. Фикс: .ld-grid>* min-width:0 + перенос длинных ссылок.
- Фронт 16/16.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при autopodbor_max_competitors=0 джоба делала array_slice($unique,0,0)=[]
→ сохраняла 0 конкурентов, НО всё равно списывала 50₽ (chargeForRun после обрезки,
проверка empty стои́т ДО неё). Теперь $max>0?slice:все — 0 значит «сохранить всех».
Плюс авто-повтор findCompetitors при пустом сборе (search_retry_on_empty, деф. 2):
моргание 2ГИС/Яндекс/EXA не показывает клиенту «ничего не нашлось». Реально пустая
ниша → empty без списания. 2 теста; ветка автоподбора 176/176.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Джобы Search/Study/Resolve делали AutopodborRun::findOrFail() ДО set_config
app.current_tenant_id. На проде роль crm_app_user под FORCE RLS (на боевом
кластере ни одна роль не BYPASSRLS) не видит строку без tenant-контекста →
ModelNotFound, все три кнопки автоподбора падали. На dev маскировалось
суперюзером postgres. Теперь tenantId передаётся в dispatch и контекст
выставляется первым. 3 регресс-теста на порядок (set до чтения runs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>