Commit Graph

1228 Commits

Author SHA1 Message Date
Дмитрий 7d6516e6b2 feat(autopodbor): журнал слияний + возврат поглощённой карточки
Слияние конкурентов больше не «чёрная дыра» (баг «САПС/Метрополис»: карточка
исчезла без следа, вернуть было нечем). Теперь каждое слияние пишет событие в
autopodbor_merge_events со СНИМКОМ поглощённых карточек (имя/сайт/телефоны/
справочники), кто и когда слил.

- Таблица autopodbor_merge_events (+RLS tenant_isolation, паттерн 1:1 с sources,
  rls-ревью OK; nullOnDelete на survivor_id/user_id — история не рушится).
- AutopodborCompetitorMerger::merge($tenantId,$ids,$name,$userId) пишет событие.
- GET /field/merge-events — история; POST /field/merge-events/{id}/restore —
  возврат поглощённых как предложения (защита от повторного возврата).
- Фронт: панель «🕘 История слияний» + «↩ Вернуть карточку».

Бэк 428/428, фронт 12/12, Pint чистый, build ок. schema.sql канон-синк всех
таблиц автоподбора — предвыкатный TODO (вся фича migration-only в этой ветке).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:52:43 +03:00
Дмитрий f06b336363 feat(autopodbor): кросс-ящик дублей поле↔предложения на экране поля (доведён до рабочего)
Кнопка «Найти и объединить дубли» на экране поля теперь ловит и предложения,
дублирующие карточки поля (scope=cross): счётчик учитывает их, окно помечает
«· предложение», объединение сливает предложение в карточку поля (выживает поле,
merger не трогали). Бэкенд оставляет только группы с ≥1 карточкой поля — пары
чисто-предложений на экране поля не мусорят.

Файл-статус сессии — docs/superpowers/findings/2026-07-05-avtopodbor-session-status.md.

Feature 154/154, фронт 32/32, Pint чистый, билд собран.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:19:38 +03:00
Дмитрий 9fce28fda8 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
Дмитрий 36c3a7fe7f 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
Дмитрий f1352083a1 docs(autopodbor): решение по сайтам проектов застройщика (ДСК) — что упустили и план
Застройщик = холдинг из проектов-ЖК, каждый со своим сайтом; шаг 2 берёт только
главную карточку. Решение: проекты = элементы конкурента, авто-открытие из 2ГИС/
сайта (нужен го владельца), не переобрабатывать уже собранную главную. Эталон —
ДСК (tenant 38, competitor 720). Открытые вопросы О-1..О-4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 06:49:16 +03:00
Дмитрий f757d8adaf docs(autopodbor): handoff сессии 04-05.07 - что сделано по #2, что открыто (#1 прогресс, баг городов, похожесть)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 03:42:41 +03:00
Дмитрий 47bc508246 docs(autopodbor): находки похожесть-погода и конкурент-из-элементов + план Часть 1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:14:54 +03:00
Дмитрий 66a0f2bb67 docs(автоподбор): снимок 04.07 — движок шага 2 на ветке + приёмка 385 + UI + что осталось
Полный статус: движок перенесён (134 теста, ревью «Ready»), 385 подтверждён 1:1 с
ЭТАЛОНом живьём, корневой баг «нашлось мало» = завис queue:work (починен перезапуском),
UI (метки/регион-тайпахед/групповое создание) закоммичен ac3d6975. Осталось: приёмка
412/414/416/420, открытый вопрос дефолта «Вся РФ», перенос на прод по слову.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 11:13:25 +03:00
Дмитрий 2907a02d45 docs(автоподбор): план реализации переноса движка шага 2 (TDD, 12 задач)
12 задач по TDD: тумблер+is_federal, CompetitorGeo, DomainKinship (близнецы
НЕ сливать), PlatformClassifier, отпечаток+запросы EXA, AnchorGate,
FootprintSearch, SiteOpener, DeepStudyCollector (маппинг 1:1 со словарём
чипов песочницы), развилка по тумблеру, DI, приёмка на 5 против ЭТАЛОНа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:32:17 +03:00
Дмитрий 07fc46d524 docs(автоподбор): дизайн переноса движка «глобального шага 2» в боевой шаг 2
Закон: результат боевого шага 2 строго 1:1 с песочницей. Полный движок за
тумблером autopodbor.deep_study, старый путь — запасной. Явная НЕ-цель:
сайты-близнецы НЕ сливать (красломбард.рф и kraslombard24 — две отдельные
записи). Хранение/биллинг/идемпотентность не трогаем. Приёмка — те же 5
конкурентов через боевой шаг 2 = одобренные артефакты bylo-stanet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 22:17:19 +03:00
Дмитрий ec4a2b178c feat(автоподбор): песочница «глобального шага 2» + артефакты было/станет
Прототип нового шага 2 «Собрать источники» — отдельный скрипт scripts/sandbox-step2.php,
боевой шаг 2 НЕ тронут. Отпечаток фирмы (домен+имя+тип+город) → поиск по отпечатку через EXA
(гео-скоуп по типу фед/рег) → фильтр «точно она» по якорному телефону → глубокое вскрытие
сайтов (код/скрытые/подменный коллтрекинг/Контакты) + 2ГИС(поиск по имени, JSON) + Яндекс(Playwright)
→ все номера с пометками. Генерит HTML «было/станет» на конкурента (было из БД, станет из прогона),
ссылки кликабельные. scripts/_shot.cjs — скриншот HTML в PNG.

