Commit Graph

164 Commits

Author SHA1 Message Date
Дмитрий e969b7700a chore(chat): убрать настройки Jivo — чат теперь свой; экскурсии выключены
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>
2026-07-13 08:32:21 +03:00
Дмитрий cfd26e38ef feat(chat): свой приём сообщений вместо вебхука Jivo
Заменяет 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
2026-07-13 07:04:38 +03:00
Дмитрий c639b3b651 style(bot): pint — неиспользуемые импорты в трёх bot-тестах
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:48:51 +03:00
Дмитрий 264a6f8b5f feat(bot): JivoBotClient — BOT_MESSAGE/INVITE_AGENT, dev-режим без URL 2026-07-10 08:48:48 +03:00
Дмитрий 3c936a2ab0 feat(bot): YandexGptClient — completion Lite, таймаут 8с, null при беде 2026-07-10 08:48:47 +03:00
Дмитрий a5d75465df feat(bot): HelpArticleParser — frontmatter + чанки статей инструкции 2026-07-10 08:47:46 +03:00
Дмитрий 97c75407ab fix(autopodbor): RLS-политика межтенантного распорядителя + фронт 422-дискриминатор + price=0 гейт
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:45:12 +03:00
Дмитрий e2663f90b1 feat(autopodbor): AutopodborStudyScheduler — одна дорожка + round-robin 2026-07-09 09:45:09 +03:00
Дмитрий 9d3df153a5 feat(autopodbor): AutopodborBudgetGate — деньги пакета с резервом проектов
Переиспользует BalancePreflightService.evaluate() для проверки, что после
списания за пакет ещё хватает баланса на дневной лимит активных проектов.
Бинарный поиск максимально доступного количества единиц пакета (maxAffordable)
и расчёт недостающей суммы (topupRub) при отказе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 09:45:08 +03:00
Дмитрий a32f64cf5c fix(автоподбор): гейт «та же фирма» на разворот 2ГИС — режет иногородних чужаков
Баг AutoMoney: разворот по общему/партнёрскому/иногороднему номеру тащил чужие МФО
из Москвы/Питера (moneyman, finombudsman — фин.омбудсмен!, npmir, razvitie…) в источники
красноярского конкурента. crowdedCap ловил только массовые 8-800.

Гейт (100%-надёжный, проверен на реальных данных прода): карточку 2ГИС из разворота
выкидываем, ТОЛЬКО если город из URL карточки (2gis.ru/<city>/firm) ≠ город конкурента
И домен не наш бренд (с учётом поддоменов-филиалов). Свой город ИЛИ свой бренд — оставляем.
Яндекс НЕ трогаем (город в URL не виден, там же законные филиалы — риск срезать своих).

