[BLOCKED #2000] feat(tradein/domclick): Layer B detail-backfill + local runner + JSONL ingest #2005

Closed
lekss361 wants to merge 8 commits from feat/tradein-domclick-detail-backfill into main
Owner

Что

scripts/ingest_domclick_jsonl.py — ингест JSONL, который генерит локальный DomClick-раннер (scripts/domclick_local_runner.py, домашний IP + new-headless), в БД tradein. Запускается ВНУТРИ tradein-контейнера (импортит app). Эта ветка также несёт запаркованный Layer B (domclick_detail.py, tasks/domclick_detail_backfill.py, миграция 139 — всё DORMANT).

Логика ингеста

Каждая ok-запись (status=="ok" + валидный price_rub) →

  1. ScrapedLotsave_listings() — upsert по dedup_hash + house-match хук в SAVEPOINT (fault-tolerant: если дом не матчится, листинг всё равно вставляется) + snapshot.
  2. listing.id достаётся отдельным SELECT ... WHERE source='domklik' AND source_id = ANY(:ids) (psycopg v3 list-bind).
  3. DomClickDetailEnrichmentsave_detail_enrichment() — deep detail (owners_count, encumbrances_clean, kitchen, views, wall/floor в raw_payload, repair_type) + offer_price_history (ON CONFLICT idempotent, source='domklik').

--limit N / --dry-run. Per-record try/except в enrichment-loop — одна битая запись не роняет хвост (важно для full-прогона на тысячах). Идемпотентно (ON CONFLICT / COALESCE).

Тест (живьём, tradein-backend, 30 тест-записей)

save_listings: inserted=0 updated=30 matched=30 match_failures=0
ИТОГО: house_matched=30 match_failed=0 detail_applied=30
       price_history_attempted=75 enrich_failed=0
  • house_id_fk: 0 → 30/30 залинковано (до этого raw-insert был без house-match)
  • offer_price_history: 75 → 75 (повторный прогон без дублей — идемпотентно)
  • detail-колонки (owners/encumbrances/views/wall_type) заполнены

ruff-100 clean, pre-commit passed.

⚠️ DO NOT MERGE (blocked)

Глубокая карточка заблокирована QRATOR на прод-прокси — issue #2000. Не мержить до чистого прокси.

Перед merge также: свериться с решением #1980 (drop dead domklik из honest source-list) — реактивация domklik-данных не должна воскресить «мёртвый» источник в UI.

Сам ингест-скрипт от прокси не зависит (грузит JSONL, который юзер делает локально), но он на этой ветке вместе с остальным parked Layer-B.

## Что `scripts/ingest_domclick_jsonl.py` — ингест JSONL, который генерит локальный DomClick-раннер (`scripts/domclick_local_runner.py`, домашний IP + new-headless), в БД tradein. Запускается ВНУТРИ tradein-контейнера (импортит `app`). Эта ветка также несёт запаркованный Layer B (`domclick_detail.py`, `tasks/domclick_detail_backfill.py`, миграция 139 — всё DORMANT). ## Логика ингеста Каждая `ok`-запись (status=="ok" + валидный price_rub) → 1. `ScrapedLot` → `save_listings()` — upsert по `dedup_hash` + **house-match хук в SAVEPOINT** (fault-tolerant: если дом не матчится, листинг **всё равно** вставляется) + snapshot. 2. listing.id достаётся отдельным `SELECT ... WHERE source='domklik' AND source_id = ANY(:ids)` (psycopg v3 list-bind). 3. `DomClickDetailEnrichment` → `save_detail_enrichment()` — deep detail (owners_count, encumbrances_clean, kitchen, views, wall/floor в `raw_payload`, repair_type) + `offer_price_history` (ON CONFLICT idempotent, source='domklik'). `--limit N` / `--dry-run`. Per-record try/except в enrichment-loop — одна битая запись не роняет хвост (важно для full-прогона на тысячах). Идемпотентно (ON CONFLICT / COALESCE). ## Тест (живьём, tradein-backend, 30 тест-записей) ``` save_listings: inserted=0 updated=30 matched=30 match_failures=0 ИТОГО: house_matched=30 match_failed=0 detail_applied=30 price_history_attempted=75 enrich_failed=0 ``` - `house_id_fk`: 0 → **30/30** залинковано (до этого raw-insert был без house-match) - `offer_price_history`: 75 → **75** (повторный прогон без дублей — идемпотентно) - detail-колонки (owners/encumbrances/views/wall_type) заполнены ruff-100 clean, pre-commit passed. ## ⚠️ DO NOT MERGE (blocked) Глубокая карточка заблокирована QRATOR на прод-прокси — **issue #2000**. Не мержить до чистого прокси. Перед merge также: свериться с решением #1980 (drop dead `domklik` из honest source-list) — реактивация `domklik`-данных не должна воскресить «мёртвый» источник в UI. Сам ингест-скрипт от прокси не зависит (грузит JSONL, который юзер делает локально), но он на этой ветке вместе с остальным parked Layer-B.
lekss361 added 4 commits 2026-06-27 16:15:01 +00:00
ПАРКОВКА (не мержить до прокси): код готов — parser на реальных __SSR_STATE__ путях
(productCard.objectInfo/priceInfo.priceHistory/egrnData/houseInfo.info + pricePrediction AVM),
orchestrator зеркало avito_detail_backfill, schedule 139 enabled=false. Деплоить нельзя:
DomClick card/offer-card за QRATOR reputation-403 на прод mobile-прокси (нужен dedicated
residential RU IP). Детали/аудит транспорта — Forgejo issue.
Self-contained script (playwright chromium, NO app imports) для сбора DomClick EKB
detail с домашнего residential IP (обходит QRATOR — verified live: 2 карточки ok,
repair/areas/priceHistory/egrn/owners собраны). Enumerate via bff (Layer A) + card
__SSR_STATE__ parse (Layer B) → JSONL (resume/idempotent).

KEY: дифференциатор — РЕЖИМ HEADLESS, не только IP. С того же IP старый headless=True
→ 403, new-headless → 200 + SSR. Раннер дефолтит на new-headless.

Пейсинг: рандом 12-30с между карточками, render-jitter 6-10с, human-break каждые
25-40 (60-180с), 1 retry на 403. ~100-150 карточек/час, full ~6400 ≈ 45-65ч.
JSONL ингестится в tradein DB отдельным шагом. НЕ deploy — локальный ops-инструмент.
feat(tradein/domclick): JSONL ingest script for local detail-backfill runner
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
f7426dc12b
scripts/ingest_domclick_jsonl.py — idempotent ingest of domclick_local_runner
output: filter ok+price, upsert via save_listings (fault-tolerant house-match
hook in SAVEPOINT — non-matched listings still persist), deep-detail enrichment
via save_detail_enrichment (owners/encumbrances/kitchen/views/wall/floor/
price-history). --limit / --dry-run. psycopg v3 ANY(:ids) list-bind.
Per-record try/except in enrichment loop so one bad row never aborts the tail.

Live-tested in tradein-backend on 30 records: updated=30, house_matched=30,
detail_applied=30, price_history idempotent (75->75), enrich_failed=0.
bot-backend added 1 commit 2026-06-27 17:03:17 +00:00
feat(tradein/domclick-local): SERP-enumerate (QRATOR-обход) + offer-card v3 extras
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
9b1f064069
(а) enumerate: прямой bff-search offers/v1+count/v1 режет QRATOR-403 с home-IP →
переведён на навигацию по фронтовому SERP (page.goto, тот же чистый транспорт что
card-detail): id из DOM (a[href*=/card/]), total из <title>, пагинация offset=20,
price-bisection при total>2000 ("5+"→5%2B через urlencode). Layer-A
(price/area/rooms/floor/total_floors/lat/lon/address) теперь из card-SSR
(productCard.priceInfo/objectInfo + address.position + houseInfo.info), т.к.
enumerate отдаёт только id+url. Удалены мёртвые BFF-функции. Схема JSONL не изменилась.

(б) detail: дозабор с offer-card v3 (200 с home-IP, в отличие от прод-прокси):
price_prediction (AVM заполненный + repair_quality + аренда, всегда) и sold_similar
(опц., --with-sold-similar/--sold-limit) через page.evaluate из QRATOR-сессии карточки.
XHR-AVM переопределяет пустой SSR pricePrediction; sold_similar в raw_extra.

Live-tested: enum-only собрал ids без QRATOR-аборта; full --limit 2 → price_rub
6.1M/3.1M, area/floor/coords/address из card-SSR, AVM market_price 4.99M/3.01M,
sold_similar.total≈1691. ruff clean.
bot-backend added 2 commits 2026-06-27 19:40:19 +00:00
Карточки SERP появляются клиентской гидрацией через ~6-12с после domcontentloaded.
Фиксированный render-sleep ловил пустой DOM при медленном ответе → enumerate молча
собирал 0 ids (silent 0-coverage; всплыло при ускоренном профиле render 3-5).
Теперь fetch_serp_page ждёт 'a[href*=/card/]' явно (timeout 12с) → пустой ценовой
band корректно даёт 0 по таймауту, render_wait остаётся коротким settle. Подтверждено
enumerate-only --limit 5 с render 3-5.
fix(tradein/domclick-local): resume — убрать преждевременный added==0 break в _paginate
All checks were successful
CI / changes (pull_request) Successful in 14s
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
0f7f5ea0ab
При resume первые страницы каждого бакета = топ-qi офферы, уже лежащие в done →
_add_offers возвращал added=0 → _paginate делал break → enumerate собирал 0 новых
(st=0 при resume; на первом прогоне done пуст, не проявлялось). Терминаторы пагинации
— только пустая страница и offset>=bound. Подтверждено: resume со 114 done → st собрал
546 новых студий (660-114).
bot-backend added 1 commit 2026-06-27 20:37:38 +00:00
feat(tradein/domclick-local): --enum-cache — enumerate один раз, resume detail поверх
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Has been skipped
5f994cc300
enumerate (~6422 id через SERP, ~1.5-2ч) теперь пишет полный список id в файл-кэш
(--enum-cache PATH) с sentinel завершённости; следующий запуск грузит кэш и полностью
пропускает enumerate → сразу detail. detail-resume по done (JSONL) композится поверх.
Кэш пишется только после реально завершённого свода (прерванный enumerate не кэшируется).
Тест: run1 записал 5 id+sentinel, run2 загрузил и пропустил SERP.
Author
Owner

Закрываю PR без merge — проверка 2026-07-02:

  1. Вся прокси-независимая ценность уже в main коммитом 43b392b8 «local-runner + ingest tooling → main (split from parked Layer-B)»: domclick_local_runner.py, ingest_domclick_jsonl.py, domclick_detail.py (парсер) + tests/scrapers/test_domclick_detail.py. Diff ветки против main по этим файлам = 0 строк.
  2. Оставшееся уникальное в ветке — прод-оркестратор (tasks/domclick_detail_backfill.py, wiring в scheduler.py, seed 139_...sql) — заблокировано открытым #2000 (нет чистого residential-прокси) и как merge-vehicle этот PR уже мёртв: mergeable=false, слот миграции 139 на main занят (139_premium_houses.sql), в scrapers-core идёт strangler-рефактор scraper-kit (#2132).
  3. Ветка feat/tradein-domclick-detail-backfill НЕ удаляется — это парковка Layer-B кода, на неё ссылается #2000 (commits eef23071 / 480ecc90 / 5f994cc3). При разблокировке #2000 резать свежий PR от актуального main: перенумеровать миграцию, переложить оркестратор на scraper-kit core.
Закрываю PR без merge — проверка 2026-07-02: 1. **Вся прокси-независимая ценность уже в main** коммитом 43b392b8 «local-runner + ingest tooling → main (split from parked Layer-B)»: `domclick_local_runner.py`, `ingest_domclick_jsonl.py`, `domclick_detail.py` (парсер) + `tests/scrapers/test_domclick_detail.py`. Diff ветки против main по этим файлам = 0 строк. 2. **Оставшееся уникальное** в ветке — прод-оркестратор (`tasks/domclick_detail_backfill.py`, wiring в scheduler.py, seed `139_...sql`) — заблокировано открытым #2000 (нет чистого residential-прокси) и как merge-vehicle этот PR уже мёртв: mergeable=false, слот миграции 139 на main занят (`139_premium_houses.sql`), в scrapers-core идёт strangler-рефактор scraper-kit (#2132). 3. **Ветка `feat/tradein-domclick-detail-backfill` НЕ удаляется** — это парковка Layer-B кода, на неё ссылается #2000 (commits eef23071 / 480ecc90 / 5f994cc3). При разблокировке #2000 резать свежий PR от актуального main: перенумеровать миграцию, переложить оркестратор на scraper-kit core.
lekss361 closed this pull request 2026-07-02 16:07:33 +00:00
All checks were successful
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Has been skipped

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2005
No description provided.