Артефакты bylo-stanet/ на 5 конкурентов с «Предложениями» (tenant 36). Свежие со всеми фиксами —
КрасЛомбард и Банк ДОМ.РФ; остальным нужен финальный перегон. Детали, дизайн и 4 фикса
(схема сайта, Яндекс→Playwright, 2ГИС по имени/JSON/ретрай, близнецы кир/лат) — в файле находок.

НЕ в портале, НЕ на проде. Прототип для оценки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:22:13 +03:00
Дмитрий df83fa836c 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
Дмитрий 65770f0e70 docs: снимок 02.07 — launch-gate ЯДРО на проде + автоподбор в воркстри + промт продолжения
Консолидированное состояние двух треков (launch-gate выкачен на liderra.ru core-only;
автоподбор работает только в воркстри) + квирки окружения/тестов + промт для
восстановления контекста после компакта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:22:53 +03:00
Дмитрий 4c05af7fda docs(биллинг): статус фичи launch-gate — реализовано, на gitea, follow-up
Спека помечена РЕАЛИЗОВАНО (коммиты f78ab4af/9ff7bc8f/8a8783df), ревью opus без
критичных находок. Зафиксирован follow-up: прод-флаг BILLING_LAUNCH_REQUIRES_ACTIVE_TIERS,
DRY-сведение оставшихся копий правила, пред-существующая партиция-хрупкость тест-инфры.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:26:41 +03:00
Дмитрий f78ab4af3d 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
Дмитрий a88c4034b7 docs(автоподбор): снимок 02.07 — живой движок работает end-to-end + ускорение 2ГИС + промт продолжения
Зафиксировано состояние: фича Stage 1+2 собрана, живой сбор проверен (прогон #12 ~8 мин, 139 фирм 2ГИС,
14 сохранено, 300₽), коммит b3f5158e, квирки (лимит xfetch, EXA OS-env, очередь-локально, node-путь) и
промт для восстановления контекста. Детали движка — в памяти project-autopodbor-engine-live-working-2026-07-02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 06:30:03 +03:00
Дмитрий 76c67e1fc5 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
Дмитрий 82123057f5 docs(автоподбор): спека + план фичи сверки находок + файл состояния Stage 1 и промт продолжения
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 15:47:12 +03:00
Дмитрий 68341e5576 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
Дмитрий f1ab608444 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
Дмитрий f9f4743392 docs(автоподбор): файл состояния движка + промт восстановления контекста (01.07)
Единая точка: что сделано (движок шага 1 пересобран — АНАЛИЗ→канал А 2ГИС(xfetch)+Яндекс
(Playwright)→слияние→канал В федералы(EXA)→сборка; 136 unit+74 feature зелёные, HEAD dee2ebbc),
что осталось (полный живой прогон+сверка на 72, калибровка, параллельность), ключи/окружение,
карта кода, промт для восстановления после компакта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 04:55:51 +03:00
Дмитрий 5a65165114 feat(автоподбор): движок шага 1 пересобран под финал v4 (каналы А+В, EXA)
Замена вырожденного «одна фраза → одна страница» на §12/§11.3 финал:
- Шаг АНАЛИЗ (ChannelA\AitunnelQueryAnalyzer): описание → запросы-рубрики (мелкая модель).
- Канал А (ChannelA\CategoryScraper): скрейп категории 2ГИС с пагинацией → резолв карточек.
- Канал В (ChannelB\*): ОДНА модель sonar-reasoning-pro × 2 прохода → ТОЛЬКО имена
  федералов; стоп-лист = имена из А + примеры; сайт федерала через EXA (ExaSiteFinder),
  т.к. у федерала нет карточки в 2ГИС/Яндексе на регион.