Проверка на проде: #16 AutoMoney/#64 Ваш Инвестор — все 2ГИС moscow/spb уходят;
#22 (автоломбард krasnoyarsk), #48 Кредит-финанс (Екб/Томск, Яндекс) — остаются.
11/11 тестов expander, вся ветка 220 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 19:50:38 +03:00
Дмитрий e9600fb3b1 feat(автоподбор): текстовые номера с сайта → «на сайте, проверьте» (unverified)
Второй пробел после фикса таймаута рендера: номера, написанные на сайте простым
текстом (не кликабельные 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>
2026-07-07 19:23:37 +03:00
Дмитрий 4dc897d61c fix(автоподбор): рендер сайта больше не теряет номера при таймауте Playwright
Шаг 2: CurlPlaywrightFetcher.site() curl'ом успешно тянул страницу с tel:-номерами,
но render() (node render-page.cjs) висел на waitUntil networkidle и падал по таймауту.
Symfony Process::run() при таймауте бросает ProcessTimedOutException — оно вылетало из
site(), SiteOpener обнулял весь сбор. Итог: 0 номеров с сайта (боевой кейс Центрофинанс,
tenant info@lkomega.ru, прогон 07.07 14:07, только сайт+карточка, ни одного code-номера).

- render()/renderFirm() ловят \Throwable -> null: падение/вис рендера НЕ фатально,
  curl-HTML с номерами 'в коде сайта' сохраняется
- render-page.cjs: networkidle -> domcontentloaded + пауза; рендер 30с+вис -> 3.7с (боевая проверка)
- сиды curlRunner/nodeRunner для офлайн-теста; регресс-тест воспроизводит потерю номеров

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:16:42 +03:00
Дмитрий a3fd8a6e9a feat(autopodbor): «Удалить» на актуализации — мягкое отклонение находки
Кнопка «Удалить» на карточках «На актуализацию» рядом с Добавить/Заменить.
Отклоняет ТОЛЬКО находку (новый сайт/адрес): фирма поля остаётся, находка → архив,
её новые ключи запоминаются у фирмы поля (новая колонка 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>
2026-07-07 15:50:59 +03:00
Дмитрий f420817eeb feat(autopodbor): EXA через прокси (EXA_PROXY) — обход блокировки российских IP
EXA за Cloudflare режет российские IP, федеральный канал возвращал пусто с боевого.
Добавлен services.exa.proxy (env EXA_PROXY) → Guzzle proxy option в findSite/findSites.
Пусто → напрямую (dev). Тест: с proxy цепочка не ломается, домен извлекается (8/8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:21:39 +03:00
Дмитрий 0f30edb2b6 Merge remote-tracking branch 'gitea/main' into worktree-avtopodbor
# Conflicts:
#	app/config/services.php
#	app/tests/Pest.php
2026-07-06 14:50:58 +03:00
Дмитрий 883fdb9f91 style(autopodbor): pint — причесать миграции/фетчер/тесты перед релизом
Стиль-only (braces_position, ordered_imports, fully_qualified_strict_types и т.п.),
поведение не меняется. Autopodbor 476/476 зелёный после причёсывания.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:35:22 +03:00
Дмитрий ab3b5e9a71 feat(autopodbor): кэш отрисованных карточек 2ГИС/Яндекс за тумблером
CachingPageFetcher — декоратор над загрузчиком карточек (SelfRenderClient/
XfetchClient): кэширует в общем Cache (Redis/file) по ключу URL на N дней ТОЛЬКО
карточки (/firm/ 2ГИС, /maps/org/ Яндекс). Поиск (/search/) и ленты — всегда мимо
кэша (свежее добывание новых фирм). Пустое не кэшируем — разовый сбой не отравит кэш.

Кэш ОБЩИЙ для шага 1 (поиск) и шага 2 (изучение+разворот) и повторных прогонов:
ключ по URL → любые экземпляры делят один кэш. Вшит в AutopodborServiceProvider
через wrapCache(); 2ГИС-база RoutingBatchFetcher и self-render card reader обёрнуты,
яндекс-Playwright/ленты — нет.

Флаг AUTOPODBOR_RENDER_CACHE (ВЫКЛ по умолчанию) + render_cache_ttl_days (5).
TDD: 8 тестов CachingPageFetcher + 4 теста проводки — зелёные. Живой замер ДСК:
тёплый прогон — 26 карточек из кэша, 0 обращений к 2ГИС/Яндексу, источников 11=11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 10:52:14 +03:00
Дмитрий 2b8c0469ee fix(autopodbor): якорь по данным клиента на шаге 2 + параллельный разворот
Качество. Привязанные клиентом карточки 2ГИС/Яндекс теперь используются как
«земля» и якорь холдинга (раньше выбрасывались). Слепой поиск по имени берётся
только при наличии якоря; AnchorGate: пустой якорь → drop (было keep-всё — корень
шума). Без сайта чужие фирмы по широкому имени («Притяжение» → стоматология,
салоны, эротический магазин) больше не попадают в карточку конкурента. Доказано на
живых данных (Сибагропромстрой): свежий полный сбор — 0 салонов, только номера
застройщика.

Скорость (за флагом AUTOPODBOR_PHONE_REVERSE_PARALLEL, ВЫКЛ по умолчанию).
ParallelPhoneReverse крутит 2ГИС- и Яндекс-разворот ОДНОВРЕМЕННО (итог по времени
= max, не сумма; оба источника отрабатывают полностью — ничего не режется).
Сверка последовательно vs параллельно: данные один в один, потерь нет. Таймаут
подпроцесса 600с (драйвер process иначе берёт дефолт Symfony 60с и убивал разворот).

Тесты: AnchorGate (пустой якорь → drop; телефон-якорь режет чужое), якорь
привязанной карточки режет салоны, ParallelPhoneReverse (слияние на sync-драйвере).
Весь Autopodbor 464/464, Pint чист.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:54:48 +03:00
Дмитрий 9d7b1e3ba1 feat(autopodbor): полное прокси-покрытие Яндекса через виртуалку (фаза 7b)
Закрыт хвост §5.3: node-скриптовые точки Яндекса уведены на рендер-сервис, чтобы
под self_render ВЕСЬ Яндекс шёл через виртуалку+прокси (не крутил браузер на портале).

- YandexListSource (интерфейс) + LocalYandexListSource (xf4, локальный Playwright) +
  RenderServiceYandexListSource (self-render, POST type=yandex-list на виртуалку).
- PlaywrightYandexDirectory (листинг шаг 1) и PlaywrightYandexPhoneSearch (разворот)
  расшиты на YandexListSource — заодно стали тестируемы офлайн (6 новых тестов).
- PlaywrightYandexCardReader под self_render получает SelfRenderClient (был PageFetcher).
- Рендер-сервис: режим type=yandex-list (скролл ленты + сбор организаций → {orgs}),
  проброс scrolls.
- Провайдер: yandexListSource() по флагу; разворот-Яндекс на виртуалку+прокси.

Итог: при self_render ВКЛ на xf4 и на локальном браузере портала не остаётся НИ ОДНОЙ
точки — 2ГИС и весь Яндекс через виртуалку+прокси. 459/459 тестов, Pint+Larastan чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:18:00 +03:00
Дмитрий f74c4d00f7 feat(autopodbor): SelfRenderClient — HTTP-клиент к рендер-сервису (фаза 5)
HTTP-клиент к своей виртуалке (замена xf4). Реализует BatchPageFetcher (и
PageFetcher): для 2ГИС просит initialState, для Яндекса/сайтов — HTML. Секрет
в заголовке X-Render-Key (из .env). Без ключа/адреса — молча пусто (деградация).
Http::pool для батча, ретраи. 7/7 тестов, Pint чисто.

- config services.self_render (endpoint/key/concurrency, всё из .env).
- config autopodbor: флаги self_render + гранулярные self_render_2gis/_yandex
  (наследуют master), дефолт ВЫКЛ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:56:10 +03:00
Дмитрий 025adfb1ee refactor(autopodbor): расшивка точек 2ГИС на шов TwoGisReader (фаза 4)
Расшиты 4 потребителя 2ГИС — вместо прямых вызовов парсеров зовут интерфейс
TwoGisReader (дефолт HtmlTwoGisReader = поведение 1:1). Готовит подмену на
self-render без слома xf4-пути. 443/443 теста автоподбора зелёные, Pint чисто.

- CategoryScraper: CategoryListingParser → TwoGisReader (шаг 1 листинг).
- CompetitorCardEnricher: 2ГИС-карточки через TwoGisReader, Яндекс на parseYandexCard.
- PhoneReverseExpander: полностью на TwoGisReader (2ГИС-only разворот).
- DeepStudyCollector (2 вскрытых стыка): конструктор XfetchClient→BatchPageFetcher
  (под SelfRenderClient); collectYandex — инлайновый фетчер вынесен в инъекцию
  ($yandexDir), под self_render пойдёт на рендер-сервис+прокси, а не локальный
  Playwright. 2ГИС-разбор через TwoGisReader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:53:10 +03:00
Дмитрий 31048478bf feat(autopodbor): шов TwoGisReader + HtmlTwoGisReader + TwoGisJsonReader (TDD)
Фазы 2-3 плана self-render. Только новые файлы — существующий код не тронут,
xf4-путь цел. 281/281 юнит-тестов зелёные, Pint чисто.

- TwoGisReader — интерфейс разбора 2ГИС (3 метода: listing/card/branchList),
  единый контракт поверх HTML (xf4) ИЛИ initialState-JSON (self-render).
- HtmlTwoGisReader — xf4-путь: тонкий делегат к CategoryListingParser/
  DirectoryParser, поведение 1:1 (4 теста контракт-идентичности на реальных
  фикстурах 2gis-*-kraslombard).
- TwoGisJsonReader — self-render путь: разбор window.initialState по карте
  путей спайка (pagination-список → entity.profile), фильтр type=branch
  (фирма-в-фирму, без зданий/улиц). 5 тестов на реальных фикстурах: 12 фирм
  листинга, телефон+сайт карточки, 2 пути поиска-по-телефону, деградация.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:40:39 +03:00
Дмитрий f80e12549b perf(autopodbor): мульти-ключ xfetch round-robin — параллельность ключи×concurrency
Сбор 2ГИС упирался в лимит xf4.ru ~3-4 запроса за раз на КЛЮЧ (выше 429).
XfetchClient теперь принимает список ключей и раскидывает запросы по кругу:
размер пачки = concurrency × число ключей, каждому URL — свой ключ. Живой залп
подтвердил: 9 параллельно (3 ключа × 3) без 429 → лимит per-key, ключи дают ×3.

Ключи — только в .env (XFETCH_API_KEYS через запятую), в код/гит не идут; пусто →
работает по-старому на одиночном XFETCH_API_KEY. Совместимость: конструктор
принимает и строку, и список. TDD: XfetchClientTest 11/11, автоподбор зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:28:19 +03:00
Дмитрий 3644402300 fix(autopodbor): дубли по телефону застройщика — снят порог «людного» номера
Кейс «Образцово»: у застройщика (СМ.Сити) один номер отдела продаж легитимно
висит на всех его ЖК (277-99-01 на 9 проектах). Прежний порог dropCrowdedKeys
(>4 карточек → «рекламный», выкинуть) прятал это, и окно «Найти дубли» не
показывало проекты застройщика — systematically (6 таких номеров в данных).

Решение владельца 05.07: порог по ТЕЛЕФОНУ (p:) убрать — клиент подтверждает
склейку вручную («Это разные»), тихой ложной склейки нет; рекламные-подменыши
и так отсекаются на сборе. Порог по АДРЕСУ (a:, бизнес-центр) оставлен.
Проверено на AutoMoney: его номер у 3 карточек — все реально Автодом24, группа
верная, монстр-группы снятие порога не даёт.

Тесты переписаны со старого поведения на новое (unit + feature), 421/421.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:47:40 +03:00
Дмитрий 13c8620dc5 feat(autopodbor): Яндекс-разворот по телефонам на шаге 2 (рядом с 2ГИС)
Для застройщиков, которые сидят только в Яндексе. По каждому номеру ищем в
Яндекс.Картах, но список рыхлый — поэтому КАЖДУЮ карточку открываем и оставляем
только те, где искомый номер реально есть. Это отсекает «Похожие места рядом»,
рекламу и рыхлые совпадения по бренду. Оба справочника объединяются
(CompositePhoneReverse), дубли по домену схлопываются.

Переиспользована готовая механика Яндекса: render-yandex-list.cjs (список) +
DirectoryParser::parseYandexCard (карточка). Браузерные адаптеры за интерфейсами
(YandexPhoneSearch/YandexCardReader) — логика TDD-ится на фейках; порог общей
линии считается по подтверждённым карточкам.

За отдельным тумблером AUTOPODBOR_PHONE_REVERSE_YANDEX (по умолчанию ВЫКЛ,
требует phone_reverse). Юнит 267/267, feature 150/150. План и находки —
docs/superpowers/{plans,findings}/2026-07-05-*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 09:30:27 +03:00
Дмитрий 44610812a8 feat(autopodbor): разворот по телефонам на шаге 2 (сайты проектов застройщика)
По личным телефонам фирмы обратным поиском в 2ГИС вскрываем связанные карточки
холдинга (проекты застройщика) — забираем их сайты и телефоны, цепочкой «до талого».
Общий номер (у >4 фирм) пропускаем; visited-set + потолок карточек; уже известную
главную карточку не дублируем. Защита от AutoMoney: реклама отсечена парсером,
общий номер обрублен до разворота, находки не клеятся молча.

За тумблером AUTOPODBOR_PHONE_REVERSE (по умолчанию ВЫКЛ). Юнит 259/259, feature
146/146. Решение и план — в docs/superpowers/{findings,plans}/2026-07-05-*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 07:55:03 +03:00
Дмитрий f807bcd455 feat(autopodbor): прогресс прогона — очередь и этапы (бэкенд)
Клиент во время сбора видит место в общей очереди (пока ждёт) и текущий
этап (пока идёт). Очередь — длина общей Redis-очереди (честное ожидание,
без RLS, одинаково dev/прод). Этапы пишет сам движок через RunProgressChannel:
поиск (анализ→справочники→федералы→сбор→отбор), изучение — по каждому элементу.

- миграция progress (jsonb) на autopodbor_runs + cast
- RunResource отдаёт queue_position (AutopodborQueue) и progress
- RunProgressChannel (синглтон), джобы привязывают его к своему прогону
- SearchStages + инструментовка LiveFindCompetitors и RealCompetitorAgent
- 16 тестов (Feature+Unit), все зелёные

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:31:01 +03:00
Дмитрий 1435e168c4 feat(autopodbor): формы создания/правки конкурента через элементы - правка не теряет карточки
Бэк: хелпер CompetitorElements нормализует элементы и выводит легаси site_url/directory_urls
из ВСЕХ карточек. Ручки manual/update принимают elements.
Фронт: компонент CompetitorElementsEditor встроен в формы создания и правки на экранах
Поле и Предложения. Прежний баг - пересбор directory_urls из двух полей - закрыт.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:32:16 +03:00
Дмитрий 9b8475feb7 style(автоподбор): Pint по пакету Study + провайдеру 2026-07-03 23:46:28 +03:00
Дмитрий a860d07b6f feat(автоподбор): AnchorGate — фильтр «точно она» для сайта и карточки 2026-07-03 23:17:02 +03:00
Дмитрий ac8ce69489 test(автоподбор): точное имя теста отпечатка (path+utm, не схема) 2026-07-03 23:04:48 +03:00
Дмитрий 85b0fe7869 feat(автоподбор): отпечаток конкурента + запросы EXA 2026-07-03 23:01:37 +03:00
Дмитрий 80e2163d50 feat(автоподбор): PlatformClassifier — площадки по корзинам 2026-07-03 22:58:24 +03:00
Дмитрий c6dc875f17 feat(автоподбор): DomainKinship — родство брендов, близнецы НЕ сливаются 2026-07-03 22:54:11 +03:00
Дмитрий bd869966cf feat(автоподбор): CompetitorGeo — гео по региону (засеян Красноярск) 2026-07-03 22:49:38 +03:00
Дмитрий f295351504 feat(автоподбор): Конкурентное поле — финальный проход склейки, вскрыть всё, группы дублей, телефоны, отсев рекламных номеров 2ГИС
Шаг 1 «Конкурентное поле» — чекпоинт:
- Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча.
- Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента.
- Колонка phones jsonb; телефон-дубли видны уже на «Предложениях».
- Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт».
- Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание.
- Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм.

Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:34:03 +03:00
Дмитрий b2699f0d84 feat(y360): баланс почты Яндекс 360 — ручной ввод + кнопка Пополнить
email — денежный сервис; сумма вписывается в админке «Система» (Yandex360BalanceStore),
светофор по порогам, кнопка «Открыть оплату»/«Пополнить» → admin.yandex.ru/products.
Робот-скрейпер отклонён (SPA Яндекса враждебен ботам + автопополнение защищает баланс).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:34:13 +03:00
Дмитрий f2ca0c8cb0 feat(биллинг): баланс блокирует запуск проекта, а не создание — единый гейт
Создавать проекты можно всегда; баланс проверяется в момент ЗАПУСКА
(создать-и-запустить / запустить / возобновить одиночно и пачкой / автоподбор)
под замком на клиента (без гонок). Не хватает — проект остаётся на паузе с
меткой preflight_blocked_at, клиенту сообщение в рублях (сколько пополнить).
Групповой запуск «сколько влезло». Нет активного тарифа на дату → запуск
запрещён (fail-closed). Гейт реквизитов первого проекта добавлен и в автоподбор.

- LaunchBalanceGate — единый гейт вместо 3 копий preflight (ProjectController
  store/update, AutopodborController), под DB::transaction + lockForUpdate(Tenant).
- ProjectService::create($launch) + новый setActive(); bulk resume «сколько влезло».
- AutopodborProjectCreator: пачка в транзакции через общий ProjectService::create.
- Идемпотентность box/phone_type миграций автоподбора (Schema::hasColumn guard).
- Тест-инфра: afterRefreshingDatabase восстанавливает месячные партиции.

Тесты фичи 40/40 зелёные. Спека и план — docs/superpowers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:07:22 +03:00
Дмитрий 2adc363123 feat(external): CaptchaLivenessProbe — статус капчи
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:03:52 +03:00
Дмитрий edc648d37e feat(external): JivoLivenessProbe — живость чата
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:03:51 +03:00
Дмитрий 31e7e2e77e feat(external): YooKassaLivenessProbe — живость платёжного шлюза
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:03:50 +03:00
Дмитрий dc392945e7 feat(external): SmtpLivenessProbe — живость почты Yandex 360
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:03:50 +03:00
Дмитрий 8869cf51eb perf/fix автоподбор: параллельный сбор 2ГИС по 3 + большие таймауты + безопасное письмо + node из config
Ускорение шага 1: CategoryScraper грузит страницы категории 2ГИС не по одной, а пачками поперёк рубрик
через BatchPageFetcher. На остывшем xfetch 2ГИС упал с ~23 мин до ~5 мин и собрал 139 фирм вместо 12.
Общий лимит одновременных xfetch снижен 4→3 (безопаснее под оконный rate-limit сервиса).

Таймауты ИИ/EXA подняты с запасом (латентность моделей плавает, сбор асинхронный, клиент ждёт):
sonar 120→300с, отсев агрегаторов 90→240с, общий AITUNNEL 30→120с, EXA 30→90с. Джоба: один прогон
без авто-ретраев (tries 3→1, чтобы таймаут не перезапускал платный движок трижды) и общий таймаут 900→1800с.

Фиксы, найденные на живом прогоне:
- Письмо «готово» больше не роняет прогон при недоступной почте (тесты на сбой SMTP при status done/empty).
- Яндекс-загрузчик берёт путь к node из config (autopodbor.node_bin, дефолт node) — из-под PHP node
  теперь находится; на живом прогоне Яндекс дал 78 фирм вместо 0.

По TDD. Бэкенд автоподбора 262/262, Pint чисто. Живой прогон end-to-end: ~8 мин, 14 новых конкурентов,
списание 300 ₽ корректно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:43:34 +03:00
Дмитрий 7f2e896c38 feat автоподбор параллельная загрузка карточек справочника пачками по 4
Замер боевого лимита xf4.ru показал безопасный потолок 4 одновременных запроса — выше идут 429 Too Many Requests, плюс есть лимит за окно времени. Сбор до 25 карточек филиалов в XfetchDirectoryFetcher грузился по одной секунды на каждую — узкое место шага 2.

Новый интерфейс BatchPageFetcher + XfetchClient::htmlBatch грузит страницы порциями по services.xfetch.concurrency дефолт 4, параллелит внутри порции через Http::pool, ретраит пустые и 429. XfetchDirectoryFetcher берёт карточки одной пачкой если загрузчик batch-совместимый, иначе по одной. Два справочника шага 2 оставлены последовательно намеренно — иначе 2x4=8 за лимитом. Число выносится в XFETCH_CONCURRENCY без деплоя.

По TDD. Бэкенд автоподбора 258/258, Pint чисто.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:52:18 +03:00
Дмитрий aef695e2e2 style(автоподбор): pint-форматирование — скобки конструкторов, порядок импортов, выравнивание; логика без изменений
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:37 +03:00
Дмитрий 2834ff0f5a feat(автоподбор): сверка находок со состоянием клиента — 3 группы предложений + скрытые дубли, мягкое удаление в архив, убран вычет себя
Stage 1: опознавалка фирмы по сайту/карточкам, ProposalClassifier new/actualize/archived/hidden, box=archived, bulk удалить всех ранее удалённых. Backend 249/249, front autopodbor 61/61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:45:11 +03:00
Дмитрий d84bc396cf feat(автоподбор): центр похожести из описания ниши, а не только доменов-примеров
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:44:43 +03:00
Дмитрий f04c82e5ee fix(автоподбор): дедуп ДО отсева агрегаторов + файл состояния/промт продолжения
Отсев агрегаторов шёл по СЫРЬЮ до склейки — gpt-4o-mini залетало 227 позиций
с дублями (Ломбардико ×6). Порядок в FindCompetitorsAssembler: сначала
склейка/дедуп, потом отсев — классифицируем ~90 уникальных, а не 227.

Плюс файл состояния сессии (что сделано/осталось) + промт восстановления
контекста для компакта. TDD, 234/234 зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:27:26 +03:00
Дмитрий 2975bf7967 feat(автоподбор): масштаб и чистота шага 1 — пул EXA, очередь-throttle, батч агрегатора, узкие рубрики, дедуп по словарю
Масштаб (лимиты внешних сервисов — на КЛЮЧ, общий на всех клиентов):
- EXA: параллельный пул findSites (concurrency=5) вместо 40 запросов по одному.
- Агрегатор: батч вместо ~90 запросов по одному + чанкинг по 40 и длинный
  таймаут 90с (большой список gpt-4o-mini не успевал в 30с).
- Очередь autopodbor + WithoutOverlapping на всех 3 джобах — глобальный
  потолок = число воркеров; assertNoInFlight держит один подбор на клиента.

Чистота выдачи (универсально, без зашитых ниш):
- Анализатор: узкие рубрики, запрет зонтичных слов («финансовые услуги» тащила
  юрфирмы/банки).
- Дедуп: ключ имени минус слова из СЛОВАРЯ рубрик прогона («Яричъ Ломбард»→«Яричъ»);
  слово-категория из данных, не из списка. Гард: склейка только если остаётся
  ровно один слово-токен (генерики вроде «займы под залог» не трогаем).

TDD, 233/233 (unit+feature) зелёные. НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:18:09 +03:00
Дмитрий f420924257 feat(автоподбор): рубрика из списка Яндекса в описание + межотраслевой промпт
Playwright тянет рубрику фирмы прямо из СПИСКА Яндекса («Ломбард, автоломбард»,
«Микрофинансовая организация», «Банк») без захода в карточку — быстро и бесплатно.
Рубрика идёт в описание конкурента → в похожесть-эмбеддинги (раньше меряли по
голому имени, хлам и целевое сбивались). Промпт анализатора обезличен: примеры
из РАЗНЫХ отраслей (стоматология/автосервис/доставка) + «подбери под описание
клиента» — движок универсален, не подточен под нишу займов Омеги. TDD 216/216, НЕ прод.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 07:00:41 +03:00