Антибот 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>
Москва 495, Питер 812, Екатеринбург 343 — код города берётся со страницы
конкурента или из города запроса, никакой привязки к Красноярску. Защита
от регрессии. Autopodbor 41/41.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сканер кода сайта выкидывал короткие локальные номера (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>
- CURLOPT_SSL_VERIFYPEER/VERIFYHOST включены
- isSafeUrl: только http/https, блок loopback/приватных/служебных IP
- FOLLOWLOCATION выключен, протоколы ограничены HTTP/HTTPS
- render-page.cjs валидирует схему URL перед навигацией
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Сверка прототипа с реализацией показала расхождения — закрыты по TDD (dev, фронт):
- F1: экран «Предложения» (FieldProposalsScreen) переписан под вид «Поля» —
карточки-плитки field-shared, тип+«предложение», крупная похожесть, Сайт +
Справочник 2ГИС·Яндекс, править/удалять в карточке, массовый перенос; кнопка
«Собрать конкурентов» открывает единое окно сбора 300 ₽ вместо старого autoform.
- F2: новый дружелюбный админ-экран AdminAutopodborPricingView (правка цен
доп.услуг через PUT /api/admin/system-settings/{key} с обоснованием для аудита,
сетка лидов для справки) + маршрут /admin/autopodbor-pricing + пункт меню.
- F3: колонка «когда списывается» в панели доп.услуг биллинга.
- M2: удалён мёртвый экран FieldManualCompetitorScreen (+ спека) — на него не
было переходов; ручное добавление живёт окном на «Поле».
Тесты автоподбор+админ 43/43 зелёные, продакшен-вёрстка eslint-чистая, vite build ✅.
НЕ на проде. M1 (18:00/21:00 МСК) — не баг, реальный инвариант продукта, не трогал.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Фича «Конкурентное поле» на dev до уровня прототипа 2026-06-29-konkurentnoe-pole-proto.html.
Данные: box (proposal|field) на competitors+sources; phone_type city/mobile/tollfree рядом
с phone_kind (вариант C). 3 миграции, дефолты тарифов 300/50.
API (AutopodborController): GET /field (+счётчики), GET /proposals, PATCH/DELETE competitors
и sources с гвардами активного проекта, переключение box, POST /competitors/manual (+directory_urls),
competitor(id) обогащён box+project-статусом; projectStatus отдаёт limit/delivered/days/regions.
Смена источника проекта = PATCH /api/projects/{id} (реальный гвард слепка §14.10).
Фронт: FieldWorkspaceScreen/FieldCompetitorScreen/FieldProposalsScreen/FieldManualCompetitorScreen
+ field-shared.css (Forest) + AutopodborServicesPanel в Биллинге. Дословно по прототипу: подзаголовки,
баннер предложений, баннер правил времени 18:00 МСК, Справочник 2ГИС·Яндекс, статус проекта
5/день·заявки, окна сбора с ценами 300/50 + «что известно», полные формы. Пункт меню «Конкурентное поле».
Тесты: backend автоподбор 80/80, фронт автоподбор 49/49. Движок шага 2 = заглушка FakeCompetitorAgent.
OmegaDemoFieldSeeder — только для визуальной проверки (НЕ на прод).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AutopodborChargeService::chargeForRun — DB::transaction + lockForUpdate
на AutopodborRun (guard идемпотентности по balance_transaction_id) и Tenant;
bcmath (bcsub/bccomp/bcmul), никаких float; throw InsufficientBalanceException
до любых изменений баланса при нехватке средств.
- Миграция 2026_06_28_110100: расширяет CHECK constraint
balance_transactions_type_check — добавляет 'autopodbor_charge'.
- Тест: 2 money-инварианта (идемпотентность + noop при нехватке).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AutopodborNormalizer: domainHead (схема/www/путь/порт → голова),
phone (через PhoneNormalizer::normalize → 7xxxxxxxxxx без плюса),
sourceKey и competitorKey для дедупликации конкурентов и источников.
4 теста, 9 assertions, все GREEN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Политики tenant_isolation приведены к каноничной харднинг-форме
NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
(после переноса на gitea/main с v8.57). Запись CHANGELOG для всех 3 таблиц.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>