Commit graph

625 commits

Author SHA1 Message Date
3b40ba09f7 fix(tradein/geocoder): темп Nominatim сдерживается перед запросом, а не после успеха (#2953) (#2954)
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 3m32s
Deploy Trade-In / build-backend (push) Successful in 1m20s
Deploy Trade-In / deploy (push) Successful in 1m36s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
2026-08-20 07:55:41 +00:00
cf48e6d6c8 feat(tradein): позиция квартиры внутри когорты аналогов — перцентиль (#2899) (#2926)
All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
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 3m54s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 2m6s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
2026-08-19 10:08:18 +00:00
d0071c57bc fix(tradein): фильтр выдачи Яндекса мёртв — slug ЖК ищем по странице объекта (#2860) (#2923)
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / deploy (push) Successful in 1m42s
2026-08-19 08:37:40 +00:00
215b78a348 Merge pull request 'fix(tradein/estimate): залипший anchor_tier больше не глушит четыре механизма разом' (#2920) from fix/2656-anchor-tier-reset into main
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 3m45s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 1m57s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-17 08:00:33 +00:00
bot-backend
42b0ebe338 fix(tradein/estimate): залипший anchor_tier молча глушил IMV-blend и quarter-index
All checks were successful
CI Trade-In / browser-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 / 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 Trade-In / backend-tests (pull_request) Successful in 4m32s
Флаг anchor_tier оставался равным anchor_tier_fetched ("A"/"C"), когда якорь
фактически НЕ строился — сброс делал только low-conf гейт (#audit-1), но не
Tier C corridor-гейт (#1795) и не сама _compute_same_building_anchor, когда
она отклоняет кандидата (комплов меньше estimate_sb_min_comps). Дальше по коду
залипший флаг читается как «headline построил якорь» и молча глушит IMV/Yandex
blend (#651, гейт `anchor_tier is None`) и quarter-index correction (#764
Guard-1a) — притом что радиусный headline их не получал.

Замер: 154 из 996 сделок теряют tier-флаг этой правкой, и у всех 154 изменение
цены ровно 0.000% — чинится именно залипший ФЛАГ, не ценообразование (баланс
метрик бэктеста подтверждает: единственная дельта в baseline — новая канарейка
unrecorded_lookup_calls, все остальные метрики побитово те же).

- estimator.py: сброс `anchor_tier = None` единой веткой `if anchor is None`
  после всех трёх гейтов (Tier C / low-conf / _compute_same_building_anchor);
  display-only IMV-карточка больше не гейтится по `anchor_tier is not None`
  (иначе терялась в щели «тир добыт, якорь не построен, headline подавлен»).
- backtest_estimator.py: quarter_index_lookup/quarter_indexes_lookup в реплее
  отвечают «промах» (None/{}), если сброс флага открыл путь, которого не было
  в замороженной фикстуре, вместо падения с RuntimeError; счётчик таких промахов
  уходит в baseline как unrecorded_lookup_calls (точное целое, канарейка на
  расхождение реплея с захватом). Заодно пиннится estimate_dedup_analogs_enabled
  = False внутри replay_fixture (было только в самом гейте) — иначе штатная
  регенерация baseline (--from-fixture --update-baseline) писала baseline,
  который тест не совпадал бы никогда.
- backtest_baseline.json: перегенерирован штатным путём, unrecorded_lookup_calls=0.

Выделено из #2656/PR #2661 — фильтры свежести (scraped_at) в якоре дома и в
знаменателе коэффициента выкупа остаются в исходном PR как отдельная, более
спорная правка (двигает деньги: знаменатель просаживается на ~1.3% по бакетам).
2026-08-17 10:54:07 +03:00
bot-backend
c66047683c Merge remote-tracking branch 'forgejo/main' into local/pr2836
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 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 4m36s
2026-08-17 10:41:25 +03:00
bot-backend
e00dcac177 fix(tradein/deactivate): resolve migration 264 renumber collision on merge with main
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / 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 4m27s
fix/tradein-ttl-effective-cap (PR #2907, merged into main while this branch was in
flight) already claimed 264/265 for deactivate_stale_avito_cap_mult /
deactivate_stale_yandex_cap_mult. Renumbers this branch's
264_seed_deactivate_stale_null_segment_yandex_cian.sql -> 266_seed_... (git mv +
_manifest_applied.txt entry moved after 264/265 + self-references in the migration
header and in test_deactivate_stale_listings.py's _MIGRATION_264 constant/test names).

Merges main's bool-guard (ttl_days/cap_mult reject bool) + CAP_MULT ceiling +
per-source cap_mult calibration with this branch's null_segment_only kwarg
(explicit `listing_segment IS NULL` predicate, since ANY(:segments) never matches
NULL) -- both features apply to the same deactivate_stale_listings() call site in
product_handlers.py and the same function signature/docstring in
deactivate_stale_avito.py, so every conflict was signature/docstring-level, not
logic-level (git already auto-merged the function body correctly since the two
features touch disjoint lines below the signature). Also reconciled the module
docstring's "known gap" note (main) to reflect that the NULL-segment slice it
measured (cian 211 / yandex 523 rows >60d) is now closed by this migration --
novostroyki stays open, unrelated to this branch.
2026-08-15 21:13:41 +03:00
bot-backend
cfb4c159ab fix(tradein/scraper): deactivate stale yandex/cian listings with NULL segment
544 yandex + 224 cian active rows carry listing_segment=NULL (legacy rows
predating migration 011, plus a small trickle that can never self-heal since
the upsert ON CONFLICT never rewrites listing_segment on re-scrape). 94-97%
of them are frozen at ~86 days old, yet the estimator's Tier A (same-building)
and Tier C (micro-radius) anchor queries filter only is_active=true -- no
freshness column -- so these stale asking prices anchor live valuations.

deactivate_stale_yandex/_cian (migration 115) already run at TTL=30 but scope
segments=['vtorichka'] only: `= ANY(CAST(:segments AS text[]))` never matches
NULL, so the NULL bucket was invisible to both existing jobs and to
avito/domklik/n1 (which are source-blanket or vtorichka-only respectively).

Adds null_segment_only kwarg to deactivate_stale_listings() building an
explicit `listing_segment IS NULL` predicate (confirmations/revisit-floor
builders extended in parallel for correctness, though both gates are kept
off for this slice -- population too small for thresholds calibrated on a
full vtorichka sweep, would permanently skip as unhealthy). Two new
scrape_schedules rows (migration 264) run it per source, untouched
novostroyki/vtorichka jobs unaffected.

TTL=60d (vs 30d for vtorichka): revisit-floor is not computable here (rows
out of dedicated sweep scope have no revisit-gap history), so the margin is
folded into the TTL directly -- 2.26x/1.4x over the measured p99 revisit gaps
already on file for cian/yandex vtorichka (26.6d/43.0d). Bimodal age
distribution means this costs almost nothing in coverage (30d vs 60d: 744 vs
734 deactivated).

First run: 734 of 768 NULL rows deactivated (211 cian, 523 yandex), 34 remain
(fresher than 60d, still incidentally re-touched). Comparable pool
(is_active AND segment IN (NULL, vtorichka)) after: cian -2.7% (7739->7528),
yandex -10.2% (5133->4610) -- yandex crosses the 10% flag threshold. All 734
removed rows already had scraped_at frozen >60d, i.e. already excluded from
Tier S/H (which do filter freshness, 14-60d window) -- the drop is real for
is_active headcount but zero-impact there; it only prunes Tier A/C, where it
removes stale prices rather than live comps.

Separate finding (not fixed here): base.py's ON CONFLICT DO UPDATE omits
listing_segment from SET entirely, so a legacy NULL row can never heal even
though cian/yandex SERP always compute segment deterministically on re-scrape.
2026-08-15 20:44:36 +03:00
bot-backend
8fff174337 Merge remote-tracking branch 'forgejo/main' into fix/tradein-ttl-effective-cap 2026-08-15 20:26:40 +03:00
bot-backend
349494a9df fix(tradein/observability): close RetryError-half of GlitchTip noise fix (round 2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 Trade-In / backend-tests (pull_request) Successful in 4m30s
CI / openapi-codegen-check (pull_request) Has been skipped
Ревью round 1 подтвердил basic_auth-часть, но нашёл 4 факта в RetryError-части:

1. reraise=True в geocoder.py не убирает шум, а переименовывает: наружу летит
   httpx.HTTPStatusError, чей str() содержит ПОЛНЫЙ request URL с query string
   (`for url '...search?q=<адрес>&...'`) — воспроизведено эмпирически. Тот же
   per-address issue-explosion, просто под другим типом исключения.
   Фикс: _HTTPX_ERROR_URL_QUERY_RE в sentry_scrub.scrub_pii_event режет query
   string из httpx-style "for url '...'" сообщений — стабилизирует ТЕКСТ, не
   только тип, независимо от того, уважает ли GlitchTip fingerprint-поле.

2. stabilize_retry_error_fingerprint затирал fingerprint целиком по (типу
   причины) — RetryError из НЕСВЯЗАННЫХ подсистем с одинаковым типом причины
   схлопнулись бы в один issue (geocoder vs scraper_kit оба ловят httpx-типы).
   Фикс: culprit = event["logger"] (LoggingIntegration ставит его = имя
   модуля-источника logger.exception) идёт первым компонентом fingerprint —
   разные подсистемы больше не сливаются.

3. Второй живой источник RetryError, пропущенный round 1 (грепали литерал
   "RetryError", не producers): BaseScraper._http_get в packages/scraper-kit —
   @retry БЕЗ reraise=True, живой путь через YandexDetailScraper.fetch_detail
   (yandex/serp.py и valuation.py переопределяют _http_get без retry — не
   затронуты). Оставлен на fingerprint-хук намеренно: detail-URL варьируются
   в ПУТИ (offer id), не в query — _HTTPX_ERROR_URL_QUERY_RE их не покрывает,
   а добавление reraise=True туда воспроизвело бы ту же проблему через
   HTTPStatusError с variable path вместо query.

4. type(exc).__name__ == "RetryError" (string-compare) → isinstance(exc,
   RetryError) с прямым импортом tenacity.RetryError — не матчит посторонние
   классы с тем же __name__, не промахивается мимо подклассов.

Полный backend suite (4479 passed, 21 skipped) + geocoder/scheduler/alerts
подмножества — без регрессий (reraise=True уже влит в main).

Не тронуто (вне scope round 2, подтверждено ревьюером как верное):
ops/glitchtip-auth-forwarder/* (basic_auth 401 дроп), массовая чистка
накопленных issue.
2026-08-15 20:02:44 +03:00
bot-backend
662720e1c8 Merge remote-tracking branch 'forgejo/main' into _cont-tradein-glitchtip-noise 2026-08-15 19:43:34 +03:00
bot-backend
bef2a05f60 Merge remote-tracking branch 'forgejo/main' into local/fix-ttl-effective-cap-review2 2026-08-15 19:43:03 +03:00
85414dabd2 Merge pull request 'fix(tradein/scrapers): прогон больше не рапортует «done» поверх провала и нуля' (#2892) from fix/tradein-honest-run-status into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-frontend (push) Successful in 3m0s
Deploy Trade-In / test (push) Successful in 4m22s
Deploy Trade-In / build-browser (push) Successful in 4m50s
Deploy Trade-In / build-backend (push) Successful in 1m51s
Deploy Trade-In / deploy (push) Successful in 2m40s
Deploy Trade-In / deploy-status (push) Successful in 1s
2026-08-15 16:20:30 +00:00
bot-backend
fa84705ec7 fix(tradein/geocoder): stop apt number leaking into house + houses bbox/sibling guard
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / 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 5m0s
Review round 2 on #2626 (local houses fallback) found two HIGH-severity bugs
verified live against prod data:

1. _extract_local_house_token took the LAST digit-like token in the raw
   address, so "...Педагогическая, д 15, кв 11" resolved house=11 (apartment
   number) instead of 15 -- confidently returning a stranger's building with
   confidence='exact', written to geocode_cache. Fixed by stripping the
   apartment/office/floor/entrance tail (кв/оф/пом/подъезд/этаж -- NOT
   корп/к, which is part of the house number) before extracting the token.
   Fixes the exact prod case from the review plus the corpus+apartment
   combo ("д 26 к 1, кв 41" -> 26к1, not 41).

2. houses is not an EKB-only table (21% of rows with coords are outside the
   metro, some as far as another city) -- "улица Маяковского, 7" in houses
   resolves to Серов, not Екатеринбург, and use_local_ekb only gates the
   user's query text, not the source row. Added an is_within_ekb_bbox_wide
   check on every candidate row before it can become a match.

Also addressed two MEDIUM findings from the same review:

3. The "<номер> -> <номер>к1" corpus guess only checked uniqueness among
   к1-labelled rows, so real multi-building addresses (Онуфриева 24: к1/к2/к3,
   250-400m apart) resolved confidently to к1 anyway. Guess is now skipped
   when any other corpus/slash variant of the same base number exists among
   the street's candidates.

4. Houses-fallback results are no longer cached in geocode_cache -- the
   source (scraped listings) is less reliable than geoportal/cadastral/
   Nominatim, and the lookup is cheap/local, so caching only extended the
   lifetime of a possible bad match. Side benefit: address_refined now
   survives every repeat request of the same raw address, not just the
   first.

Also added ORDER BY address, id to the underlying query so the coordinate
dedup picks a deterministic row (LOW finding #5).

14 new/updated tests in test_geocoder_local_houses_fallback.py cover all
five findings against real prod address/houses-row fixtures. Full geocoder
+ dadata + estimator/pdf regression suite (402 tests) green.
2026-08-15 18:50:17 +03:00
bot-backend
e9ca744e85 fix(tradein/scrapers): не путать rows_inserted/processed с честным результатным ключом
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / 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 5m4s
Ревью честного run-status нашло, что _RESULT_COUNTER_KEYS ловил не только целевой
yandex_newbuilding_sweep, но и rosreestr_dkp_import (rows_inserted, 66 из 67 прод-
прогонов = здоровый ноль догнавшего инкрементального импорта) и newbuilding_enrich
(processed — счётчик попыток, ==limit даже при частичном провале). Первое завело бы
практически непрерываемый ложный zero-стрик у здорового источника, второе маскировало
бы реальные отказы под measured-N.

Проверено по прод-БД (2026-08-15): "succeeded" пишут ТОЛЬКО yandex_newbuilding_sweep
(42 прогона/90д) и newbuilding_enrich (65/90д) — ни разу rosreestr_dkp_import; у
yandex_newbuilding_sweep succeeded численно совпадает с rows_inserted на всех 42/42
прогонах. Заменил "rows_inserted"+"processed" на "succeeded" в _RESULT_COUNTER_KEYS
(app-копия и byte-эквивалентная kit-копия) — цель (b) исходной правки сохранена, ложный
стрик у rosreestr_dkp_import снят, попутно newbuilding_enrich получает честное
измерение вместо счётчика попыток.

Также поправлены докстринги test_backfill_honest_status.py — два кейса (76%/72%
отказов -> 'done') проверяют только выбор финализатора mark_backfill_finished
(mark_done там замокан); реальный mark_done с honest-run-status переквалифицирует их
в 'failed' через _failed_ratio_too_high — это не документировалось явно.
2026-08-15 18:49:37 +03:00
bot-backend
cb79c67bfc fix(tradein/deactivate): make TTL-cap multiplier configurable per source
Review of 3a1e29a7 found CAP_MULT=2 is uniform across sources with wildly
different ttl_days, so it produces a different ABSOLUTE ceiling per source:
cian/yandex (ttl=30) -> 60d, avito (ttl=10) -> 20d, domklik (ttl=14) -> 28d.
That breaks exactly where the crawl's revisit tail doesn't scale with
ttl_days: avito's measured p99 revisit gap is 42.1d (_REVISIT_TAIL) --
above its own default cap of 20d -- so a legitimately slow-but-alive avito
crawl cycle would get its floor cut below the very tail the floor exists
to protect (the false-kill scenario #2659 was filed for). cian/yandex/
domklik aren't affected: their default ceilings (60/60/28) already sit
comfortably above their own measured tails (26.6/43.0/3.1).

Fix: cap_mult is now a function parameter (same pattern as
revisit_floor_quantile/min_confirmations) with the module constant CAP_MULT
as its default, wired through product_handlers via default_params["cap_mult"]
so a schedule can override it without touching the shared default. Also
closes the ttl_days<=0 edge case flagged in the same review: before the cap,
max(ttl_days, floor) tolerated a misconfigured ttl_days<=0 as long as the
floor was positive; with the cap, min(floor, ttl_days*cap_mult<=0) would
silently defeat that protection and match nearly the whole active pool.
ttl_days<=0 now raises ValueError before any SQL, same contract as the
existing staleness_column whitelist check.

Also verified (read-only, postgres-tradein) the review's core "no-op"
claim: false. scrape_runs.counters for the 6 days since the revisit-floor
went live (08-10..08-15) show the cap DID bind on 3 of 6 runs for avito
(floor 52 vs cap 20) and 3 of 6 for yandex (floor 75 vs cap 60) -- the
reviewer's "no source hits the cap" read a single-day trough right after
a natural recovery, not the whole observation window. See PR discussion
for the full counter history and refutation detail.

Refs #2659
2026-08-15 18:47:49 +03:00
bot-backend
c75206c348 fix(tradein/geocoder): local houses fallback + downgrade DaData CLEAN-disabled noise
28/1084 прод-оценок имели lat IS NULL — гарантированный ноль аналогов, клиент
не получал оценку вовсе. Дом уже был в houses (скрейпленные листинги), но не
резолвился ни geoportal/cad_buildings, ни Nominatim: разговорное/усечённое имя
улицы («Онуфриева» вместо ГАР-каноничного «Начдива Онуфриева») или отсутствующий
в вводе корпус («49» вместо реального «49к1»). Добавлен последний тир geocode()
с двумя defensive-допущениями (суффиксный матч улицы + опциональная догадка
«номер+к1») — при любой неоднозначности возвращает None, а не гадает; проверено
живыми прод-адресами (Онуфриева/Хрустальногорская резолвятся, Крестинского
корректно остаётся неоднозначным — два разных дома в houses под одним номером).

Отдельно: HTTP 403 «услуга CLEAN выключена на аккаунте» логировался как ERROR
на каждый /estimate (164 события) — это статичная конфигурация аккаунта, а не
сбой; понижено до WARNING (первый раз за процесс) + DEBUG на повторы, чтобы
ERROR продолжал значить настоящую проблему.
2026-08-15 18:08:09 +03:00
bot-backend
8fcec9f12e fix(tradein/observability): stop basic_auth 401 and RetryError GlitchTip noise
83% of tracker issues (7460 total) were pure noise drowning real signal:
- basic_auth 401 (3738 issues, 2019 distinct titles) — ops/glitchtip-auth-
  forwarder sent EVERY 401 from bots scanning gendsgn.ru (GET /wp-admin/
  install.php etc.) as an individual GlitchTip event, remote_ip baked into
  message/tags inflated cardinality. Not an application error — expected
  bot-scan traffic against a basic_auth-protected site.
- RetryError (2462 issues) — geocoder.py's three tenacity @retry-wrapped
  Nominatim helpers (lookup/suggest/reverse) raised tenacity.RetryError on
  exhaustion without reraise=True; RetryError.__str__() embeds a Future
  repr() with a memory address that differs every call, so GlitchTip
  grouped each exhausted retry as a distinct issue instead of one.

Fix at the source, not post-hoc issue cleanup:
- forwarder.py: before_send drops events tagged event_type in
  {basic_auth_failed, basic_auth_storm}; forwarder's own capture_exception
  (real script bugs) carries no such tag and passes through untouched.
- geocoder.py: reraise=True on all three @retry decorators — propagates
  the real underlying exception (stable type + stacktrace) instead of the
  unstable RetryError wrapper.
- sentry_scrub.stabilize_retry_error_fingerprint: belt-and-suspenders
  before_send hook, composed into both app/main.py and scheduler_main.py
  (geocoder runs in both processes — FastAPI request path and the
  overnight geocode_missing_listings batch). Collapses any RetryError that
  still slips through into one persistent issue per cause-exception type
  name only — never IP/address/listing-id.

Content-ful categories (OperationalError, city-sweep, harvest_quarter,
cian/avito/yandex sweep failures, scrape_freshness_check — ~700 issues)
are untouched: filters key off event_type tag / exception type name only.
2026-08-15 18:08:04 +03:00
bot-backend
885031420e fix(tradein/scrapers): honest run status — стоп 'done' поверх провала и нуля
Три прод-факта, где status='done' врал о реальном исходе прогона:

- avito_detail_backfill 15.08: {"attempted":64,"failed":57,"enriched":6,"blocked":1}
  -> 'done'. mark_backfill_finished звал mark_done, потому что produced=6 (>0);
  ни _sweep_run_did_nothing (нет anchors_total/errors_count у backfill'ов), ни
  _phase_totally_failed (голые "attempted"/"failed" без фазового префикса) эту
  форму counters не ловили. Новый _failed_ratio_too_high внутри mark_done:
  failed/attempted >= 0.5 -> 'failed', >= 0.15 -> тоже 'failed' (другая
  формулировка причины в error-тексте) — 'partial' статусом не заведён: это
  потребовало бы DROP+ADD CHECK constraint (051_scrape_runs_extend.sql) и
  дообучения ещё 4 мест (Literal-фильтр admin API, статусы фронта, оба
  IN-списка сторожей) — тот же класс проводки, что и у ban_kind (#2686/#2764),
  который сознательно не стал новым статусом.

- yandex_newbuilding_sweep 26.07-10.08: десять прогонов подряд 'done' при
  processed=5 succeeded=0 rows_inserted=0 failed_resolve=4-5 — сторож нулевого
  результата (_alert_if_consecutive_zero_results) не видел ни один результатный
  ключ этого sweep'а и молчал навсегда. _RESULT_COUNTER_KEYS дополнен
  rows_inserted/processed (именно в этом порядке — rows_inserted это результат,
  processed это попытки; иначе "5 обработано, 0 записано" замаскировалось бы
  под measured-5).

- admin-витрина показывала new_count=0 у трёх подряд cian_full_load при реально
  сохранённых saved_inserted=482/214/239 — full-load'ы не пишут ни 'new_count',
  ни 'lots_inserted'. _column_counts дополнен saved_inserted/rows_inserted.

Правки продублированы в scraper_kit/orchestration/runs.py (byte-эквивалент
app.services.scrape_runs, см. докстринг модуля) для параллели: единственный
текущий писатель "attempted"/"failed" (mark_backfill_finished) живёт только в
app-копии, но приоритет ключей/константы держим синхронными на будущее.

Не тронуто: сознательно пустые sweep'ы (errors_count=0, honest empty) и малые
батчи (attempted < 3) — доля отказов на них не считается диагнозом.

Tests: tests/test_honest_run_status_failed_ratio.py (41 кейс, оба модуля,
включая точные прод-числа из трёх фактов выше) + regression-прогон 609 тестов
по всем файлам, трогающим scrape_runs/orchestration.runs — 0 регрессий.
2026-08-15 18:06:33 +03:00
bot-backend
eb0f082790 fix(tradein/pdf): 135-ФЗ дисклеймер в футере каждой страницы (Блок 4.2)
Some checks failed
CI / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / 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) Failing after 4m42s
Running @bottom-center margin-box печатал только мета/wordmark — юр-требование
(индикативный расчёт, не отчёт об оценке по 135-ФЗ) отсутствовало на всех 4
страницах. Бюджет высоты подвала = margin-bottom (19mm≈53.9pt) уже был
заполнен почти впритык (~51pt) после 42a50cf8 (ровно 4 страницы без пустых).

Сжат существующий HUD-хром внутри _page_footer (margin-top 6→4pt, padding-top
8→6pt, line-height мета/wordmark 1.35→1.15, разделитель margin 6pt 0→3pt 0,
экономия ~13pt) + добавлен текст дисклеймера отдельным блоком (5pt/line-height
1.15, ~3 строки ≈17pt). Экономии внутри подвала не хватило без деградации до
нечитаемого — минимально поднят @page margin-bottom 19mm→21mm (+2mm).

Реальный WeasyPrint-рендер (native Pango/cairo) недоступен на Windows-деве —
пагинация (риск отката к 5-й пустой странице из-за margin-bottom на всех 4
страницах) не подтверждена локально, арифметика в docstring _page_footer.
2026-08-14 15:25:00 +03:00
a4d6cbba25 fix(tradein/proxy): учитывать историю банов при выборе egress-узла (#2877)
All checks were successful
Deploy Trade-In / changes (push) Successful in 21s
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 4m1s
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 1m24s
2026-08-13 17:47:01 +00:00
17d558b18c fix(tradein/yandex): снести признак «панорама» — его нет на площадке (#2851)
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 3m27s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 2m12s
2026-08-12 20:44:11 +00:00
bcc4f2c29e docs(tradein/estimator): докстринг домового IMV-якоря говорит правду о таблице (#2674) (#2852)
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 3m28s
Deploy Trade-In / build-backend (push) Successful in 1m2s
Deploy Trade-In / deploy (push) Successful in 1m19s
2026-08-12 20:29:28 +00:00
13ce1045bb fix(tradein/estimate): домовой IMV-якорь приводится к базису ремонта квартиры (#2677) (#2816)
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 3m30s
Deploy Trade-In / build-backend (push) Successful in 1m0s
Deploy Trade-In / deploy (push) Successful in 1m52s
2026-08-12 20:17:10 +00:00
3cd7e0a9c4 fix(tradein/sber): сторож мерит отставание загрузки, а не календарь (#2846) (#2849)
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 3m32s
Deploy Trade-In / build-backend (push) Successful in 1m2s
Deploy Trade-In / deploy (push) Successful in 2m3s
2026-08-12 19:36:20 +00:00
e59a102b16 fix(tradein/dkp): «ФАКТИЧЕСКИЕ СДЕЛКИ» называют свой возраст, а не окно поиска (#2847)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m13s
Deploy Trade-In / test (push) Successful in 3m33s
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / deploy (push) Successful in 7m1s
2026-08-12 18:51:10 +00:00
c927b77777 fix(tradein/imv): «временная» ошибка снова временная — 1390 домов возвращаются в очередь (#2843)
Some checks failed
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Failing after 3m29s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
2026-08-12 16:06:24 +00:00
9a89c34971 fix(tradein/estimate): не подставлять квартал соседа вместо квартала цели (#2839)
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 3m13s
Deploy Trade-In / build-backend (push) Successful in 1m2s
Deploy Trade-In / deploy (push) Successful in 1m30s
2026-08-12 15:55:24 +00:00
677fcb749c fix(tradein/geocoder): инвариант «чужой топоним + результат внутри ЕКБ = недостоверно» (#2590) (#2837)
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 3m11s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 2m9s
2026-08-12 14:26:58 +00:00
dbe3e05fb1 fix(tradein/estimate): нет индекса у аналогов — нет квартальной поправки (#2583 C3)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
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 Trade-In / backend-tests (pull_request) Successful in 3m59s
price_index нормирован на медиану Екатеринбурга (99a_quarter_price_index.sql),
поэтому фолбэк `avg_analog_index = ... else 1.0` подставлял в знаменатель
gap-коррекции не «нейтраль», а уровень ЕКБ. Для цели вне ЕКБ (индексы области
0.28–0.82) это превращало поправку в безусловную скидку: factor = target_qi,
после клампа до −40%, с подписью «Учтена локация квартала» — то есть догадка
выдавалась пользователю за методику.

Нет данных → нет поправки. Ровно тот же factor=1.0 получается из avg := target_qi,
и это лучшая оценка неизвестного avg на живых данных: медиана |ошибки| 0.116
против 0.161 у 1.0, p90 0.337 против 0.517 (400 лотов, 2026-08-12).

Проверка направления на сделках Росреестра (12 мес, медианы ₽/м² по городам):
без поправки ошибка +0…+14%, с текущей поправкой −32…−40%. Правка поднимает
цену и одновременно уводит её к правде, а не просто вверх.

MV и FDW не трогаются намеренно: строки basis='district'/'city_fallback' имеют
n_deals 3–4, а эстиматор требует n_deals >= 10 — второй 1.0 (city_fallback в
99a) до него структурно не доходит (прод: 0 из 1894 строк видимы).

Тесты: два прежних кейса задавали уровень аналогов отсутствием кадастра,
то есть опирались на сам дефект — переведены на явную карту analog_indexes.

Refs #2583
2026-08-12 19:18:01 +05:00
ad190be233 fix(tradein/estimate): отдавать фактический радиус подбора, а не выбор пользователя (#2835)
All checks were successful
Deploy Trade-In / changes (push) Successful in 18s
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 3m23s
Deploy Trade-In / build-backend (push) Successful in 1m6s
Deploy Trade-In / deploy (push) Successful in 1m47s
2026-08-12 12:48:18 +00:00
e17687aed7 fix(tradein/scrapers): убрать оставшиеся обходы пула прокси (#2830) (#2833)
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 3m13s
Deploy Trade-In / build-backend (push) Successful in 1m46s
Deploy Trade-In / deploy (push) Successful in 1m48s
2026-08-12 10:53:36 +00:00
f2cbd76ae0 fix(tradein/scrapers): egress по источнику из пула с учётом банов, а не статичный env (#2831)
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 3m12s
Deploy Trade-In / build-backend (push) Successful in 1m1s
Deploy Trade-In / deploy (push) Successful in 1m27s
2026-08-11 06:29:48 +00:00
82d8db9f42 fix(tradein/estimate): оживлять мёртвые сохранённые оценки при открытии (#2826)
All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 3m11s
Deploy Trade-In / test (push) Successful in 3m42s
Deploy Trade-In / build-backend (push) Successful in 1m8s
Deploy Trade-In / deploy (push) Successful in 6m59s
2026-08-11 04:15:40 +00:00
0b84f0b661 fix(tradein/estimate): уверенность не может противоречить надёжности выборки (#2825)
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 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Successful in 2m1s
2026-08-10 16:21:31 +00:00
8423af5dd5 feat(tradein): версионирование продукта — единый источник, подвал, PDF, /versions (#2824)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Successful in 36s
Deploy Trade-In / build-frontend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m14s
Deploy Trade-In / build-backend (push) Successful in 4m19s
Deploy Trade-In / deploy (push) Successful in 1m44s
2026-08-10 16:00:54 +00:00
9d9457f67d fix(tradein/estimate): не блокировать оценку — расширять подбор и честно предупреждать (#2823)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m30s
Deploy Trade-In / test (push) Successful in 3m14s
Deploy Trade-In / build-backend (push) Successful in 7m24s
Deploy Trade-In / deploy (push) Successful in 1m25s
2026-08-10 15:42:22 +00:00
74344f7b8b docs(tradein/scraper): 42 прогона, а не 52 — в замере считались пары «прогон × фаза» (#2700) (#2822)
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 3m5s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m36s
2026-08-10 11:49:27 +00:00
20ec6a5d33 fix(tradein/cian): 403 снимает узел с выдачи, а не гаснет в return None (#2700) (#2821)
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 3m10s
Deploy Trade-In / build-backend (push) Successful in 1m34s
Deploy Trade-In / deploy (push) Successful in 2m4s
2026-08-10 11:29:19 +00:00
0ed0140c9e fix(tradein/dedup): остаток схлопывания домов становится измеряемым числом, а не оценкой (#2690) (#2820)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
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 3m11s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 1m57s
2026-08-10 11:18:19 +00:00
a227877905 fix(tradein/scraper): прогон, у которого отказал каждый якорь, перестаёт быть успехом (#2625) (#2813)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
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 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 1m35s
2026-08-10 09:36:23 +00:00
272abac4d2 fix(tradein/matching): город развёртки доезжает до стража Tier-2a — межгородские склейки домов (#2777) (#2808)
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 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 2m3s
2026-08-10 08:51:14 +00:00
27e199e370 fix(tradein/proxy): упавшая проба присваивала себе бан боевого сбора (#2800) (#2805)
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 3m4s
Deploy Trade-In / build-backend (push) Successful in 1m1s
Deploy Trade-In / deploy (push) Successful in 1m12s
2026-08-09 20:13:10 +00:00
08bb9d6549 fix(tradein/proxy): проба спрашивает каждую площадку, вердикт пишется на пару «узел × источник» (#2800 B) (#2803)
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 3m4s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 2m0s
2026-08-09 18:28:37 +00:00
7cd8c63b89 fix(tradein/proxy): узел, зарезервированный за Домкликом, до Домклика не доходит (#2800 A) (#2802)
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 3m6s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 6m36s
2026-08-09 18:15:55 +00:00
f1f2bca2e9 fix(tradein/deactivate): TTL не снимает объявления по порогу ниже собственного цикла обхода (#2797)
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 3m5s
Deploy Trade-In / build-backend (push) Successful in 56s
Deploy Trade-In / deploy (push) Successful in 1m45s
2026-08-09 17:26:18 +00:00
bot-backend
7431615415 Merge remote-tracking branch 'forgejo/main' into feat/tradein-paid-retention
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 3m57s
# Conflicts:
#	tradein-mvp/backend/data/sql/_manifest_applied.txt
#	tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts
2026-08-07 15:40:52 +03:00
306fd82836 fix(tradein): свести высоту потолков к одной колонке ceiling_height_m (#2779)
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 3m5s
Deploy Trade-In / build-backend (push) Successful in 1m43s
Deploy Trade-In / deploy (push) Successful in 2m26s
2026-08-07 09:18:48 +00:00
de4b2a4ae5 fix(tradein/houses): вернуть координаты объявлений в дом, когда объявления согласны (#2771) (#2780)
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m19s
Deploy Trade-In / build-frontend (push) Successful in 3m48s
Deploy Trade-In / build-backend (push) Successful in 1m8s
Deploy Trade-In / deploy (push) Successful in 1m44s
2026-08-07 09:08:06 +00:00
0de22f4bc9 fix(tradein/scraper): диагноз бана перестаёт назначаться по умолчанию (#2764) (#2765)
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 2m59s
Deploy Trade-In / build-backend (push) Successful in 2m1s
Deploy Trade-In / deploy (push) Successful in 2m14s
2026-08-06 23:17:59 +00:00