App-level logs (logging.getLogger("app.*")) had no StreamHandler, so their
INFO was lost — `docker logs gendesign-backend-1` showed only uvicorn loggers.
Useful lines (e.g. "OSRM road-distance applied", RBAC, analyze) were invisible
when debugging on the VPS (only Sentry/GlitchTip received them as breadcrumbs).
main.py now attaches one StreamHandler to the root "app" logger at
APP_LOG_LEVEL (default INFO), idempotent (marker guard), propagate=True so the
Sentry LoggingIntegration on root still gets breadcrumbs/events and there's no
stdout double (root has no stdout handler). API process only — celery worker
(celery_app.py) untouched, since its loaders log per-sync and would be noisy.
Flood-safe on the API: the analyze hot path logs INFO per-request (parcels.py
has 3 logger.info), not per-row.
Adds tests/test_app_logging.py (handler present, level INFO, propagate intact).
Refs #1926
Found by adversarial valuation audit (2 confirmed, bot-safe).
FIX A (#5): both radius comp queries (Tier H ~3990, Tier W ~4135) ended with
a bare ORDER BY relevance_score; on score ties Postgres returned rows in
undefined order, so the same /analyze could pick different comps across runs.
Append deterministic tiebreaker: relevance_score ASC, distance_m ASC,
scraped_at DESC NULLS LAST, id ASC (id = listings PK → total order). Added id
to each base CTE; outer projection unchanged (no leak downstream).
FIX B (#2): _filter_outliers keeps rows with price_per_m2 IS NULL, but the
median is built from prices_ppm2 (drops them) while n_analogs counted all of
listings_clean — overstating contributing comps ("Найдено N аналогов"
misleading; all-price-less -> median=0 but n_analogs>0). Count n_analogs from
prices_ppm2 in the radius path. #698 anchor overwrite + #691 zero-analog->low
guard unaffected; listings_clean itself not filtered.
Adds tests/test_estimator_n_analogs_priced.py (verified to fail on old code).
Audit also flagged velocity fan-out (false-positive: 0 duplicate domrf_obj_id
on prod) and >/>= disclosure tweaks (cosmetic) — deliberately not changed.
Refs #1871
First XHR after goto(origin) intermittently hit "NetworkError when
attempting to fetch resource" (page net stack/anti-bot not ready),
healed only by the outer re-navigation retry (~30-45s/house in the
house_imv_backfill, #562).
_fetch_json_once now:
- settles FETCH_JSON_SETTLE_MS (default 1200, was hardcoded 500) — fewer
first-fails;
- wraps the in-page fetch() in a JS retry loop (FETCH_JSON_INPAGE_RETRIES
default 1, FETCH_JSON_RETRY_DELAY_MS default 800) that retries ONLY on
network throw, never on HTTP status (4xx/5xx short-circuit, caller
decides). An in-page retry costs ~retryDelayMs vs the ~30-45s outer
re-navigation. Last error re-thrown — outer crash-retry contract intact.
/fetch (SERP) path untouched. +2 tests (settle ms, retry params).
Refs #1917, #562
#1928 fixed scrape_runs.total_seen/new_count for sweeps via _column_counts
but only regression-tested the yandex mark_done path. Add equivalents for
the CitySweepCounters (avito/cian) sweep and the mark_failed path — both
explicitly named in the #1926 audit — so the all-sweeps + failure-path
guarantee is locked in.
Refs #1926
Migration 130 only filled house_id_fk for listings carrying
house_source/house_ext_id. Prod (2026-06-27): 1884 active NULL-FK rows
remain, 0 with house_source/ext_id (130 re-run = no-op). They were
matched via the per-listing-identity path the realtime mirror uses when
no house catalog id exists: match_or_create_house(ext_source=source,
ext_id=source_id) -> Tier-1 house_sources lookup (confidence 1.0).
136 replays that exact lookup offline: UPDATE listings SET house_id_fk
= house_sources.house_id WHERE (ext_source=source, ext_id=source_id) and
house_id_fk IS NULL. Faithful to base.py:704-750; idempotent (NULL
guard); deterministic (no fuzzy/geo); collision-safe (catalog ids live
under distinct ext_source like 'cian_newbuilding'). Dry-run BEGIN/ROLLBACK
fills 4608 NULL-FK rows (744 active), restoring estimator Tier-S
"same-building" grouping. The remaining ~1140 active need re-scrape/
re-match (scraper coverage — #1781 Лёха).
Refs #1781
Walk-relevant POIs (school/shop/park/kindergarten/pharmacy/stops) now route
via a FOOT OSRM graph (osrm-walk service), car-relevant POIs (mall/hospital +
unknown) keep the DRIVING graph. Validation showed driving overstated
pedestrian-proximity distance — median 1.6-2.9x straight-line (#39).
- config: osrm_walk_local_url + osrm_walk_categories (frozenset, 9 walk cats)
- osrm_client_local: base_url override on get_road_distances_m (default unchanged)
- _apply_osrm_road_distances: split POIs by category, per-group OSRM call with
independent graceful fallback (one server down -> its group keeps straight-line),
in-place write-back by original index; never raises; flag-OFF byte-identical
- docker-compose: osrm-walk service (foot graph, internal, mem_limit 1.5g)
- build_osrm.sh: CAR=0 gate for foot-only refresh (doesn't touch live car graph)
- tests: per-category split, per-group fallback, asymmetric intra-group write-back
Still flag-gated (use_osrm_distances OFF) — enabling is a product decision.
Refs #39
Добавляет тест test_n_analogs_reflects_deduped_count: 3 дубля одного объекта
(один source_id) → 1 кандидат (freshest scraped_at); 3 разных объекта → все 3.
Верифицирует ключевое требование #1871 P2: n_analogs отражает дедупнутое число,
а не raw кол-во строк в listings с дублями (prod: 797 yandex + 93 cian дублей
раздували n_analogs и искажали медиану/перцентили).
Добавляет settings-флаг estimate_confidence_floor_no_analogs (дефолт True),
гейтящий вызов _enforce_zero_analog_low перед сборкой AggregatedEstimate.
При n_analogs==0 и confidence!='low' форсит 'low' + добавляет caveat в
explanation («без сопоставимых аналогов рядом»), предотвращая показ выдуманного
«высокого» доверия когда оценка построена только на внешних оценщиках
(yandex_valuation/cian_valuation) без реальных рыночных аналогов.
Migration 133 failed on prod (offer_price_history UNIQUE (listing_id,change_time) + multi-loser collisions). Drop data-child re-points, rely on FK ON DELETE CASCADE; keep only merged_into. Dry-run on prod: 9840 groups, 9889 losers deleted, clean. Refs #1773
kn (DOM.РФ ЖК) runs weekly (Mon cron) but thresholds were 2/5 → false "stale" every Wed-Sun. Recalibrate to 8/14 (weekly, mirrors objective) + regression test. Surfaced by the freshness monitor's first prod run. Refs #73
smr/sale_inflation_annual in _build_monthly_cashflow, gated by module constants =0.0 → byte-identical zero-diff (deep-review verified IEEE-754 identity + all 57 prior tests unchanged). Land not escalated; marketing/VAT/tax retimed on escalated revenue (total preserved). Capability wired+tested, OFF until calibrated indices + invariant reframe. No schema/codegen. Refs #1881
Extract compute_freshness(db) from the /admin/scrape/freshness handler (endpoint unchanged) + daily beat task scrape_freshness_check that alerts via sentry_sdk.capture_message when a source is stale/failed (error on critical-failed, else warning). Registered in celery include + beat (09:00 MSK). Refs #73
Cache-miss upsert now runs in a fresh SessionLocal() instead of committing the shared /analyze transaction mid-request. Read-back via shared session unchanged. Refs #1850
Regenerate the drifted dev fixture financial_estimate via compute_financial() from its own teap_synth inputs — parking comfort-priced (#1893), VAT parking-only (#1898), levered_irr refreshed (#1900). Dev-only. Refs #1881
New price tier objective_geo_radius: ST_DWithin median of Objective new-build prices (objective_lots ⋈ complexes) within 3km of parcel centroid, between quarter-MV and district_reference. Closes the name-match gap (5 of 9 EKB districts had no Objective name-match). data/sql/168 functional GIST index (prod EXPLAIN 114ms→28ms). Degenerate-centroid guard + honest RU price_source captions. Deep-review ✅.
Refs #1881
VAT charged on full gross margin → only on parking value-added; residential (реализация жилья + услуги застройщика по ДДУ) exempt per пп.22/23.1 п.3 ст. 149 НК; input VAT embedded in gross СМР. Σ cashflow == net_profit invariant preserved. PDF + concept-UI caveats synced. +3 tests, deep-review ✅.
Refs #1881