Commit graph

76 commits

Author SHA1 Message Date
ff979d4afd fix(tradein/cian): капча Циана с HTTP 200 — отказ площадки в обоих слоях (#3402)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 5m11s
Циан отдаёт капчу (`<title>Captcha - база объявлений ЦИАН`, 44 КБ) и страницу
ошибки (`<title>Ошибка - Циан`, 374 КБ) с кодом 200. Детектор сайдкара их не знал
(_REFUSAL_STATUSES {403,429} + маркеры Авито/Домклика), HTML уезжал клиенту как
успех, extract_state возвращал None и провайдер печатал «defaultState extraction
failed» — отказ ПЛОЩАДКИ читался как дрейф НАШЕЙ разметки. Аренда при этом не
менялась: fetch() уже отрапортовал mark_health(ok=True), fail-streak обнулялся, и
один капча-узел сжигал батч целиком (6200: 0/210; 6123/6091/6052/6032/6010/5981:
0/400 — против 161/162 через здоровый узел на прогоне 13).

Два слоя, потому что образы backend и browser деплоятся раздельно и расходятся
на часы:
* сайдкар (browser/server.py) — детект по <title> на обоих путях (navigate и
  подзапрос) → BanPageDetectedError → прежний путь #3288/#3379: 403 + ban_page +
  ЧЕСТНЫЙ upstream-статус 200;
* kit (providers/cian/detail.py) — при провале extract_state те же маркеры →
  CianBlockedError вместо тихого None, плюс report_platform_ban по живому lease.
  Там же ветка SidecarBanPageError: отказ, опознанный сайдкаром, больше не
  гасится общим `except` в «не смогли разобрать».

Слово `captcha` признаком быть не может: в нормальной карточке оно встречается 11
раз, на капче 17. Детект по <title> с нормализацией тире.

`_report_platform_ban` → `report_platform_ban` (публичный): тем же путём обязан
идти отказ, распознанный не сайдкаром, а провайдером. report_ban один только
пишет бан пары «узел×источник» — сменить сожжённую аренду ВНУТРИ батча позволяет
только fail-streak (_LEASE_ROTATE_AFTER_FAILS).
2026-09-06 14:10:57 +05:00
1b86ba7e06 fix(#3376): первая точка — только по третьей точке истории; числа под критерием
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
2026-09-06 05:12:30 +05:00
721ceb9876 fix(tradein): выборка миграции 286 повторяет гейт 1:1, правило первой точки (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m56s
Ревью нашло, что миграция и код ловили РАЗНОЕ. Миграция брала базой предыдущую
СЫРУЮ строку (lag), гейт — предыдущую ОСТАВЛЕННУЮ. На 1M→10M→1M→10M (цена 1M)
lag-версия удаляла честную точку, на 1M→10M→1.05M→9.9M — не была идемпотентной
(второй прогон доедал 9.9M). Теперь кандидаты выбирает PL/pgSQL-цикл, пошагово
повторяющий drop_decimal_slips, а правило первой точки — отдельным INSERT..SELECT
уже по ОСТАВШИМСЯ строкам.

Правило первой точки — из прод-разбора: 12 из 20 остатков domklik это серии вида
330 000 → 3 300 000 (текущая цена 3 300 000) и 420 000 → 4 200 000 → 4 500 000,
где дефектная точка ПЕРВАЯ и базы слева у неё нет. Свидетелей по-прежнему два:
×10 ко второй точке И подтверждение второй третьей-или-текущей-ценой. Решение по
первой точке принимается по kept-серии, а не по сырой, — иначе гейт теряет
идемпотентность (перебор ловит 1122 таких прогона).

Идемпотентность доказана НА ГЕЙТЕ: property-тест gate(gate(s)) == gate(s) по всем
сериям длины 2-6 (19 525 серий × 3 текущие цены). Фальсифицирован обеими
поломками — сырая база даёт 136 красных прогонов, сырые соседи первой точки 1122.
Раз SQL зеркалит гейт, свойство переносится на миграцию.

Ещё в 286: третий свидетель ПРОТИВ удаления (цена подтверждена триггерной строкой
того же объявления — значит она реально наблюдалась в listings.price_rub) и
финальный шаг |diff_percent| > 100 → NULL по всем источникам, то же правило, что
validate_diff_percent на записи. Ожидаемое число удалений в шапке — 35 + ~12 из
двухсвидетельского предзамера, а не 76 (то была односвидетельская цифра).
Прогон обеих фаз дважды с ROLLBACK — tradein-mvp/scripts/sql/286_dryrun.sql.

yandex: проводка гейта снята как мёртвая. На том пути серия из двух точек, а
свидетель последней — текущая цена лота, то есть она же сама: ветка по построению
не могла выбросить ничего. Оставлен честный комментарий-потолок и ссылка на
follow-up (отлов требует DELETE на следующем наблюдении).

cian: после выброса точки соседу пересчитывается diff_percent (было только у
domclick). domclick: цена листинга берётся RETURNING'ом у UPDATE вместо отдельного
SELECT по PK, пересчёт вынесен в общий recompute_diff_percent с гейтом на пустую
цену (ручной ingest кладёт price_changes из JSONL без валидации).
2026-09-06 03:53:08 +05:00
142967d064 fix(tradein): гейт против сдвига разряда в offer_price_history (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
В priceHistory источников встречаются точки ровно ×10/÷10 к соседям с
возвратом к базе следующей же точкой — это потерянный разряд у источника,
а не рынок. Прод-замер 06.09.2026: 76 таких строк у 55 объявлений
(domklik 48, cian 21, yandex 6; единственная avito-строка — триггерная).
Читатели колонки — медианный торг лендинга (#3223), админка, /scrapers.

drop_decimal_slips живёт рядом с validate_diff_percent — на той же единой
границе записи, что и гейт #3225, и подключён ко ВСЕМ писателям истории
(domclick/detail.py, cian/detail.py, yandex_price_history.py). Критерий
требует двух свидетелей: скачок ×10 к предыдущей точке И возврат к базе у
следующей; у последней точки серии свидетель — текущая цена объявления,
нет и её → точку не трогаем (без второго свидетеля ×10 может быть честной
сменой цены). У domklik после выброса пересчитывается diff_percent
соседа: парсер считал его от базы, которой больше нет.

Миграция 286 чистит уже собранные строки тем же критерием и только у
загрузчика (change_time <> recorded_at): триггерные строки — это живые
смены listings.price_rub, у них другая база отсчёта (см. 285). Падает,
если кандидатов больше 200.
2026-09-06 02:35:39 +05:00
afa93f9a1e fix(tradein): миграция 285 не трогает строки триггера (deep-review блокер)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m56s
Утверждение «формула триггера = формула миграции» было ложным по ИСТОЧНИКУ базы:
record_listing_price_change (131:76-86) считает процент от listings.OLD.price_rub,
а не от предыдущей строки offer_price_history. Пересчёт по lag() портил честные
значения: у листинга, чья история начинается с триггерной строки, lag() = NULL →
−0.82 уходил в NULL; у триггерной строки с соседом-строкой загрузчика база чужая.

Теперь пересчитываем и берём как базу ТОЛЬКО строки загрузчика
(change_time <> recorded_at — триггер ставит обе метки одним now()). Это ровно то,
что делает починенный код: процент внутри истории самой карточки. Окно lag()
сужено до листингов с domklik-строками — иначе оконная функция шла по всей
таблице под lock_timeout = 5s и валила деплой.

Признак закреплён тестом на INSERT загрузчика (recorded_at не указан → DEFAULT
NOW()); при добавлении recorded_at в INSERT тест краснеет — проверено.
В шапке миграции отмечена асимметрия: старые cian-строки с |x| > 100 не чиним.
2026-09-06 01:08:09 +05:00
769a36098b fix(tradein): считать domklik diff_percent из цен, отвергать не-проценты (#3225)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m6s
offer_price_history.diff_percent у domklik содержал РУБЛИ: загрузчик карточки
клал поле источника priceHistory.diff как есть, а clamp_diff_percent только
зажимал его в ±999999.99 — заведомо неправдоподобное значение проходило молча.
Прод 29.08.2026: 8900 из 11 075 непустых значений с |diff| > 50, p50 = -50 010.

- парсер Домклика считает процент сам из соседних price_rub (сортировка по
  change_time); у самой ранней записи предыдущей цены нет -> NULL, не 0;
- clamp_diff_percent -> validate_diff_percent: |x| > 100 не зажимается, а
  отвергается (NULL + warning с listing_id и сырым значением). Гейт стоит в
  общем хелпере, поэтому закрывает и cian-путь;
- миграция 285 пересчитывает уже собранные domklik-строки оконной lag() по
  (listing_id, change_time); идемпотентна (UPDATE только IS DISTINCT FROM).

Closes #3225
2026-09-06 00:48:08 +05:00
c57138f4c7 fix(tradein/proxy): бан площадки рапортует только фетчер — ротация больше не банит свежий узел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m1s
Дедуп report_ban по _banned_lease_id не достигал цели при ротации. Узел 13 ловит
бан-страницу → фетчер репортит бан 13 и по fail-streak меняет lease на 14 →
провайдерский report_ban в providers/avito/detail.py видит уже сброшенный
_banned_lease_id и банит СВЕЖИЙ узел 14, который к площадке не ходил. При трёх узлах
в пуле одна бан-страница выбивала две трети выдачи на 6 часов с эскалацией ban_count.

Убран провайдерский report_ban на ветках SidecarBanPageError в avito/detail.py и
domclick/detail.py: фетчер репортит сам, раньше и по правильному lease. Детекты не от
сайдкара (firewall / 0 карточек в serp.py, QRATOR-маркеры parse_detail_html) фетчеру
не видны — там report_ban остаётся.

Плюс два смежных: fetch()-ретрай ловил httpx.HTTPError, подклассом которого является
SidecarBanPageError, — каждая бан-страница стоила 2 POST'а и +2 к fail-streak (ротация
вдвое раньше задуманного); и NoProxyAvailableError из ротационного _acquire_lease внутри
_report_platform_ban вылетала ВМЕСТО SidecarBanPageError, подменяя диагноз platform на
infra — теперь ротация там best-effort.

Тесты: рабочий пул теперь РОТИРУЮЩИЙ (13→14) — на неподвижном пуле дефект физически не
проявляется. Два теста, пинившие прежний контракт (провайдер репортит), инвертированы:
у них MagicMock-фетчер, который настоящего рапорта не делает.

Refs #3288
2026-09-05 23:25:19 +05:00
bot-backend
58e18e6fec fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
Пройденный QRATOR proof-of-work выбрасывался после каждой карточки: `reuse_context`
во всём репозитории передавал ЕДИНСТВЕННЫЙ вызов — domclick_detail_backfill.py:346.
Значит каждый /fetch Авито шёл через sidecar'овский browser.new_page(), то есть
новый изолированный context с пустой банкой кук. В логе прод-сайдкара это видно
прямо: «PoW-челлендж снят за ~1000мс» печатается на КАЖДОЙ успешной карточке —
челлендж решается заново каждый раз, а не один раз на прогон.

Эффект той же правки у Домклика измерен и записан в domclick_detail_backfill.py:331
— 26 последовательных фетчей сайдкара дали 100% блоков, те же карточки в тёплом
контексте 5/5 за ~2с.

Второй холод — сам заход: providers/avito/detail.py звал fetch(full_url) голым,
без origin и без Referer, тогда как Домклик (#3247) идёт fetch(card_url,
origin=SERP, referer=origin). Существующий для Авито прогрев (warm_up_session)
живёт только в curl-пути и с 22.08 в проде мёртв — AVITO_DETAIL_BACKFILL_USE_CURL
выставлен в false.

Что сделано:
- avito_detail_backfill: reuse_context=True + request_context_reset() РОВНО один
  раз за прогон и только на AvitoBlockedError. Не на AvitoSidecarUnavailableError
  (подтип AvitoRateLimitedError — отказ нашего тракта, не бан площадки) и не на
  AvitoListingGoneError. Зеркалит #3212: сброс на каждый блок сам себя
  поддерживает — пропуск живёт в context'е, сброс его выбрасывает, повторная
  проверка с того же IP снова блокируется, одна осечка даёт каскад.
- fetch_detail: необязательные origin/browser_referer (имя referer уже занято под
  Referer curl-пути, это разные фетчеры и разные поля). Дефолт None → payload и
  поведение city_sweep/pipeline/admin не меняются.
- _serp_origin_for: городская SERP из URL карточки, хост берётся из самого url.

Попутно — дефект якорной вкладки сайдкара, найденный при переносе. _ensure_anchor_page
отдавала True на ЛЮБУЮ живую вкладку, не сверяя её с запрошенным origin. А origin у
обоих caller'ов выводится ИЗ URL карточки и меняется вместе с городом (Авито —
сегмент пути, Домклик — поддомен). После первой же карточки другого города Referer
называл выдачу, которую этот контекст никогда не открывал: ни куки её, ни тайминга,
площадка видит заявленный переход без единого следа. Ровно то, что #3258 запретил
делать фолбэкам якорного поиска. Добавлен _anchor_origins: origin сменился — вкладка
переоткрывается. Чинит и Домклик тоже.

Заход через поиск Яндекса (BROWSER_ANCHOR_VIA_SEARCH) для Авито НЕ включается —
расширять этот список без отдельного замера запрещает комментарий у самой константы.
Хранилище авторизованных сессий Авито (#3179/#3180) этой правкой не заменяется.

База для сравнения снята ДО выката и записана в #3251: доля блоков от попыток
46.9% / 74.2% / 96.4% / 69.8% / 52.9% / 62.3% по суткам 25-30.08. Сравнивать после
деплоя по доле блоков и обогащению за сутки, а НЕ по статусу прогона: ratio-критерий
обрывает КАЖДЫЙ прогон, и статус banned про площадку ничего не говорит.

Тесты: backend 5150 passed / 37 skipped, сайдкар 211 passed (было 206 + 5 новых на
переезд якоря), ruff чист.

Refs #3251, #3180, #3118, #3212, #3247, #3258
2026-08-30 11:47:15 +03:00
bot-backend
ff0a15d443 feat(tradein/browser): ходить по Домклику как человек — с Referer и с перезагрузкой зависшего рукопожатия
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 5m1s
Добор карточек Домклика упирался в отказ на 11-й карточке: прогон 5298 дал
attempted=13, enriched=10, blocked=3. Ручные прогоны в живом браузере брали 91 и
40 карточек без единого отказа. Разбор нашёл два отличия, и оба оказались нашими,
а не площадки.

1. Referer не отправлялся НИКОГДА. playwright'овский goto() по умолчанию этот
   заголовок не шлёт, а параметр `referer`, который он принимает, мы не передавали.
   Площадка видела десяток появлений подряд прямо на URL карточки, без источника
   перехода — так не ходит ни один человек. Комментарии в коде при этом уверяли
   про «органическую навигацию с реальным Referer»; они врали, теперь исправлены.
   Сайдкар принимает `referer` в теле /fetch и ставит его ТОЛЬКО на целевую
   навигацию; на origin и якорную вкладку не ставит — туда приходят «сами».
   Добор Домклика передаёт страницу выдачи, чем переход и является по смыслу.

2. Зависшее рукопожатие не перезагружалось. _wait_out_pow_challenge построен на
   допущении «страница перезагрузит себя сама после решения PoW»; ручная сессия
   29.08 через узел 10 это опровергла — выдача осталась на 401, и пропуск
   qrator_jsid2 выдался только после ДВУХ перезагрузок, сделанных руками:
   102.7с GET → 401, 115.1с GET → 401, 118.4с GET → 200, следом кука-пропуск, и
   карточка за 6 секунд. Пока мы только опрашивали content(), такая страница жила
   до таймаута, а бэкфилл засчитывал это в блоки. Теперь после
   BROWSER_CHALLENGE_RELOAD_AFTER_MS (8с) сайдкар перезагружает сам, не больше
   BROWSER_CHALLENGE_MAX_RELOADS (2) раз за фетч.

Оба пути безопасны на откат: без поля `referer` в теле поведение прежнее,
BROWSER_CHALLENGE_RELOAD_AFTER_MS=0 возвращает прежний опрос без навигаций,
упавшая перезагрузка не роняет фетч — опрос продолжается в том же бюджете.

Тесты: 191 passed в сайдкаре (было 182) — 4 на Referer, 5 на перезагрузку, в том
числе «страница ожила сама → лишней навигации нет» и «висит вечно → не больше
лимита». Точечные backend-тесты домклика и scraper_kit — 144 passed.
2026-08-29 22:53:11 +03:00
bot-backend
c5784e85bc fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m17s
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
#3237 научил сайдкар опознавать статический отказ Домклика самостоятельно —
это правильно и работает, но вместе с распознаванием ban-сигнал переехал не
туда. Отказ стал приезжать обычной 500-кой: browser_fetcher обнуляет на ней
last_response_status, и в detail.py срабатывает ветка except Exception,
которая по построению НЕ зовёт report_ban («не подтверждённый маркер-бан, а
сбой транспорта», #2600 п.4).

Итог на проде (прогон 5287): ban_kinds сменился с platform на unknown, и при
шести «статический отказ площадки» подряд в логах сайдкара в
scrape_proxy_source_bans не появилось НИ ОДНОЙ записи. Это не косметика
счётчиков — platform единственный диагноз, запускающий ротацию IP, поэтому мы
продолжали бы долбиться в отказавший узел вместо перехода на свободный.

Правка возвращает отказ на ban-путь, сохраняя разделение, ради которого
#3237 и делался:

- сайдкар кладёт в тело ошибки структурный признак ban_page и апстрим-статус.
  HTTP-код НЕ меняем: на 500 завязана classify_browser_probe;
- SidecarBanPageError — подкласс httpx.HTTPStatusError, поэтому ловля у
  прочих поставщиков и retry-политика fetch() не замечают нового типа;
- detail.py различает две ветки: подтверждённый отказ → report_ban + статус
  из исключения, транспортный сбой — как раньше.

Статус несём отдельным полем, а не через last_response_status: на error-пути
fetch() его обнуляет, а у Домклика отказ приходит с 401, без которого
классификатор ставит unknown. Подстрокой в тексте исключения признак искать
нельзя — _raise_for_sidecar_status обрезает тело до 300 символов, и
формулировка отказа менялась дважды за месяц.

Тесты держат обе ветки раздельно на всех трёх уровнях: сайдкар (признак есть
у бан-страницы, отсутствует у транспортной ошибки), фетчер (тип и
upstream_status, включая ловушку bool-как-int из #3196), detail.py
(report_ban зовётся / не зовётся, статус доезжает).

Closes #3239
2026-08-29 19:12:36 +03:00
bot-backend
26cd8dbe34 fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m52s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Обе ветки повтора в `fetch_detail` — 403/firewall и 429 — пересоздавали
эфемерную сессию вызовом `_build_detail_session()` БЕЗ `config`. Прокси
kit-версия читает только из `config.scraper_proxy_url`, поэтому такая сессия
уходила напрямую.

Замысел ветки прямо обратный, он записан в её же комментарии: «эфемерная
свежая сессия (новый CONNECT-туннель = свежий exit-IP)». Ветка вообще
исполняется только при backconnect=True, а он вычисляется как «задан
config.scraper_proxy_url» — то есть в момент вызова достоверно известно, что
прокси есть, и он терялся.

ПОЧЕМУ НЕ БЫЛО ВИДНО. Пока адрес самой машины не был заблокирован, прямой
повтор часто срабатывал, и подмена канала выглядела как успех. Замер 27.08 из
прод-контейнера, один и тот же URL Авито:

    через прокси — 200, 3.3 МБ страницы
    напрямую     — 429, «доступ ограничен», firewall

С этого момента каждый повтор после блока обречён. Обогащение
avito_detail_backfill по суткам: 21-25.08 — 178/129/147/138/111, 26.08 — 23,
27.08 — 0 при 25 блоках. Обвал начинается ровно с окна, в котором сменился
адрес машины.

Тот же класс ошибки чинили в #2330 для build_warmed_session; в пути повтора он
оставался.

Три теста: обе ветки на месте (страховка от проверки пустоты), ни одна не
строит сессию без config, и отдельно доказано, что без config прокси в сессии
действительно нет. Проверил красноту на старом коде — падает с точным текстом.
Прогон: 107 тестов scrapers зелёные, ruff чист.

Refs #3034, #3045
2026-08-27 15:33:29 +03:00
bot-backend
fe628a5e17 fix(tradein/scraper-kit): actualize avito fingerprint - impersonate/Sec-Fetch-Site/referer/cookie-check
Живой замер браузера 2026-08-21 разошёлся с тем, что шлёт прогретая сессия:

- impersonate="chrome120" был захардкожен в 9+ местах (avito/{serp,detail,imv,
  houses}.py, pipeline.py x4, cian/valuation.py, yandex/valuation.py) вместо
  единой DEFAULT_IMPERSONATE (providers/_base.py). Обновление до chrome146
  (макс. доступный профиль curl_cffi 0.15.0; alias "chrome" НЕ используется -
  едет сам при апгрейде библиотеки без ревью) теперь меняется в одном месте.
  Guard-тест backend/tests/test_impersonate_single_source.py грепает всё
  дерево scraper_kit на литерал "chrome120".

- DOCUMENT_HEADERS ставил Sec-Fetch-Site="none" на уровне сессии, а Referer
  добавлялся per-request (curl_cffi мёржит per-request headers поверх
  session-level) - живой Referer с "none" рядом не бывает у настоящего
  Chrome. Добавлен referer_headers() (Sec-Fetch-Site="cross-site") для
  caller'ов с чужедоменным Referer; avito warm-up (yandex/ya.ru -> avito)
  теперь его использует. Внутренний avito-search -> avito-detail Referer
  (fetch_detail, same-origin) НЕ тронут - отдельный явный комментарий почему.

- Referer прогрева заменён с yandex.ru на ya.ru (живой переход из выдачи
  даёт короткий домен). ysclid сознательно не добавлен - значение выпускает
  Яндекс, подделка хуже отсутствия.

- warm_up_session/research_in_session считали прогрев успешным по HTTP-
  статусу и отсутствию firewall-маркеров, не проверяя антибот-cookies
  (__zzatw-*/cfidsw-*, сняты с живого браузера) - detail-батч на такой
  "прогретой" сессии сжигал прокси на обречённых 403. Теперь поднимают
  AvitoWarmupCookiesMissingError (наследник AvitoBlockedError - существующие
  except-блоки/ban_kind/proxy-ротация в pipeline и backfill ловят без
  изменений).

Полный backend pytest suite зелёный (4672 passed), ruff check чист.

Refs #3034
2026-08-21 17:02:53 +03:00
bot-backend
ab5c4b86cd fix(tradein/avito): рейтинг дома перестаёт уезжать в адрес объявления (#2814)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m50s
CI Trade-In / frontend-checks (pull_request) Has been skipped
Авито с 27.07 рендерит рейтинг и число отзывов внутри того же <p> в
data-marker="item-location", откуда serp.py берёт адрес: «ул. Ткачей,17·5,0 · 4
отзыва». Прод 2026-08-10: 1 123 активных объявления с таким адресом, и у 1 123 из
1 123 нет координат — доля 100%, 711 из них геокодер уже пробовал. Контроль в тех
же данных: у чистых адресов координаты есть у 4 766 из 5 715 (83%).

Строка без geom молча выпадает из comp-пула: Tier W отбирает через ST_DWithin, а
NULL не проходит предикат и нигде не считается. 2 446 из 3 270 безкоординатных
попадают в свежий пул аналогов — 12.7% аналогов невидимы радиусному поиску.

Режем по «·», за которой идёт ЦИФРА (рейтинг «·4,9», счётчик «·2 отзыва»). По
любой «·» нельзя: разделитель района пишется «, 59 · р-н Академический» — за
точкой буква, и этот хвост _deglue_house_marker намеренно сохраняет (#1773).
Тест проверяет обе стороны плюс три прежних хвоста (CSS, метро, «от N мин.»).

Чинит только новые вставки: base.py пишет address = COALESCE(listings.address,
EXCLUDED.address), адрес при конфликте не перезаписывается осознанно (#2777).
Бэкфилл 1 123 существующих строк вынесен в #2814 для database-expert.
2026-08-10 14:32:25 +05:00
58dcd76d2e fix(tradein/domclick): sale_type перестаёт быть третьим словарём (#2674) (#2696)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m51s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m44s
2026-08-06 06:11:45 +00:00
d362b16d7c fix(tradein/proxy): доводить сигнал бана площадки до пула (#2600 п.1) (#2653)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m40s
2026-08-05 11:37:35 +00:00
1600a6be18 fix(tradein/domclick): единый список QRATOR-маркеров serp+detail (#2636) (#2645)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m38s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m54s
2026-08-05 07:06:41 +00:00
2cd24ecfbc fix(tradein/scrapers): clamp implausible listing/publish dates (LOW audit R2 #6) (#2507)
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
2026-07-12 22:11:06 +00:00
ced616d150 feat(tradein/domclick): cookie-injection MVP для обхода QRATOR-блока (#2000) (#2433)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 49s
Deploy Trade-In / build-browser (push) Successful in 2m13s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 7m16s
2026-07-04 17:49:40 +00:00
102bdbda47 fix(tradein/domclick): органический заход через SERP перед card-fetch (anti-QRATOR) (#2430)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 52s
Deploy Trade-In / build-browser (push) Successful in 2m16s
Deploy Trade-In / build-backend (push) Successful in 1m45s
Deploy Trade-In / deploy (push) Successful in 8m15s
2026-07-04 16:39:47 +00:00
bot-backend
27bbd579c3 chore(tradein/scrapers): удалить весь legacy scrapers/ каталог — final E (#2397, #2277)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 47s
Удаляет весь `app/services/scrapers/` (16 файлов, ~7100 строк) — Part D (D1-D5)
убрал всех внешних вызывающих, 0 runtime importers подтверждено grep'ом на main.

Заодно:
- удалены 7 осиротевших локальных probe/sweep-скриптов (tradein-mvp/scripts/),
  импортировавших уже-удалённые или удаляемые сейчас legacy-модули
- тест-хирургия по 65+ файлам: DELETE прямых legacy-юнит-тестов, RETARGET
  тестов, тестирующих ещё живую бизнес-логику (переключены на scraper_kit.*
  эквиваленты, включая quality-gate #781/#753/#754/#755/#773/#740), partial-delete
  golden-parity тестов, потерявших legacy-оракл
- kit save_listings/AvitoScraper/etc. требуют инжектируемые matcher/config —
  ретаргетированные тесты обновлены под новую сигнатуру (RealScraperConfig(),
  MagicMock HouseMatcher, region_code=66)

Полный pytest suite зелёный (2255 passed, 6 skipped) кроме известного флейка
#2208 (test_search_cache_hit, не связан со scrapers).
2026-07-04 15:58:15 +03:00
bot-backend
575c233e51 refactor(tradein/scrapers): удалить 6 осиротевших legacy scraper-модулей + тест-хирургия (#2397 Part E2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m2s
app.services.scrapers.{cian,cian_detail,yandex_realty,domclick,domclick_exceptions,
avito_houses} имели 0 runtime-импортёров после E1 (scrape_pipeline.py удалён,
#2405) — admin.py/cian_history_backfill.py уже на scraper_kit.providers.*.

Удаление:
- 6 модулей целиком (avito_houses.py, cian.py, cian_detail.py, domclick.py,
  domclick_exceptions.py, yandex_realty.py)
- 3 осиротевших dev-sweep скрипта (local-sweep-ekb{,-cian,-yandex}.py) —
  эксклюзивно драйвили удалённые легаси-скраперы, kit-путь их заменяет

Тесты (18 файлов DELETE — эксклюзивно тестировали удалённые модули или были
чистым legacy-vs-kit parity, чей легаси-эталон исчез):
test_cian_detail.py, test_cian_serp_scraper.py, tests/scrapers/test_cian_exhaustive.py,
test_yandex_realty_serp.py, tests/scrapers/test_domclick.py,
test_avito_houses_dedup.py, test_avito_houses_parse.py,
test_houses_rating_filter_entries_1789.py, test_scraper_kit_domclick_golden_parity.py,
tests/scrapers/test_admin_{avito,cian,yandex}_kit_parity.py.

CONVERT (файл покрывает СМЕШАННЫЙ live+legacy набор — вырезаны только
секции удалённых модулей, live-покрытие сохранено):
- test_scraper_kit_cian_golden_parity.py — SERP/`_format_address`/detail
  (cian.py/cian_detail.py) убраны; extract_state (cian_state_parser, live)/
  valuation (cian_valuation, live)/newbuilding (cian_newbuilding, live)/
  no-app-imports guard — остались
- test_scraper_kit_yandex_golden_parity.py — SERP `_entity_to_lot`/
  `_parse_gate_json`/`_build_url` (yandex_realty) убраны; normalize_house_type
  (live)/newbuilding parse (yandex_newbuilding, live)/no-app-imports guard —
  остались
- test_scraper_kit_bisection_parity.py — cian/domclick/yandex-ветки убраны;
  avito-ветка (live, единственная оставшаяся легаси-бисекция) — осталась
  полностью, включая full-seed-grid
- test_scraper_kit_group_c_backfill_kit_parity.py — cian fetch_detail
  browser_fetcher parity-тест убран; avito/house_imv_backfill/
  cian_history_backfill/ekb_geoportal тесты — остались
- test_scraper_kit_pricehistory_session_parity.py — fetch_detail
  config-injection/proxy-wiring тесты (cian_detail) убраны; extract_state
  (cian_state_parser, live)/ScrapedLot (base, live) parity — остались
- test_yandex_scrapers_delay_wiring.py — yandex_realty delay-тест убран
  (kit-эквивалент уже покрыт test_kit_serp_proxy_pool.py); yandex_newbuilding
  (live)/yandex_detail/yandex_valuation (kit) тесты — остались
- test_scraper_proxy.py — yandex_realty BrowserFetcher-тест убран (redundant
  с test_kit_serp_proxy_pool.py::test_yandex_serp_no_pool_by_default)

RETARGET на kit (символ живёт, только источник поменялся):
- test_domclick_sweep.py — `_map_item` → scraper_kit.providers.domclick.serp
- test_yandex_city_sweep.py — DEFAULT_PRICE_RANGES/ROOM_PATH →
  scraper_kit.providers.yandex.serp
- test_pipeline_browser_routing.py — fetch_house_catalog →
  scraper_kit.providers.avito.houses (+ KitAvitoBlockedError для firewall-теста)
- test_snapshot_writer.py — DetailEnrichment/save_detail_enrichment →
  scraper_kit.providers.cian.detail (byte-identical strangler-копия)
- test_backfill_wave2.py — DetailEnrichment → scraper_kit.providers.cian.detail
- tests/scrapers/test_avito_unix_date_tz_consistency.py — avito_houses →
  scraper_kit.providers.avito.houses (houses-ветка _unix_to_date/RUS_MONTHS
  consolidation-проверки)

Stale-комментарии поправлены в cian_price_history.py, tests/scrapers/
test_domclick_detail.py (описывали ещё-живой domclick.py/domclick_exceptions.py).

grep -rn scrapers\.\(cian\|cian_detail\|yandex_realty\|domclick\|domclick_exceptions\|
avito_houses\) app/ scripts/ packages/ — только historical footnotes
("strangler-копия app.services.scrapers.X", past-tense "удалён/deleted").

Backend suite: 2811 passed, 6 skipped, 1 failed (known flake test_search_cache_hit
#2208, не регрессия). ruff check чист на всех изменённых файлах.
2026-07-04 14:37:19 +03:00
84716e61de Merge pull request 'refactor(tradein/scrapers): удалить legacy scrape_pipeline.py + тест-хирургия (#2397 Part E1)' (#2405) from chore/tradein-partE1-delete-scrape-pipeline into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m37s
Deploy Trade-In / build-backend (push) Successful in 1m20s
Deploy Trade-In / deploy (push) Successful in 1m15s
2026-07-04 11:09:39 +00:00
bot-backend
79a77200d7 refactor(tradein/scrapers): удалить legacy scrape_pipeline.py + тест-хирургия (#2397 Part E1)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m43s
app.services.scrape_pipeline (3734 строк) имел 0 runtime-импортёров — Part C убрал
последнего вызывающего (admin.py, scheduler_main.py уже на
scraper_kit.orchestration.pipeline). Удаление:

- backend/app/services/scrape_pipeline.py — удалён целиком

Тесты (18 файлов, импортировавших scrape_pipeline):

DELETE (чистая legacy-оркестрация, kit-эквивалент покрыт в
test_scraper_kit_pipeline_parity{,2}.py):
- test_scrape_pipeline.py, test_scrape_pipeline_proxy_pool.py (proxy-pool DB-lease
  вытеснен RealProxyProvider DI-адаптером, покрытым test_scraper_adapters_contracts.py)
- test_anchor_watchdog.py, test_ban_rotation_1790.py, test_provider_rotation_1848.py
- test_scraper_resilience_1949_1950.py, test_sweep_drain.py

CONVERT (parity-сравнение убрано, kit-only regression оставлен/усилен):
- test_scraper_kit_pipeline_parity.py, test_scraper_kit_pipeline_parity2.py —
  _drive_old убран, _drive_new стал единственной веткой; ассёрты на конкретные
  значения counters сохранены (это теперь основное regression-покрытие kit
  orchestration)
- test_sweep_imv_phase.py — 6 IMV-phase тестов (enrich_imv true/false/no-touched/
  crash/failed-counter/cancel) переведены на run_avito_city_sweep (kit) с DI
  (config/matcher/enrichment); enrichment.process_houses_imv_batch — инжектируемый
  Protocol-метод вместо прямого импорта app.services.house_imv_backfill

PARTIAL-EDIT (легаси sweep-behavior вырезан, admin/kit-DI/несвязанные тесты
оставлены):
- test_city_sweep.py — только retarget 4 импортов на scraper_kit.orchestration.pipeline
- test_cian_city_sweep.py, test_yandex_city_sweep.py — sweep-behavior секции удалены,
  admin endpoint тесты (уже kit-DI) + anchor-timeout/watchdog формулы (retarget import)
  оставлены
- test_domclick_sweep.py — sweep-wiring тесты удалены, Counters + _map_item оставлены
- test_avito_newbuilding_sweep.py — sweep-wiring тест #5 удалён, retarget
  NewbuildingSweepCounters
- test_pipeline_browser_routing.py — 3 run_avito_pipeline теста удалены, browser-routing
  на уровне fetch_detail/fetch_house_catalog/AvitoScraper не тронут
- test_scraper_proxy.py, tests/scrapers/test_avito_anti_bot.py — _avito_proxies()
  переведён на kit (DI-параметр вместо settings-patch); pipeline-level тесты удалены

Stale-комментарии (scrape_pipeline.py как текущая реальность) поправлены в
admin.py, avito_detail_backfill.py, house_imv_backfill.py, yandex_price_history.py,
scrapers/avito_detail.py + kit-пакете (orchestration/__init__.py, orchestration
/pipeline.py, providers/avito/detail.py — strangler завершён, легаси удалён).

grep -rn scrape_pipeline app/ scripts/ packages/ — только past-tense упоминания
("удалён в Part E1", "перед его удалением") + один historical footnote
(contracts.py:138, provenance-заметка "собраны grep'ом по ... .py").

Backend suite: 3088 passed, 6 skipped (известный flake test_search_cache_hit #2208 —
не регрессия). ruff check чист на всех изменённых файлах.
2026-07-04 13:56:06 +03:00
2e345acb82 fix(tradein/scraper-kit): клампить diff_percent перед INSERT offer_price_history (#2404)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m38s
Deploy Trade-In / build-backend (push) Successful in 1m28s
Deploy Trade-In / deploy (push) Successful in 1m45s
2026-07-04 10:44:26 +00:00
bot-backend
a9b87aa311 chore(tradein/scraper-kit): удалить 5 orphaned legacy-модулей (#2277 финальный шаг, частичный)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m36s
Удалены (0 runtime importers в app/, scripts/, packages/; проверено grep'ом,
включая intra-legacy импорты в оставшихся 21 legacy-модуле scrapers/):
- backend/app/services/scrapers/avito_imv.py
- backend/app/services/scrapers/domclick_detail.py
- backend/app/services/scrapers/ekb_geoportal_client.py
- backend/app/services/scrapers/yandex_detail.py
- backend/app/services/scrapers/yandex_valuation.py

Тесты — удалены (pure legacy-vs-kit parity, kit-only coverage уже есть в другом
месте):
- tests/scrapers/test_ekb_geoportal_client_kit_parity.py
- tests/scrapers/test_domclick_detail_kit_parity.py

Тесты — конвертированы в kit-only (импорт легаси заменён на scraper_kit.*,
покрытие сохранено без потерь):
- tests/scrapers/test_avito_imv_kit_parity.py (parity-тесты убраны, config-footgun
  regression на kit-стороне оставлен)
- tests/scrapers/test_avito_imv_browser_transport.py
- tests/scrapers/test_yandex_valuation_kit_migration.py (parity убрана, footgun-
  регрессия mandatory config/delay_provider оставлена)
- tests/scrapers/test_domclick_detail.py (+ exceptions переведены на
  scraper_kit.domclick_exceptions, т.к. kit parse_detail_html поднимает их)
- tests/test_avito_imv_parse.py
- tests/test_ekb_geoportal_ingest.py (client-часть; ingest/geocoder-часть не
  трогали — не зависят от удалённого модуля)
- tests/test_yandex_detail.py
- tests/test_yandex_detail_structural.py
- tests/test_yandex_valuation.py (YandexValuationScraper() → config=_KIT_CONFIG,
  kit конструктор требует обязательный config)
- tests/test_yandex_valuation_save.py
- tests/test_extval_house_id_write_path.py (только yandex_valuation-часть;
  cian_valuation остался нетронутым — живой легаси-модуль)
- tests/test_yandex_history_area_filter.py

Тесты — частично отредактированы (убраны только части про удалённые модули,
живые легаси-модули/их parity не тронуты):
- tests/scrapers/test_admin_domclick_ingest_kit_parity.py (base.py/ScrapedLot
  parity остался)
- tests/scrapers/test_admin_yandex_kit_parity.py (yandex_realty parity остался)
- tests/scrapers/test_admin_avito_kit_parity.py (avito.py/avito_houses.py parity
  остался; _parse_price parity убран)
- tests/scrapers/test_avito_unix_date_tz_consistency.py (IMV-ветка переведена на
  kit avito.imv, легаси avito/avito_houses/avito_shared/avito_detail не тронуты)
- tests/tasks/test_yandex_detail_backfill.py (subject — kit-задача, уже вызывает
  scraper_kit.providers.yandex.detail; save_detail_enrichment coverage-тесты
  внизу файла переведены на kit)
- tests/test_scraper_kit_yandex_golden_parity.py (detail/valuation parity убраны;
  serp/newbuilding/house_type_normalizer/build_url для yandex_realty/
  yandex_newbuilding не тронуты)
- tests/test_scraper_proxy.py (YandexValuation proxy-тесты переведены на kit
  config= DI; test_avito_imv_own_session_receives_proxies удалён — дублирует
  test_avito_imv_kit_parity.py footgun-тесты)
- tests/test_yandex_scrapers_delay_wiring.py (yandex_detail/yandex_valuation →
  kit delay_provider=; yandex_realty/yandex_newbuilding не тронуты)
- tests/test_scraper_kit_group_c_backfill_kit_parity.py (docstring уточнение,
  без функциональных изменений)

Полный backend-suite зелёный (3253 passed, 6 skipped) кроме известного
pre-existing flake test_search_api.py::test_search_cache_hit (#2208, не
регрессия этого PR). ruff check + ruff format — чисто на всех изменённых файлах.
2026-07-04 10:41:46 +03:00
bot-backend
ecec9ff6d7 fix(tradein/scraper-kit): точность докстрингов про прод-путь build_warmed_session
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m40s
code-reviewer отметил: формулировка "PROD DEFAULT" в докстрингах могла ввести в
заблуждение — прод-путь сегодня всё ещё легаси (avito_detail_backfill.py
импортирует build_warmed_session из legacy-модуля), этот фикс — advance-prep,
не изменение живого прод-поведения.
2026-07-04 03:01:27 +03:00
bot-backend
fea054d24f fix(tradein/scraper-kit): проброс config= в avito build_warmed_session (#2330) 2026-07-04 02:55:44 +03:00
f872f5636b Merge pull request 'test(tradein/scraper-kit): prove yandex_valuation kit path safe for #2337 (#2336)' (#2345) from feat/tradein-migrate-yandex-valuation-kit into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m35s
Deploy Trade-In / build-backend (push) Successful in 28s
Deploy Trade-In / deploy (push) Successful in 46s
2026-07-03 23:04:13 +00:00
bot-backend
7546868535 test(tradein/scraper-kit): prove yandex_valuation kit-wiring safety (#2336)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 17s
CI / changes (pull_request) Successful in 16s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m50s
Group E3 recon confirmed `scraper_kit.providers.yandex.valuation` is already a
strangler-copy (#2133) and already used via admin.py's debug route (#2305,
Group A) with the established config=RealScraperConfig()/delay_provider=
get_scraper_delay convention. estimator.py (the real caller, #2337's scope)
still imports the legacy module directly with no arguments -- not touched here.

Adds regression coverage for the two confirmed footguns before #2337 switches
that call site:
- mandatory `config: ScraperConfig` raises TypeError when omitted (same
  discipline as the cian_valuation footgun, #2335)
- optional `delay_provider` silently falls back to hardcoded 5.0s when not
  wired, discarding admin-tuned anti-ban throttling (scraper_settings.
  get_scraper_delay)

Also extends the existing `.parse()` golden-parity test (test_scraper_kit_
yandex_golden_parity.py, SimpleNamespace config) with an assert_parity-based
proof over the REAL production construction path (RealScraperConfig() +
get_scraper_delay), plus a harness-catches-divergence sanity check.

No production code changed -- app/services/scrapers/yandex_valuation.py and
scraper_kit/providers/yandex/valuation.py are both pre-existing.
2026-07-04 01:50:55 +03:00
bot-backend
7049717782 test(tradein/scraper-kit): golden-parity + config-footgun proof for avito_imv (#2334)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 11s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m56s
Group E1 of the scraper_kit migration epic (#2277 -> #2308 -> #2334): prepares the
kit side of app/services/scrapers/avito_imv.py -> scraper_kit.providers.avito.imv
WITHOUT switching any real call site (estimator.py is #2337, a separate final step;
house_imv_backfill.py is deliberately deferred too, see report).

- Golden-parity: full evaluate_via_imv() async flow (geocode A/B + evaluate C) proven
  byte-identical between legacy and kit on REAL live-captured Avito API responses
  (tests/fixtures/avito_imv_geo_position.json + avito_imv_getdata.json), routed through
  the browser_fetcher transport to stay offline/deterministic. Previously this function
  had zero parity coverage (only _parse_price was covered, via admin.py Group A).
  Added cheap pure-function parity for compute_imv_cache_key/_parse_geo_position/
  _parse_placement_history/_parse_suggestions too.
- Config-footgun regression: kit evaluate_via_imv only reads config.scraper_proxy_url
  when config= is explicitly passed; called bare it silently builds a proxy-less
  curl_cffi session even with a real proxy configured. Two tests lock this down
  (without config= -> proxies=None, with config=RealScraperConfig() -> proxies wired) —
  manually verified by breaking scraper_kit/providers/avito/imv.py's config threading,
  watching the "with config" test fail on the exact proxies assertion, then reverting.

No production import switched in this commit — see PR/issue report for the full
caller audit and the house_imv_backfill.py deferral rationale.
2026-07-04 01:49:21 +03:00
bot-backend
ea24289f47 feat(tradein/scraper-kit): migrate admin.py debug routes + domclick ingest to kit (#2305)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m36s
Group A of the scraper_kit migration epic (#2277). Switches admin.py's manual
"run parser" debug endpoints and scripts/ingest_domclick_jsonl.py from direct
app.services.scrapers.* imports to their scraper_kit.providers.* equivalents,
using the DI adapters (RealScraperConfig/RealMatcherAdapter/RealProxyProvider,
app.services.scraper_settings.get_scraper_delay) already established by
app.scheduler_main._run_kit_scheduler.

Migrated: /scrape (AvitoScraper/CianScraper/YandexRealtyScraper + save_listings),
scrape_avito_house, scrape_avito_detail, scrape_avito_imv, scrape_yandex_detail,
scrape_yandex_valuation, scrape_cian_detail, cian_auto_login's BrowserFetcher.
scripts/ingest_domclick_jsonl.py: ScrapedLot/save_listings + (now that #2307/
Group D ported a kit equivalent while this was in flight) DomClickDetailEnrichment/
save_detail_enrichment.

Deliberately NOT migrated (documented in admin.py): scrape_yandex_newbuilding /
scrape_cian_newbuilding — their kit equivalents
(providers/{yandex,cian}/newbuilding.py) construct an internal BrowserFetcher(...)
without the mandatory `endpoint` kwarg, so any call crashes/silently-fails
regardless of caller-side DI. Bug lives in scraper_kit provider code, out of
scope here (only consuming, not touching provider logic) — flagged as follow-up.

Parity proven via tests/support/parity.py (assert_parity) against the exact
functions each debug route now calls, on offline fixtures — no live network/DB.

Refs #2305
2026-07-04 00:27:01 +03:00
bot-backend
8c8289cdad feat(tradein/scraper-kit): port domclick_detail + ekb_geoportal_client to kit (#2307)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m44s
Group D of the scraper_kit migration epic (#2277). Both legacy modules had no
kit-side equivalent yet (per audit Scraper_Kit_Legacy_Dependency_Audit_0703),
unlike the other 24/26 files which were already strangler-duplicated.

- scraper_kit/providers/domclick/detail.py — Layer B detail-card enrichment
  (parse_detail_html/fetch_detail/save_detail_enrichment), no ScraperConfig
  injection needed (fetch_detail takes an already-open browser_fetcher).
- scraper_kit/providers/ekb_geoportal/{__init__,client}.py — new subpackage,
  first non-listing-site provider in scraper_kit. Legacy module had zero
  app.* coupling (only httpx), so the port is near-verbatim.

Parity proven via tests/support/parity.py (assert_parity) against the exact
legacy functions on realistic fixtures (live-card SSR JSON, GeoJSON
FeatureCollection).

Callers of both legacy modules are unchanged (out of scope — ekb_geoportal_ingest
switch is #2310; the only other domclick_detail caller is
scripts/ingest_domclick_jsonl.py, also untouched).
2026-07-04 00:05:07 +03:00
bot-backend
d95c9e6e55 fix(tradein/tests): harden parity harness — bool-type mismatch detection + divergence-catch proof (#2304)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m34s
Code-reviewer follow-up on the parity harness (aaaf8179):

- parity.py: plain `==` in the scalar-equality branch let `True == 1` /
  `False == 0` silently pass (Python bool is an int subclass). A future
  migration bug turning a `bool | None` field into a raw 0/1 would slip
  through undetected. Now any type mismatch where exactly one side is a
  bool is reported as a diff, regardless of numeric equality.
- test_parity.py: unit test for the new bool-vs-int branch.
- test_avito_detail_kit_parity.py: the existing smoke test only proved the
  harness reports "no diff" on two genuinely-identical real dataclass
  instances — it never proved the harness catches a real divergence on
  this same 30+-field shape (only the toy fixtures in test_parity.py did).
  Added a test that mutates `price_rub` via dataclasses.replace() on the
  real kit DetailEnrichment and asserts assert_parity raises
  ParityMismatchError naming that field.
2026-07-03 23:45:58 +03:00
bot-backend
57963e3a06 test(tradein): reusable legacy/scraper_kit parity harness (#2304)
Stage 0 of the scraper_kit migration epic (#2277): shared test tool for
issues #2305-#2310, which each need to prove their kit-path importer
produces the same output as the legacy path on the same input.

- tests/support/parity.py: assert_parity()/compare_outputs() normalize
  dataclass/pydantic outputs to dict/list/scalar before comparing, since
  legacy vs kit dataclasses (e.g. DetailEnrichment) are different classes
  and dataclass __eq__ always returns False across classes even when all
  field values match. Supports ignore_fields (drop non-deterministic
  fields like latency_ms/fetched_at) and numeric tolerance (math.isclose)
  for float fields, with an assertion listing every differing field
  (path + legacy value + kit value) on mismatch.
- tests/support/test_parity.py: unit tests for the harness itself
  (identical outputs pass, differing outputs raise with informative
  diff, tolerance/ignore_fields options, cross-class dataclass parity).
- tests/scrapers/test_avito_detail_kit_parity.py: end-to-end smoke proof
  against real code — app.services.scrapers.avito_detail.parse_detail_html
  (legacy, reached via admin.py's scrape_avito_detail debug endpoint
  through fetch_detail) vs scraper_kit.providers.avito.detail's copy, on
  a fixed HTML fixture.
- tests/support/README.md: usage note for #2305-#2310 migration PRs.

Found while implementing: tests/test_scraper_kit_*_parity.py (9 files,
~3400 lines) already do ad-hoc `dataclasses.asdict(old) == asdict(new)`
parity checks for the already-migrated SERP scraper modules (avito/cian/
domclick/yandex/base/scheduler/pipeline) — this harness generalizes that
repeated pattern for the remaining 12 non-scraper importers, adding
ignore_fields/tolerance which those ad-hoc checks don't have.
2026-07-03 23:45:58 +03:00
32d1818fd4 fix(scrapers/avito): unify _unix_to_date timezone across serp/houses/imv (#2129)
Some checks failed
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m32s
Deploy Trade-In / build-backend (push) Successful in 59s
Deploy Trade-In / deploy (push) Has been cancelled
2026-07-02 14:03:11 +00:00
69845ddcee fix(tradein/avito-detail): живой прогрев сессии + батч-на-сессию — снимает 403 на curl-backfill (#1551)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m27s
Deploy Trade-In / build-backend (push) Successful in 51s
Deploy Trade-In / deploy (push) Successful in 1m37s
2026-06-28 22:34:38 +00:00
a50af40ad6 fix(tradein/avito): reap dead 404 listings in detail backfill (don't trip block breaker)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
avito_detail_backfill (mode=browser) aborted prod run 458 with
attempted=5 enriched=0 blocked=5: the lat-null pending queue is dominated
by DEAD listings (404 — that's why they are stale coord-holes). In browser
mode a dead listing's 404 page ("Ошибка 404. Страница не найдена") has no
item-view, so _is_detail_soft_block returned True → AvitoBlockedError →
backfill counted it as `blocked` and the consecutive-block breaker aborted
the run before reaching live listings.

- New AvitoListingGoneError (subclass of common AvitoError, NOT of
  AvitoBlockedError/AvitoRateLimitedError) so the backfill block-trap
  does not catch it.
- New _is_detail_not_found() detector + browser-mode branch in fetch_detail,
  checked BEFORE firewall/soft-block (keyed on the 404 title so it does not
  swallow the generic soft-block deflect). Curl path unchanged (404 → 302/
  non-200 → ValueError → failed, as before).
- Backfill: new `gone` counter; dedicated except AvitoListingGoneError branch
  that is neutral to the consecutive-block breaker and marks the listing
  is_active=FALSE (SAVEPOINT) so it leaves the snapshot scope.

Tests: unit for _is_detail_not_found (404 True; soft-block/item-view/empty
False), browser-mode fetch_detail raising AvitoListingGoneError (not Blocked),
and backfill gone-row marks inactive without aborting the breaker.
2026-06-28 14:05:31 +03:00
ca4637e5c8 fix(tradein/avito): detect HTTP-200 soft-block → rotate + coords-priority backfill ordering (#1950, #1967)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m26s
Deploy Trade-In / build-backend (push) Successful in 52s
Deploy Trade-In / deploy (push) Successful in 48s
Soft-block (HTTP 200 generic витрина без item-view) теперь детектится как блок → существующая 403/firewall reconnect-машинерия → ротация exit-IP вместо silent failed (blocked=0, enriched→0 каскад). + snapshot ORDER BY (lat IS NULL) DESC для приоритизации coord-holes (#1967, in-scope 321). Tests: test_avito_detail_soft_block.py. Full tradein suite 2591 passed.
2026-06-28 08:06:12 +00:00
43b392b809 feat(tradein/domclick): local-runner + ingest tooling → main (split from parked Layer-B)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Выделяет МЕРДЖАБЕЛЬНУЮ часть DomClick-тулинга из запаркованного PR #2005
(BLOCKED #2000 — прод Layer-B скрейпер через прокси режет QRATOR-403):

- scripts/domclick_local_runner.py — ручной offline-сборщик с домашнего IP:
  SERP-enumerate (фронт, BFF-JSON режет QRATOR) + card __SSR_STATE__ +
  offer-card v3 (AVM price_prediction + sold_similar). --enum-cache (enumerate
  1 раз → файл с sentinel → следующий запуск грузит кэш и пропускает enumerate),
  wait_for_selector (SERP-карточки догидрируются), resume по id. Self-contained.
- scripts/ingest_domclick_jsonl.py — JSONL → trade-in БД (save_listings +
  house-match в SAVEPOINT + save_detail_enrichment, идемпотентно, --limit/--dry-run).
- app/services/scrapers/domclick_detail.py + domclick_exceptions.py — card-SSR
  parse + save_detail_enrichment (нужны ингесту). Импортятся ТОЛЬКО ингестом
  (operator-run); прод-runtime их не дёргает (dormant в образе).
- tests/scrapers/test_domclick_detail.py + обновлённый scripts/README.md.

НЕ включает прод Layer-B (tasks/domclick_detail_backfill.py, scheduler-расписание,
data/sql/139_*) — остаётся запаркован на PR #2005 до чистого прокси (#2000).
БД-схема не меняется (detail-колонки уже в проде, миграции нет).

Live-validated: раннер качает ЕКБ-вторичку прямо сейчас (600+ карт, 0 блоков),
ингест залил их в прод-БД (house_matched, идемпотентно).
2026-06-28 10:26:52 +03:00
54a5d13c48 feat(tradein/domclick): enrich Layer A SERP parser — description, repair-inference, minhash, seller
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Capture extra bff-search fields in _map_item (verified live):
- description → ScrapedLot.description + infer_repair_state_from_text → repair_state
  (DomClick search has no renovation-enum; text inference like cian #622)
- sha1 description_minhash (cross-source dedup)
- agency_name from seller (defensive str|dict)
- raw_payload: is_sber_collateral, has_discount, discount_value, duplicates_offer_count, seller

Additive only; existing Layer A behavior unchanged. Deep card detail (renovation-enum,
wallType, full price-history, owners_count) is card-only = QRATOR-blocked on prod proxy
→ separate effort (needs dedicated residential IP).
2026-06-27 16:35:25 +03:00
52fc920507 fix(tradein/domclick): rewrite dead scraper to BFF JSON API + honest status (#1846, #1968)
Old HTML scraper dead: /search?city_id retired in DomClick redesign + VPS IP
QRATOR-banned (prod domclick_city_sweep reported done/0 every run). New path:

- GET bff-search-web.domclick.ru/api/offers/v1 + count/v1 via
  BrowserFetcher(source="domclick") -> generic provider -> shared mobile proxy
  -> bypasses QRATOR (prod-verified live: 20 items returned, no block).
- Sweeps 6 room buckets (st/1/2/3/4/5+); recursive binary price-split for
  oversized buckets (rooms=2 ~2215 > offset cap 2000); count/v1 sizes buckets.
- Maps SERP JSON -> ScrapedLot with lat/lon at ~100% (closes avito 54% NULL-lat
  gap); per-card EKB geo-guard.
- Honest status (#1968): QRATOR block OR fetch errors with 0 lots -> mark_failed,
  not mark_done. Parse failures are per-page resilient (break bucket, keep partial
  lots) so a transient bad page never aborts the whole sweep.
- Schedule shipped DORMANT (enabled=false, pages=100) via
  138_domclick_bff_rewrite_schedule.sql; operator enables after prod smoke.

42 scraper tests (price-split boundaries, parse-error resilience, honest status);
full backend suite green; ruff clean. Layer B (detail renovation/wallType/
priceHistory) deferred to #1846 follow-up.
2026-06-27 13:57:07 +03:00
e03c810306 feat(tradein-imv): route IMV backfill через BrowserFetcher.fetch_json за dormant-флагом (#915 Stage 3) (#1916)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 27s
Deploy Trade-In / test (push) Successful in 1m23s
Deploy Trade-In / build-backend (push) Successful in 51s
Deploy Trade-In / deploy (push) Successful in 46s
2026-06-26 09:54:28 +00:00
d479edeb3f feat(tradein-browser): additive POST /fetch-json + BrowserFetcher.fetch_json (#915 Stage 2) (#1915)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m25s
Deploy Trade-In / build-backend (push) Successful in 52s
Deploy Trade-In / build-browser (push) Successful in 2m53s
Deploy Trade-In / deploy (push) Successful in 1m30s
2026-06-26 09:30:35 +00:00
cd91d5e279 fix(scrapers): avito pipeline — новостройки house graceful (без housePage), AvitoBlockedError per-item не крашит прогон (#1820)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
2026-06-20 09:53:46 +03:00
37fc50ad08 fix(scrapers): avito_detail_backfill — снапшот ЕКБ+active, detail browser-only без curl-fallback на бан (#1814)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
2026-06-19 21:58:35 +03:00
b1fb0c5711 feat(scrapers): cian SERP на BrowserFetcher (camoufox), снят code-side rotate (#1806)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
2026-06-19 20:41:44 +03:00
d65a88f704 fix(avito): reconnect-retry on detail 429 under backconnect proxy
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Detail backfill (run 217) hit 5 consecutive HTTP 429s and aborted at 19 enriched
because the detail fetcher only had a same-session 429 short-retry (no reconnect).
429s are persistent: keep-alive reuses one backconnect exit IP across sequential
detail fetches -> avito rate-limits it. Mirror the merged SERP 429 fix (#1769):
widen short-retry 4->8 and add up to 3 ephemeral-session reconnects (fresh exit IP)
on 429 exhaustion before raising. Caller session never touched (as with 403 path).
2026-06-18 23:29:47 +03:00
bee44ffd3d fix(avito): tolerate 429 per-bucket instead of banning whole sweep
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Run 216 recovered 403s (merged fix) but still banned on a 429 at page 36
(deep pagination, backconnect conn-limit). Widen the 429 retry budget + one
reconnect on exhaustion in _fetch_serp_html, and make fetch_all_secondary skip
a blocked bucket and continue rather than aborting the run; only N consecutive
blocked buckets (real hard ban) re-raises. Preserves hard-ban detection.
2026-06-18 23:03:28 +03:00
c993243bc7 fix(avito): reconnect-retry on detail 403/firewall under backconnect proxy
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fetch_detail raised on the first 403/429, aborting the detail backfill (run 191
enriched only 48 then zombied) -> avito detail coverage stuck at 14% of active.
Mirror the merged SERP fix (#1765): on 403/firewall under the backconnect proxy,
rebuild an ephemeral session (fresh exit IP) and retry up to N before raising;
short-retry 429. Never close/rebuild the caller-provided shared session. Also
add the missing HTTP-200 firewall-interstitial check on the curl detail path.
2026-06-18 22:12:03 +03:00
bdedac3012 feat(avito): incremental SERP pagination with date early-stop (param-gated, default off)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Adds since=date mode to fetch_all_secondary: per room x seed-bracket, paginate
sequentially newest-first and stop once cards fall below the watermark, skipping
deep pagination (and the 403 clusters it triggers). since=None keeps the exact
exhaustive bisection walk. No scheduler/schedule change here — wiring follows
in a separate PR.
2026-06-18 22:01:46 +03:00