Хвост предыдущего коммита: форматтер причесал докблоки уже после того, как коммит забрал
содержимое файлов. Правка чисто косметическая.
NB: LEFTHOOK_EXCLUDE=larastan — те же 3 pre-existing ошибки в Sales-коде (b694c215).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Файл-подсказку _ide_helper_models.php генерировали ключом -N: он ОБЪЯВЛЯЕТ классы моделей
заново, и Larastan видел не настоящую модель, а этот огрызок. Отсюда 239 фантомных ошибок
в 104 файлах («у Tenant нет requiredLeadsForTomorrow()», «у Project нет aggregateSyncStatus()»
— при том что методы есть). Хук pre-commit падал на любом коммите: ни закоммитить, ни
разобрать, где настоящая ошибка.
Правильный режим — --write-mixin: стаб не подменяет классы, а подключается к ним через
@mixin в самой модели. Эта строка и добавлена в 22 модели; сам стаб gitignored, каждый
генерирует у себя.
Итог: 239 ошибок → 3. Оставшиеся три — настоящие (SalesAttachmentService:214,
SalesPayoutService:129/134 — лишние проверки на null там, где null невозможен), они из
b694c215 и раньше были не видны за фантомами. Отдельным решением.
NB: LEFTHOOK_EXCLUDE=larastan на этот коммит — иначе хук падает на тех самых 3 ошибках,
которые он же и помог увидеть. Bypass согласован с владельцем.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Перенос 7fa811b4 из gitea/main в ветку стройки, чтобы выкат отсюда не вернул баг на
боевой (класс ошибки 08.07 — «затёрло выкатом из устаревшей ветки»).
Суть: в batch-режиме портал слал поставщику «каркас» с limit=0 и без регионов. Кабинет
отбивает такой запрос ВСЕГДА — снято живьём с боевого 14.07:
{"status":"Error","message":"Введите limit!"}. Портал считал отказ поломкой, дёргал
запасной путь через браузер, тот тоже падал → проект уезжал в ручную очередь. Итог на
бою: 114 мусорных записей и 2 ложных high-инцидента «кабинет поставщика упал» (кабинет
при этом жив — отдаёт 140 проектов).
Лиды не терялись: настоящие строки создаёт вечерний SyncSupplierProjectsJob (18:00) с
посчитанными лимитами и регионами. Теперь handleBatch к поставщику при создании не ходит
(слать нечего), идемпотентная привязка существующих строк сохранена.
NB: то же самое для ОНЛАЙН-пути в этой ветке уже сделано («кабинет отклоняет limit=0» —
площадки с нулевой долей не создаются). Правки не пересекаются: там handleOnline, тут
handleBatch.
Тесты: 254/254 (Supplier + Plan5) в этой ветке; в main полный прогон 2453/2458.
NB: LEFTHOOK_EXCLUDE=larastan — статанализ в рабочей копии сломан устаревшим
_ide_helper_models.php (фантомы «Project::aggregateSyncStatus() не существует», хотя метод
есть в Project.php:181). По изменённым файлам статанализ прогнан в чистой песочнице: 0
ошибок. Bypass согласован с владельцем. Follow-up: перегенерировать стаб (--write-mixin).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Перенос fc78ee1e из gitea/main в ветку стройки, чтобы выкат отсюда не вернул
сломанные письма обратно на боевой (класс ошибки 08.07 — «затёрло выкатом из
устаревшей ветки»).
Суть: 5 писем (заморозка / напоминание / финальное / разморозка / «проект
остановлен — нет денег») уезжали в очередь с моделью Tenant; воркер грузил её
заново под crm_app_user, где RLS без app.current_tenant_id отдаёт 0 строк →
ModelNotFoundException, письмо не уходило никогда. Теперь письмо несёт снимок
данных и в БД при отправке не ходит.
Плюс дедуп persistent-инцидентов сторожа — по факту незакрытого инцидента,
а не по окну 60 мин (иначе копия инцидента каждый час, бесконечно).
NB: LEFTHOOK_EXCLUDE=larastan — статанализ в рабочей копии сломан устаревшим
_ide_helper_models.php (239 ошибок в 104 ЧУЖИХ файлах, напр. фантом
«Tenant::requiredLeadsForTomorrow() не существует», хотя метод есть в
Tenant.php:93). По изменённым файлам статанализ прогнан отдельно: 0 ошибок.
Bypass согласован с владельцем. Follow-up: перегенерировать стаб (--write-mixin).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Цены на кадрах теперь как на боевом: 55 ₽ за заявку, сетка 55→25 ₽, сбор
конкурентов 50 ₽, сбор источников 10 ₽ (были тестовые 500→250 ₽ и 300/50 ₽).
Две подписи прямо врали ценой — исправлены.
Кадры перестали ловить экран на полпути: робот дожидается загрузки данных.
Подписи, забегавшие вперёд экрана, переписаны; добавлен настоящий кадр раздела
«Отчёты»; стрелка в «Пометках» больше не тычет в пустой блок; демо-проект —
стоматология, а не окна.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карта узлов в JS строилась как MAP[n.s]={l,c} — флаг q (можно искать) НЕ
копировался. Из-за этого current() не находил искомый узел, в rubric ложилась
пустота: зелёные города не подсвечивались, а кнопка была заблокирована, хотя
ВЫГЛЯДЕЛА рабочей (у .btn не было стиля :disabled).
- MAP теперь несёт q; добавлен стиль .btn:disabled (видно, что заблокирована).
- Тесты-регрессии: карта обязана нести q; заблокированная кнопка обязана
выглядеть заблокированной.
- Проверено в живом браузере (Playwright): Медицина → Частные стоматологии →
rubric=chastnye-stomatologii, кнопка активна, 85 городов зелёные, Москва → #181.
228 тестов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
По просьбе владельца: «детские или взрослые стоматологии нам не интересны» —
надо уметь собрать «Частные стоматологии» целиком, не уточняя.
- rubrics.build_catalog: ГРУППА теперь тоже ищется — по своему имени; лист —
своей фразой 2ГИС. Не ищутся только раздел (слишком широко) и служебный
ящик «Ещё рубрики» (BUCKET_LABEL). Добавлен is_searchable().
- web/ui.py: в дерево для JS добавлен флаг q (можно искать). Каскад берёт САМЫЙ
ГЛУБОКИЙ выбранный искомый узел; уточнять глубже необязательно (плейсхолдер
«— не уточнять —»); кнопка активна сразу, как выбрана ниша или группа.
- Живьём: группа «Частные стоматологии» → 12 клиник, «Медтовары» → 12 фирм,
глубокий лист «Пластическая хирургия» → 12 клиник. 226 тестов.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Каталог перестроен из плоского (19×~100) в ДЕРЕВО реального рубрикатора 2ГИС:
19 разделов → 1522 листа, глубина 3 (раздел → ниша/группа → узкая рубрика).
Тот самый «3-й эшелон» за кнопкой «Ещё» 2ГИС (закрыт открытый вопрос §6.7g).
- tools/crawl_rubricator.py — краулер рубрикатора через xfetch (кэш, отсечка
глобального меню 19 разделов, «Ещё» = отдельная группа). Данные заморожены
в salesfinder/rubricator.json (кэш rub_cache/ и *_raw.json в git НЕ идут).
- rubrics.py переписан на дерево: build_catalog+slug, queries_for(leaf)=[query]
(группа не ищется — поиск только по конечному листу), _LEGACY_LABELS для
русских подписей старых списков.
- web/ui.py: адаптивный каскад — списки всплывают по мере надобности; лист
уходит как rubric, кнопка заблокирована пока не выбран лист.
- Живьём: глубокий лист «Пластическая хирургия» → 12 клиник из 2ГИС. 223 теста.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Пути к кадрам и списку тем были относительными, а страница открывается по
адресу /kak-eto-rabotaet (без слэша на конце) — браузер искал их этажом
выше и не находил ни одного. Пути от корня сайта: так же будет работать и
на боевом, где страница и папка guides/ лежат рядом с лендингом.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- вверху выбираешь РАЗДЕЛ 2ГИС, ниже каскадом появляется НИША этого раздела (уходит как rubric)
- по выбранной нише города, где уже есть готовый список, подсвечиваются зелёным и кликабельны →
клик открывает готовый список (/view/{id})
- выбор раздела/ниши запоминается (localStorage) — возврат назад не сбрасывает
- SECTIONS + DONE как JS-данные из GROUPS и recent(done); города несут data-city
- 219 тестов зелёных; серверная часть проверена на тест-стенде
- боевой портал НЕ перезапускался (идёт сбор) — правка применится при следующем перезапуске
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Диапазон 999-000 у операторов не существует — заведён специально для
демо-данных и съёмки инструкций, чтобы в кадр не попал настоящий номер.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Гость получает верную ссылку на разбор по всем 10 частым вопросам; где
разбора нет — бот молчит, в пустоту не шлёт. 20 тем, 126 кадров, единый
демо-клиент. Отдельно: найден баг боевой функции — поиск дублей считал
ноль признаком совпадения (на проде он есть, фикс в ветке).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ★ рядом с городом в списке выбора, если keyso_base(slug) не None (19 регионов, 30 городов)
- сноска под формой поясняет значок
- звезда не трогает сбор, только отображение выбора городов
- 216 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Точное разделение робот/браузер/человек (отклик страницы, а не запрос),
сквозная склейка лендинг→портал через куку на .liderra.ru, воронка и каналы
в админке. Метрику и Вебвизор не трогаем.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playwright-съёмщик (record.mjs) логинится демо-клиентом, проходит сценарий
из topics/<slug>.mjs и рисует поверх каждого шага стрелку+рамку+подпись
(подписи — из app/resources/help/sozdat-proekt.md, без выдумок). Первая
тема — sozdat-proekt, 8 кадров.
Мелкие находки по пути: приветственный тур перехватывал клики (глушим через
localStorage-флаг), первый проект нового клиента открывает шаг «Реквизиты»
(проходим тихо, без кадра), собранный фронт портала был устаревшим и тянул
старый виджет JivoSite поверх экрана (пересобрал npm run build — исходники
уже используют свой чат-виджет), подпись снизу могла перекрыть стрелку на
кнопках у нижнего края диалога (авто-перенос наверх).
- ниша и город показываются по-русски (label_for/city_ru) в таблице списков, шапке результата и экране сбора
- федеральный округ + русское имя города вместо слага
- кнопка «Собрать контакты по отмеченным» перенесена НАД таблицу (форма по-прежнему оборачивает галочки)
- 215 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- POST /delete/{rid} + RunStore.delete; кнопка ✕ в таблице «Последние списки» с подтверждением
- _run_one пропускает удалённый из очереди прогон (проверка существования под замком)
- убрана галочка Москвы по умолчанию — города не отмечены заранее
- 212 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- cities.federal_district(slug) → ЦФО/СЗФО/ЮФО/СКФО/ПФО/УФО/СФО/ДФО (Москва,Подмосковье→ЦФО; Питер,Ленобласть→СЗФО)
- в таблице последних списков перед городом бейдж округа
- 208 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Клипы по сути (сразу с нужного экрана, без входа и хождения по меню)
и тот же показ 2-3 картинками. Четыре темы: заявки, пополнение, создание
проекта, Конкурентное поле. Телефоны в кадре размыты. Страница сравнения —
tours-compare.html. На боевом сайте этого нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- STORE.recent(limit=200) на главной вместо дефолтных 10
- очередь из нескольких городов больше не «съедает» ранее собранные готовые списки
- тест: 15 списков — виден и самый старый, и самый свежий
- 205 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- города выбираются чекбоксами (несколько сразу), сгруппированы по регионам
- пункт «★ ВЕСЬ РЕГИОН» и режим @регион убраны
- несколько городов собираются ПОСЛЕДОВАТЕЛЬНО под глобальным замком _RUN_LOCK
(одновременно один город на весь портал — не ловим лимиты платных сервисов)
- каждый город — отдельный список; один город ведёт на его экран, несколько — на главную к очереди
- _do_run/_cities_of заменены на _run_one (под замком) + _do_batch (очередь)
- тесты очереди: порядок сохраняется, каждый город свой список; тест-изоляция STORE
- 204 теста зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Главное: сбор контактов переведён с метода inn (для ЮРлица) на inn_fl (поиск по ИНН
ФИЗЛИЦА, поле inn_fl=<ИНН>). Раньше по личному ИНН человека сервис отвечал not_found —
звали не тот метод. Теперь один запрос отдаёт полное досье, как «Поиск по ИНН» в кабинете.
Живьём: Моисеев not_found→ok 2тел, Орто-Премьер ip→ok 9тел, Медика not_found→ok 7тел.
- collect_contacts_for: один вызов inn_fl вместо двухшагового inn→name_standart; дешевле
- _api_status: ok / not_found / error по ответу сервиса
- убраны обломки двухшагового режима: _looks_like_ip, _name_standart_body, контакт-статус «ip»
- контакт ищем ТОЛЬКО по личному ИНН директора; по ИНН фирмы не ходим
- метка «ИП» перед ФИО в списке — dadata.check_ip по личному ИНН, поле director_is_ip
- пустая клетка контакта показывает причину: «сервис не знает этого человека» / «ошибка»
- 203 теста зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
20 личных вопросов на тестовом клиенте — 20 правдивых ответов; границы
держит (паузу не ставит, ПДн не даёт, будущее не прогнозирует). Гостю —
вход в кабинет. Посторонний получает 404. Две находки прогона закрыты:
жёсткий отказ в замене ловится в любой форме, правда про ниши больше
не режется. Тесты 216/216. На проде этого нет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- сбор контактов идёт ТОЛЬКО по личному ИНН директора; по ИНН фирмы больше не ищем
- директор-ИП распознаётся по ЕГРИП: даты рождения у ИП нет → второй платный запрос не делаем, помечаем status=ip
- collect_contacts_for теперь возвращает status: ok / ip / not_found / error
- в списке галочка активна лишь при личном ИНН директора; пустая клетка контакта показывает причину
- живьём: Евромед→ok 3тел·1почта, Орто-Премьер→ip, Медика→not_found; 196 тестов зелёных
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9 задач по TDD: ClientFacts (деньги/ступень/проекты/заявки под RLS),
сторож на выдуманные числа, карточка в контексте вошедшего, гостю вход
в кабинет, владелец разговора (404 чужому), ключ разговора на каждый вход,
живой прогон с судьями.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Карточка фактов (баланс/ступень/проекты/заявки) считается кодом и всегда
кладётся боту, когда клиент вошёл. Сторож режет числа не из карточки.
Переписка вошедшего закрывается от посторонних. Данные — строго под RLS
тенанта. Бот только рассказывает, ничего не меняет.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Прогон 46 диалогами (250 реплик) через новый чат: 250/250 ответов,
медиана 2,58 с, вранья 0 — переезд чистый. Одна находка (бот выдумал срок
«перезвонят в течение рабочего дня») починена правилом по смыслу.
Отдельно: счётчики от накрутки счёта сидели на файловом кэше, который считает
не атомарно (300 прибавок -> досчиталось 77). Переведены на Redis, 300/300.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Тот же скрипт, что в кабинете: /chat-widget.js отдаётся приложением
(на liderra.ru лендингу принадлежит только «/», остальные пути — приложению).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 задач по TDD: журнал разговоров -> свой приём сообщений -> опрос ответов ->
счётчики от накрутки счёта -> письмо владельцу со всей перепиской -> окошко
на портале и лендинге -> вычистка Jivo из настроек -> прогон 46 диалогами.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Владелец 13.07.2026: уходим от Jivo (3142 руб/мес корпоративный тариф ради
Bot API, ответа их поддержки так и нет), пишем своё окошко чата.
Ключевое: в чужом виджете бот не знает, кто с ним говорит -> личные ответы
невозможны. Привязка к Jivo в коде - 94 строки; мозг бота, 49 статей и
130 тестов переезжают нетронутыми.
Согласовано по частям: чат без постоянного соединения (очередь + опрос),
кабинет + лендинг, эскалация письмом владельцу (живого оператора нет),
три уровня счётчиков от накрутки счёта (потолок 1500 ответов/сутки + тревога).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Где что лежит, как поднять стенд, что держится кодом (не ослаблять),
что осталось открытым, порядок выката.
Главный вывод доводки: сторож на регулярках проигрывает гонку формулировок —
работает только правило по смыслу + запрет в промпте.
Код: ветка worktree-jivo-bot-core (004a0b17), тесты 126/126. Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Проверка advertises лезет на сайт фирмы; загрузка флакует (таймаут, TLS
на Windows), не открылся -> advertises=не проверено -> фирма вылетала из
списка (drop_not_advertising). Из-за этого улов по городам плясал (Хабаровск
13->8 между прогонами). Новый salesfinder/sitefetch.fetch_site_html: повтор
requests (https/http) attempts раз, затем xfetch-рендер. web/app._fetch_site
переведён на него. Тесты +6, всего 191 зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Что вскрылось (одиночные вопросы этого не видели): клиент оставил телефон →
бот молчал; бот забывал, что сам сказал; враньё «на паузе списаний нет»;
выдуманный телефон поддержки и юр-гарантии в новых оборотах.
Урок метода: машинный счётчик по запрещённым фразам показал 0 нарушений,
хотя враньё осталось другими словами — считать своими же регулярками нельзя.
Код и починка — в ветке worktree-jivo-bot-core (b037a216). Бот НЕ на проде.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Пример формата в ContactCapture.php остался в истории коммита 6f72367b
и блокировал push. Не ПДн — образец для распознавания; в рабочем коде уже
замаскирован. Настоящие номера сторож ловит по-прежнему.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Все 250 пар «вопрос → ответ» с финальной конфигурацией бота: модель Claude
Haiku 4.5 через AITunnel, маска ПДн, чистка markdown, сторож ответа.
Итог: 239 ответил сам, 11 увёл к специалисту, 0 «не знаю», 0 запрещённых фраз.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>