- Оркестратор LiveFindCompetitors переписан: АНАЛИЗ→А→В→слияние→отсев→дедуп→похожесть→DTO.
- Провайдер перепрошит; config services.php +research_model/exa.
Похожесть — эмбеддер-модель (математически), резолвер/дедуп — без изменений.
Всё за тонкими границами, офлайн-тесты на фикстурах: модуль 130 unit + 74 feature зелёные.
Провайдер за флагом autopodbor.real_find; на проде не меняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:06:10 +03:00
Дмитрий d468471707 docs(автоподбор): зафиксирован последний движок §12 v4 + сверка PHP
Свод воедино: методика §12 (3 канала А/В/0 → резолвер → DaData → дедуп →
эмбеддинги → DTO) + все дословные промпты (5 моделей-исследователей,
нормализатор, фильтр агрегаторов). Честная сверка: PHP LiveFindCompetitors
собрал только нижнюю половину (резолвер/дедуп/похожесть/телефоны) и берёт
1 фразу/1 страницу → 3 находки; верхняя половина (АНАЛИЗ+мульти-запрос+
пагинация канала А, канал В 5-моделей) не перенесена. Эталон работы движка —
прогон 29.06 (72 конкурента). План переделки PHP под §12 один-в-один.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:03:48 +03:00
Дмитрий f940c060b8 docs(автоподбор): контекст-промпт продолжения — живой портал :8001 (белый экран) + полное состояние 2026-06-30 19:11:39 +03:00
Дмитрий 0636a3c1b4 docs(автоподбор): план шага1 — весь движок собран (A–F+C+D+H-ядро), осталось живое подключение+флип 2026-06-30 18:04:21 +03:00
Дмитрий f7ec94e107 docs(автоподбор): план шага1 — прогресс A/E/F/B готовы, осталось живое C/D/G/H 2026-06-30 17:47:30 +03:00
Дмитрий 4de9474a27 docs(автоподбор): план шага1 — под-блок A (резолвер) отмечен готовым 2026-06-30 17:29:53 +03:00
Дмитрий 858d8be1b2 docs(автоподбор): контекст-промпт продолжения шага 1 — за основу движок v4 (2 дня прогонов)
Резюме для подхвата после компакта: брать ПОСЛЕДНИЙ вариант (§12 v4), ранние
не воскрешать (ИНН выкинут, похожесть=эмбеддинги, агрегаторы=LLM, is_federal по факту).
Следующая задача A2 TwoGisResolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:51:40 +03:00
Дмитрий 2e37a7a380 docs(автоподбор): план шага 1 — реальный findCompetitors, фундамент-первым
Декомпозиция движка v4 (§12) на под-блоки A–H: резолвер (фундамент, офлайн-TDD) →
слияние/похожесть → каналы А/В/0 → DaData → оркестрация+флип за флагом. Живые
блоки с платными API — отдельными заходами после «го» владельца.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:35:48 +03:00
Дмитрий 92e2eef702 docs(автоподбор): статус — шаг 2 встроен в боевой путь и проверен живым прогоном
Обновлён GOTOVNOST: интеграция шага 2 (провод реального агента, где-нашли→БД→API,
код города по региону, ретрай 2ГИС) сделана и проверена вживую на kraslombard24.ru.
Следующий блок — шаг 1 (реальный findCompetitors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:29:09 +03:00
Дмитрий c2d5592696 docs(автоподбор): статус готовности + контекст-промпт продолжения
- GOTOVNOST-status: к внедрению НЕ готов; шаг 2 собран по частям, не встроен;
  шаг 1 — реальной логики никогда не писали (всегда заглушка), из репо ничего
  не терялось (проверено git); собрать шаг 1 = написать одну findCompetitors.
- KONTEKST-prodolzhenie: промпт для корректного подхвата контекста + план.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:08:53 +03:00
Дмитрий 3f3a142b76 fix(безопасность): XSS в ссылках «где нашли» + path traversal в serve.js
Сек-ревью коммита нашло две дыры:
1. FieldCompetitorScreen: href из where_found рендерился без проверки схемы —
   javascript:-ссылка выполнила бы скрипт при клике. Добавлен safeUrl():
   кликабельны только http/https, остальное — простым текстом. +TDD-тест.
2. serve.js (локальный dev-сервер прототипов): обход каталога. realpath-сверка
   с ROOT + require isFile + bind 127.0.0.1.

Autopodbor FieldCompetitor 13/13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:53:04 +03:00
Дмитрий 6e5f47962c docs(автоподбор шаг2): живой показ КрасЛомбарда + сводка + резервный Playwright-путь
- Скриншоты живого прогона: страница глазами клиента (мой рендер) и НАСТОЯЩИЙ
  экран портала FieldCompetitorScreen с живыми данными (findings/*.png).
- Сводка STEP2-SVODKA: §4 чистый прогон (29 номеров + 2ГИС), §6.7 привязка
  офис↔номер, §6.8 ретрай флаки 2ГИС.
- R&D, план-промпт, прототипы sbor1/sbor2 + живая собранная страница.
- Резервный Playwright-путь справочников (CurlPlaywrightFetcher.directory +
  render-firm.cjs) — заменён xfetch; хвост SSRF §6.4 не на боевом пути.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:48:26 +03:00
Дмитрий 1b76cfec15 feat(автоподбор шаг2): справочники 2ГИС через xfetch.ru
Антибот 2ГИС бьём сервисом xfetch.ru (render:true, timeout:20 —
без timeout страница не дорисовывается). Доказано на живом КрасЛомбаре:
поиск → 12 филиалов → телефон + адрес каждой карточки.

- PageFetcher — граница «достать HTML» (тестируется без сети)
- XfetchClient — POST к xfetch, декод base64; без ключа молча пусто
- XfetchDirectoryFetcher — список→филиалы→карточки через DirectoryParser
- DirectoryParser — чтение списка и карточки 2ГИС (был в хвостах)
- config services.xfetch + .env.example; ключ только в .env (gitignored)

Яндекс.Карты — отдельно (другой формат URL карточек). TDD: Autopodbor 46/46.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:38:18 +03:00
Дмитрий b78c3edb8c fix(автоподбор шаг2): не терять номера офисов, написанные без кода города
Сканер кода сайта выкидывал короткие локальные номера (271-33-33,
2-828-828 и т.п.) как мусор — терялись реальные телефоны филиалов.
Теперь короткий номер из tel:/schema/microdata достраивается кодом
города: сперва по преобладающему коду полных номеров той же страницы,
иначе по коду региона запроса; если код не определить — номер не
теряется, а помечается «требует проверки» (phoneKind=uncertain).
Из тела текста короткие формы не достраиваются (защита от ложных).

TDD: 6 новых тестов, весь Autopodbor 40/40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 12:11:27 +03:00
Дмитрий aedefa3a94 docs(автоподбор): план Plan A (ядро) + resume-промпт
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:42:47 +03:00
Дмитрий fbebe40383 docs(автоподбор): план Plan B — добыча + RealCompetitorAgent (TDD)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:41:19 +03:00
Дмитрий 3561028dd2 docs(Конкурентное поле): прототип, план реализации, эстафета и Playwright-сверка с Омегой
HANDOFF (состояние/решения/окружение), impl-plan (фазы+догрузки), кликабельный прототип
2026-06-29-konkurentnoe-pole-proto.html (визуал-эталон), omega-visual-check (живая сверка
рабочего места с реальными конкурентами Омеги, скрины FIELD-*/PROTO-*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 04:20:25 +03:00
Дмитрий a3b68dbb95 docs(автоподбор): записка для продолжения после компакта (state + что прочитать)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:54:44 +03:00
Дмитрий b5c88b2f1d docs(автоподбор): план — выделенная тестовая БД liderra_testing_apk + RLS-харднинг конвенция
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:08:59 +03:00
Дмитрий 1fe071f203 docs(автоподбор): дизайн-документ + план реализации
Дизайн и пошаговый план фичи «Автоподбор конкурентов» (ИИ-агент находит
конкурентов и их источники). Движок — отдельной сессией, здесь розетка+заглушка.
План сверен с кодом: RLS app.current_tenant_id, tenant-контекст SET LOCAL,
тестовая БД liderra_testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:57:27 +03:00
Дмитрий 2911f3ac0e docs(ПИЛОТ): снимок 28.06 — починен тихо сломанный биллинг-сторож (RLS) + playwright durable
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Свод за заход «закрывай хвосты»: разбор и фикс preflight-sweep/reminder (no-op с 26.06
из-за RLS-роли очереди), self-heal 4 проектов на проде, деньги t2 целы, playwright в deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:19:24 +03:00
Дмитрий 75dded78a1 fix(биллинг): sweep и reminder перебирают тенантов через BYPASSRLS + playwright в зависимостях
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
Корень: после переезда на Managed PG очередь ходит под ролью crm_app_user (RLS),
и Tenant::query() в BalancePreflightSweepJob/BalanceFrozenReminderJob отдавал 0 строк
без app.current_tenant_id — биллинг-преflight молча стал no-op с 26.06 (ни заморозок,
ни снятия проектных блоков). Перечень тенантов теперь берётся через pgsql_supplier
(BYPASSRLS), модель грузится внутри per-tenant SET LOCAL контекста. Логика проверена
на боевых данных: t25/t26 снимутся, t27/t30 заморозятся.

Playwright рантайма supplier-портала объявлен в dependencies ровно 1.59.0 под
chromium-1217 + package-lock синхронизирован; деплой ставит его npm ci --omit=dev,
durable к чистке node_modules.

Тесты Billing 18/18, pint/phpstan чисто.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:11:36 +03:00
Дмитрий b2f08f28d5 feat(дашборд): Этап B+C — кликабельные группы Заказа, ссылки Здоровья, «Открыть всё»
B: строки групп «Заказа» кликабельны → проекты у поставщика (поиск по источнику) +
кнопка «Открыть проекты у поставщика». C: подсистемы «Здоровья» кликабельны →
Инциденты/Система/Интеграция с поставщиком; «Финансы» → Биллинг/Все клиенты;
«Клиенты» → Все клиенты. Сквозная вложенность дашборда замкнута до источников.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:23:30 +03:00
Дмитрий 6536c19c96 feat(дашборд): Этап A — сквозная вложенность Лиды до источника
Экран «Лиды» (/admin/leads): серверный список с фильтрами (дата/канал/поставщик/
статус/поиск) + пагинация (масштаб 10⁴+ лидов). Карточка лида (/admin/leads/{id}):
полная цепочка — ОТКУДА (поставщик B1/B2/B3 + канал + источник + регион) → КОМУ
(сделки клиентов через deals.source_crm_id = supplier_leads.vid). Дашборд: drill
Лиды +топ-10 последних + «Открыть все лиды →». Nav-пункт «Лиды». ПДн-телефон
маскируется (152-ФЗ). Тесты: backend 3 + FE 5 (38 FE всего зелёные).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:14:47 +03:00
Дмитрий 5c68b24c7b feat(дашборд): выбор периода — свой диапазон дат + спека вложенности/масштаба
Фундамент под сквозную вложенность: periodRange() читает date_from/date_to
(приоритет) либо preset; Финансы и Клиенты считаются по выбранному периоду через
whereBetween. FE: «Свой период» + два date-поля + «Применить» → date_from/date_to.
Спека дизайна A+B+C+масштаб сохранена. Baseline перегенерирован (getJson тестов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:54:09 +03:00
Дмитрий d961d1617a feat(админка): пункт «Командный центр» в левом меню
Дашборд не было видно в сайдбаре — уйдя с него, нельзя было быстро вернуться.
Добавлен первый nav-пункт «Командный центр» → /admin/dashboard (иконка dashboard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:42:19 +03:00
Дмитрий 1ecb965981 feat(дашборд): плитка «Клиенты» — активность + новые + спящие
6-я плитка «👥 Клиенты» со светофором (amber если есть спящие) + drill:
KPI за период (всего активных / новых / заходили / получали лиды / платили),
список новых клиентов (с датой входа/лидами/балансом) и «спящих» (активные
без входа 14+ дней или ни разу = не активировались). Клик по строке → карточка
клиента. Backend: clients() endpoint + clientsTile в summary (cross-tenant через
pgsql_admin); сигналы — users.last_login_at, deals, balance_transactions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:28:47 +03:00
Дмитрий 22ad20337a feat(балансы): баланс поставщика = остаток номеров × 20 ₽
У кабинета crm.bp-gr нет денежного баланса — есть «Баланс ГЦК» (остаток номеров)
в выпадашке шапки (table.balancetbl). supplier-balance.js логинится, раскрывает
выпадашку, читает «Баланс ГЦК» -> {numbers}. Провайдер: деньги = numbers ×
number_price_rub (20 ₽/шт, подтверждено владельцем). Live: 3096 -> 61 920 ₽.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:54:10 +03:00
Дмитрий fa404e98ec fix(балансы): свежий query-builder на итерацию джобы (PK violation на 2-м прогоне)
Переиспользование одного DB-билдера в цикле накапливало where-клаузы →
updateOrInsert уходил в INSERT существующей строки → SQLSTATE 23505 на проде
при повторном сборе. Билдер теперь создаётся внутри цикла. + тест на 2 прогона.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:32:17 +03:00