Запрос активности портала просил у tenants колонку name, которой не существует:
в схеме она называется organization_name. База отвечала 42703 Undefined column,
фронт показывал красную плашку «Не удалось загрузить данные» поверх страницы
/admin/visitors. Остальные три запроса страницы работали, поэтому карточки
рисовались пустыми, а не сломанными.
Наружу поле по-прежнему отдаётся как name — контракт API и фронт не менялись.
Добавлен тест на portal-эндпоинт. Из четырёх запросов страницы тестами были
покрыты три; единственный непокрытый и оказался сломанным — тот же класс потери,
что CsvReconcileJobTest: код едет, тест нет.
Проверки: AdminVisitors 5/5, смежные Tracking 14/14, Pint чисто, Larastan 0 ошибок.
Прод не тронут — выката не было.
Co-Authored-By: Claude Opus 4.8 1M context <noreply@anthropic.com>
Прод-инцидент 14.07.2026. Пять писем (заморозка, напоминание, финальное,
разморозка, «проект остановлен — нет денег») уезжали в очередь с Eloquent-моделью
Tenant. SerializesModels заменяет модель на id, а воркер грузит её заново — под
ролью crm_app_user, где RLS-policy tenants_self_isolation без app.current_tenant_id
отдаёт 0 строк → ModelNotFoundException. Клиент №7 заморожен с 12.07 и не получил
ни одного письма; на проде это ломало письма о заморозке для ВСЕХ клиентов.
Письма больше не ходят в БД при отправке: несут снимок данных (без SerializesModels).
Заодно: сторож incidents:watch-failures плодил копию persistent-инцидента каждый час
(строка в failed_jobs живёт вечно, а дедуп был окном в 60 мин) — 2 залипшие ошибки
дали 31 запись за сутки и красную лампу «Очереди/джобы». Дедуп persistent теперь по
факту незакрытого инцидента, а не по возрасту последней копии.
Регрессия: BalanceMailsQueueRestoreTest (6 кейсов) + 2 теста сторожа.
Прогон: 165/165 billing+incidents, phpstan 0, pint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В смс дорог каждый символ: вместо длинного хвоста с метками сервер сам
подставляет канал и дату рассылки (дд-мм), чтобы разные рассылки не слипались.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Полный прогон вскрыл реальный баг: миграция создавала партицию site_events_2026_07
своими границами (в местном времени), а MonthlyPartitionManager — site_events_y2026_m07
в UTC. Партиции перекрывались (42P17) и роняли 19 ЧУЖИХ тестов. Теперь партиции
нарезает только менеджер (ensureRange), как у всех остальных таблиц.
Плюс сторож схемы обновлён под +2 таблицы учёта (80 таблиц, 141 индекс) и
записаны готовые ссылки с метками для смс/hh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гостей чистит visitors:prune (воскресенье 03:15 МСК), партиции событий —
существующий partitions:drop-expired по новой настройке retention=6 месяцев.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Воронка — по уникальным живым гостям (is_datacenter=false); гости с хостингов
и VPN считаются отдельным числом, в конверсию не попадают.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Разбор докладной 13.07.2026: AutopodborController::restoreMergeEvent воскрешал
только карточку поглощённого конкурента (имя/сайт/справочники/телефоны/is_federal),
но не его autopodbor_sources — и если у источника был created_project_id (по нему
идут заявки/деньги), проект оставался приклеен к выжившей карточке под её именем.
Добавлен снимок источников поглощённых конкурентов: nullable-колонка
autopodbor_merge_events.absorbed_sources (миграция 2026_07_14_090000, идемпотентна),
заполняется AutopodborCompetitorMerger::snapshotAbsorbedSources ДО переноса/
переименования при слиянии (включая имя проекта на момент склейки). restoreMergeEvent
по этому снимку либо перевешивает исходную строку источника обратно на воскрешённую
карточку, либо пересоздаёт её (если строка была удалена при разрешении коллизии
dedup_key), и откатывает имя проекта.
Старые записи журнала (до миграции) снимка не имеют — восстанавливаются как раньше
(только карточка), ответ API помечается 'partial' => true с честным сообщением.
db/schema.sql v8.65 + db/CHANGELOG_schema.md — запись синхронизирована.
tests/Feature/Autopodbor/ + tests/Feature/Bot/ — 417/417 после migrate:fresh.
Живая проверка на локальном стенде через Playwright (создание проекта → слияние →
«Вернуть» → проект и источник вернулись на воскрешённую карточку) — подтверждено.
Ветка worktree-jivo-bot-core, НЕ на боевом проде — выкат отдельным решением владельца.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Три точки: обычный вход, вход через 2FA (оба пути — код и резервный),
подтверждение почты (именно там клиент реально создаётся, а не на /register).
Учёт в try/catch — ошибка учёта не может уронить вход. Регресс авторизации 114/114.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4 плана 2026-07-13-visitors-analytics.md: VisitTracker (создание/обновление
гостя, запись событий, привязка к клиенту) + публичный TrackController@store,
маршрут POST /api/track (throttle:track 60/мин), лимитер в AppServiceProvider,
CSRF-исключение api/track в bootstrap/app.php.
Отклонения от плана (тест-харнесс, не прод-код):
- getCookie('lid_vid', false) в тесте убрал decrypt=false — с withCookie() это
давало двойное шифрование (withCookie сам шифрует plain-значение).
- добавлен withCredentials() перед withCookie()+postJson/getJson — Laravel
тест-клиент по умолчанию не шлёт cookie на JSON-запросы (как XHR
credentials:'omit'); реальный маячок шлёт fetch с credentials:'include',
так что прод не затронут.
Pest: 5/5 (TrackEndpointTest). Pint: app/Services/Tracking + TrackController.
Ревьюер прогнал AnswerGuard руками: пропускал числа словами («два проекта»),
разрыв «число...единица» («20 в день»), чужие единицы (сделок/клиент/сутки/...)
и самое опасное — число, которое в карточке ЕСТЬ, но относится к ДРУГОМУ факту
(модель переставила местами). Карточку (ClientFacts) генерирует код с
предсказуемым форматом, поэтому сторож теперь разбирает её на пары
«факт → правильное число» (баланс, цена ступени, получено/до след. ступени,
кол-во проектов, заявки сегодня/вчера/7дней/месяц, списано) и режет
несовпадение по смыслу, а не по буквальной формулировке.
TDD: 9 новых тестов (196→195 итого с учётом снятого дубля), 195/195 зелёных.
Стресс-прогон 20 предложений (10 враньё + 10 честных) вручную — 0 расхождений
после фикса дизамбигуации «этот/этом месяц» vs голое «за месяц».
1. Экскурсии «Показать на портале» включены обратно. Прежнее правило давало ссылку,
если совпало ХОТЯ БЫ ОДНО слово, — бот вёл на уведомления в ответ на «первое
сообщение подряд». Теперь два условия: реплика должна быть ВОПРОСОМ и слово из
него должно попасть в заголовок статьи или её синонимы. Плюс экскурсия предлагается
ТОЛЬКО клиенту из кабинета: гостю с лендинга ссылка внутрь кабинета бесполезна.
Ссылки в окошке стали кликабельными (без innerHTML — текст ответа от модели).
2. На «hello» бот отвечал «в моей инструкции нет ответа» и просил телефон: заготовка
знала только русские приветствия. Приветствие — не вопрос, эскалировать его глупо.
3. «Хочу, чтобы менеджер не видел биллинг» бот принимал за просьбу позвать НАШЕГО
менеджера и уводил к специалисту, хотя ответ есть в статье, — и через реплику сам
же отвечал, отчего выглядел противоречащим себе. Правило про доступ дополнено.
Статья «Команда и доступ» дописана: сколько человек можно пустить, почему нельзя
скрыть раздел, баланс общий на всех.
Тесты 161/161.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Правило владельца 13.07.2026. Лидерра отдаёт ВСЕ номера, прошедшие через
выбранный клиентом источник (и входящие, и исходящие), и не сортирует людей
по темам. Источник конкурента может быть шире ниши клиента (клиника с 30
врачами против одной стоматологии); конкурент может расширить профиль,
ошибиться в рекламе или прозванивать купленную базу — это вне нашего контроля.
Заявка не по теме = не брак, замены за неё нет; но тон мягкий — объяснить
и развернуть клиента к работе с ИСТОЧНИКОМ (сузить, сменить, пауза).
Статья kachestvo-istochnika.md + мягкие напоминания в трёх местах, где речь
заходит об источниках. Сторож AnswerGuard::promisesNicheRelevance режет
обещания «только целевые / отсеиваем нецелевых / гарантируем трафик».
50 статей, тесты 153/153.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прогон через свой чат 13.07.2026: бот сказал «специалист перезванивает
в течение рабочего дня». Поддержка круглосуточная, «рабочего дня» у нас нет —
это обещание, которое можно нарушить. Прежнее правило перечисляло глаголы
(«ответят», «разберут») и пропустило «перезванивает».
Правило теперь по смыслу: связь + срок = режем; «круглосуточно» — оставляем.
Тесты 151/151.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Разбор хвостов полного прогона: было 2407/2418, стало 2417/2418 — единственное
оставшееся падение ExampleTest воспроизводится только там, где не собран фронт
Vite manifest not found, к коду отношения не имеет.
1. partitions:create-months — новый флаг --behind, по умолчанию 1.
Нарезалось только «текущий месяц и вперёд», поэтому на свежей базе партиции за
прошлый месяц не было НИКОГДА. Между тем строки с датой в прошлом месяце — штатное
явление: тридцатидневное окно списаний метрика runway и вставки на стыке месяцев.
На боевом не всплывало — там партиции копятся с самого начала, проверено: нарезаны
до января 2027, крон жив. А на свежей базе и в CI INSERT падал «no partition found»
в первой половине КАЖДОГО месяца. Ловилось двумя тестами AdminTenantShowTest.runway
и роутингом лида.
2. Возвращены три аудит-теста, потерянных при сведении main 09.07.
ADR-021 сделал цепочки журналов общими и AuditChainConfig в main это уже отражает,
а тесты остались на старом per-tenant поведении ADR-018 и падали. Правильные версии
лежали на ветке fix/audit-chain-global-scope, но в main не доехали — тот же класс
потери, что и CsvReconcileJobTest.
Тесты: полный прогон 2417/2418 зелёный, Pint чисто.
Известное и НЕ тронутое, вынесено в отдельный список:
- Larastan на main даёт 30 замечаний уровня типов, ни одного живого бага:
мёртвые ветки в AuditRebuildChain, оставшиеся от ADR-021; ресурсы автоподбора без
аннотации модели; ProjectController читает виртуальные пометки gate_payload и
launch_deferred, которых нет в таблице.
- Тест лимита входов флакует только в длинном прогоне, в одиночку зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-инцидент 11-12.07.2026: робот итоговой проверки слал письма «нет заказа у
поставщика» на строки, которые в кабинете ЕСТЬ, включены и с верным лимитом.
Корень: кабинет дописывает метку канала к имени строки только при СОЗДАНИИ, а при
обновлении сохраняет имя ровно как прислали. Наш ежедневный updateProject слал голый
uniqueKey и каждый прогон стирал метку. Последствия:
- итоговая проверка выводила площадку из префикса имени и переставала узнавать
строку -> ложное missing 11.07 и 12.07;
- лид от такой строки приходил с project без метки -> webhook не мог определить
канал и писал platform=DIRECT вместо B1/B2/B3, то есть терялась атрибуция канала.
Что сделано:
- SupplierPortalClient::toPayload — на update имя уходит с меткой канала; на create
остаётся голым, там метку ставит сам кабинет и один save с тремя флагами рождает
три строки, общего префикса у них нет.
- VerifySupplierOrderJob::normalizeLive — площадка берётся из служебного поля src
rt/bl/mt, а не из префикса имени; сверка больше не зависит от имени вообще.
- Новая разовая команда supplier:repair-project-names — возвращает метку строкам,
у которых её уже стёрли. Payload собирается ИЗ ЖИВОЙ строки кабинета, меняется
ровно одно поле name; по умолчанию сухой прогон, запись только с --apply.
Ветка пересобрана на gitea/main — закрывает follow-up «фича итоговой проверки заказа
не сведена в main». Попутно возвращён CsvReconcileJobTest, отставший от кода после
сведения main 09.07: он не фейкал fetchDeliveredLeads и падал 9 из 11.
Боевой liderra.ru: выкачено, починена 81 строка, робот показывает 0 расхождений
138 наших строк вместо 57. Двум лидам восстановлен канал по журналу выдач поставщика.
Тесты: Pest supplier 277/277, Pint clean, Larastan 0 новых ошибок.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. 🔴 МОЙ КОСЯК: на «как получить номер человека, заходящего на сайт» бот отвечал
«нельзя» — то есть отговаривал клиента от того, ЧТО МЫ И ПРОДАЁМ. Владелец:
«как нельзя, а мы чем занимаемся?!». Теперь: «это ровно то, что делает Лидерра —
создайте проект, укажите сайт, контакты пойдут в Сделки».
2. «Все ли номера с источника цепляет система?» — нет, ПРИМЕРНО ПОЛОВИНУ: передавать
можно только абонентов с согласием (требование закона). Формулировка владельца.
3. «Как подписать договор с юрлицом?» — у нас публичная ОФЕРТА, подписывать нечего.
Слово «договор» убрано из стоп-тем; новая статья: оферта (/legal/offer), политика,
возврат + закрывающие документы (счёт → оплата → акт, УСН без НДС).
Деньги/жалобы/претензии — по-прежнему живой специалист.
Все три проверены ЖИВЬЁМ через настоящий вебхук чата (а не в обход очереди).
Инструкция: 41 статья. Pest бот 58/58.
🪤 Урок: после правки кода бота ОБЯЗАТЕЛЬНО перезапускать queue:work — воркер
кэширует бут. Владелец видел старые ответы, пока я радовался зелёным тестам.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>