Prod prerequisites identified in deep review of the scheduler automation:
- data/sql/122: GRANT SELECT ON rosreestr_deals TO tradein_fdw_reader.
scheduler import reads gendesign.rosreestr_deals via FDW, but the reader
role was REVOKE ALL (100_tradein_fdw_role.sql) with no grant → import
would fail "permission denied for table rosreestr_deals". Single parent
grant suffices for declarative partitioning.
- tradein 072: seed next_run_at = next night-window (tomorrow + window_start
hour, pinned UTC) instead of NULL. NULL made get_due_schedules() fire both
rows within ~1 tick of deploy — outside the night window and (for cian)
before the FDW grant lands. Stays enabled; just shifts first fire to night.
Оба источника данных до этого требовали ручного запуска (HTTP-запрос в admin UI
и bash-скрипт на хосте соответственно). Теперь оба интегрированы в in-app scheduler
(тот же механизм что avito_city_sweep) — запускаются автоматически ночью.
#560 (cian_history_backfill):
- Window 02:00–05:00 UTC; pre-check verify_session() перед claim'ом слота —
если куки истекли, run не создаётся, отправляется GlitchTip capture_message.
- Вызывает существующий backfill_cian_history() как есть; checkpoint через
update_heartbeat() до и после вызова (для zombie-detection).
#563 (rosreestr_dkp_import):
- Window 04:00–06:00 UTC; sync функция в run_in_executor (DB-only, нет HTTP).
- Cursor-based пагинация (WHERE id > last_id ORDER BY id LIMIT batch_size),
heartbeat = checkpoint каждый батч (~2000 строк по умолчанию).
- SAVEPOINT per row через begin_nested() — один сбойный row не откатывает батч.
- Идемпотентен: ON CONFLICT (dedup_hash) DO NOTHING; dedup_hash = md5('ros:dkp:'+id)
соответствует Fix#549.
- Cleanup при старте: удаляет legacy rows address='Екатеринбург, реальная сделка'.
- Координаты NULL — геокодинг остаётся ручным follow-up.
Migration 072: seed rows scrape_schedules + foreign table gendesign_rosreestr_deals
(через gendesign_remote FDW server, настроен в 060).
Add warm-up GET /evaluation/realty + production browser header set (Sec-Fetch,
X-Requested-With, 25s timeout) mirroring avito.py, чтобы защитить 3-step IMV flow
от server-IP анти-бот 403. Refactor step B/C парсинга в pure helpers
(_parse_geo_position/_parse_price/_parse_placement_history/_parse_suggestions)
+ fixture-driven offline тесты из живых JSON-captures.
Честный диагноз: контракт (geoFieldsHash, price-блок, categoryId=24) НЕ менялся —
проверено живым capture'ом с этой машины (A→B→C, recommended 7.39M). Реальная
причина #565 — скорее анти-бот по PROD-IP, не код → если #565 останется после
deploy, настоящий фикс = ротация egress IP воркера (devops, отдельно).
code-reviewer: ✅ APPROVE no critical — warm-up non-fatal, injected-session path
unchanged, refactor preserves shapes, graceful-None сохранён, fixtures без PII.
Tests: 12 new (27 total). Ruff clean.
Closes#565
Avito switched the houses-catalog state blob from JSON.parse("<js-escaped>")
to a URL-encoded string literal, wrapped the 10 widgets in
placeholders[0].widgets, и re-nested developmentData под props.housePage.
JS-unescape path silently failed → houses rows перестали наполняться
(starve'ит #6 same-building + #569 deal centroids).
Extract decode в pure _extract_preloaded_state (urllib.parse.unquote,
JSON.parse fallback для старой формы), add _flatten_widgets, и сделать
_parse_house_page tolerant к обеим developmentData/ratingPreview формам.
Real-HTML fixture (Yumasheva-6 EKB) + 4 regression-теста.
code-reviewer: ✅ APPROVE no critical (regex anchoring verified, decode
safe — no eval/unicode_escape, backward-compat реальный, delisted-house
envelope degrade'ит gracefully, fixture без PII/secrets). Tests: 22 passed.
Closes#566
ORDER BY has_active_listings DESC, id во всех priority-бакетах + новый
--priority listings (focused sweep: NULL cadnum + есть active listing).
Дневной 100/day budget сначала закрывает дома, которые реально появляются
в оценках → estimator #6 cadastr_exact same-building матчинг загорается на
real estimate targets за дни, а не недели.
Correlated EXISTS бьёт по partial-индексу listings_house_id_fk_idx.
Write-path / EKB-guard / SAVEPOINT / qc_geo rules не тронуты.
code-reviewer: ✅ APPROVE no critical. Tests: 40 passed. Ruff clean.
Добавляет scripts/geocode_deals_nominatim.py для ~14% сделок всё ещё
lat IS NULL после house-centroid прохода (Step 2) — улицы без геокодированных
домов, которые centroid-join не покрывает. Переиспользует geocoder.geocode
(cache → Cadastral FDW → Yandex → Nominatim), дедупит по адресу (distinct
улицы определяют число вызовов, не 7k строк), и стампит geocode_tried_at на
КАЖДОЙ попытке (успех И чистый промах) чтобы un-geocodable строки не крутились
по 1 req/s. Resume: lat IS NULL + 30-day staleness, per-address SAVEPOINT,
--limit/--dry-run/--batch/--stale-days.
Pre-push review (code-reviewer): ✅ APPROVE, no critical — проверены SQL-safety,
no-infinite-reloop (mark-tried на miss, no-stamp на raised exc), window-cap,
geom-not-manual, dedup, async. Tests: 17 passed. Ruff clean.
Estimator теперь резолвит target-адрес в КАНОНИЧЕСКИЙ house_id (read-only,
без создания записи) и матчит аналоги по listings.house_id_fk — детерминированный
Tier S «тот же дом» вместо хрупкого address-string prefix match. 99% листингов
слинкованы с houses, поэтому это срабатывает уже сегодня и точнее работает между
разнородными источниками (Avito/Cian/Yandex форматируют адрес по-разному).
- matching/houses.py: match_house_readonly() — read-only target→house резолвер
(cadastr_exact → fingerprint → geo_proximity 50m), без INSERT / house_sources /
advisory lock. Возвращает None если уверенного match нет.
- estimator.estimate_quality: после geocode+DaData резолвит target_house_id
(cadastr от DaData → cadastr_exact заработает по мере backfill houses), graceful.
- estimator._fetch_analogs: новый param target_house_id + канонический Tier S
(WHERE house_id_fk = target) ПЕРЕД address-prefix Tier S (теперь fallback).
tier label 'S' сохранён (frontend уже рендерит).
Эффект: вместо «похожие в радиусе 800-1000м» — «вот что в ЭТОМ доме», прямой
выигрыш в точности оценки. Foundation под cadastr/FIAS exact match (DaData Q1/Q3).
Tests: 9 новых (match_house_readonly tiers + never-inserts + canonical Tier S
fires/fallback/too-few). Ruff clean. Pre-existing 4 fail в test_estimator_source_quota
(unpack 2≠3, красные на main до этого PR) — не затронуты.
Dry-run на проде показал, что DaData без явного города мапит короткие
street-адреса в Москву по умолчанию («улица Просторная» → «г Москва, ул
Просторная»). Это бы привело к catastrophic false-positive enrichment'у
если бы скрипт запустили без --dry-run.
Two-layer fix:
1. `_with_city_context(addr)` — pre-pends «Екатеринбург, » если в адресе
ещё нет упоминания города. Идемпотентен, case-insensitive.
2. `_result_in_ekb(result)` — post-filter. Проверяет `raw.city`
(структурное поле DaData) с fallback на substring search в
`canonical_address`. Защищает даже если DaData проигнорирует city-префикс
и всё равно вернёт другой город (например для homonym streets).
`_is_enriched_result()` теперь требует обе проверки: qc_geo IN (0,1)
AND coords AND city == Екатеринбург. Только так делаем UPDATE coords/cadnum.
Без этого fix'а нельзя запускать backfill — все non-EKB false-positives
перезаписали бы NULL coords мусором.
9 новых тестов: city context (prepend / idempotent / case / empty),
post-filter (raw.city / canonical / Moscow reject / None).
Combined 29 passed.
Оставляем TI-логотип и «Trade-In» рядом. PRINZIP-брендинг скрыт — для
текущего демо/пилота он не нужен. Когда понадобится white-label под
конкретного заказчика, вернём через `brand-name` slot.
Найдено пользователем в шапке gendsgn.ru/trade-in.