Move JobSettings UI from /admin/jobs/settings into JobSettingsPanel component,
render it alongside new BulkGeoPanel on /admin/scrape/all. BulkGeoPanel has a
parallelism input (1-10, default 5) and POSTs /admin/scrape/geo/bulk. Remove
the standalone /admin/jobs/ route and the nav tab pointing to it.
Объединяю несколько связанных изменений вокруг NSPD geo bulk-fetcher:
Adapt to rosreestr2coord v5 API:
- nspd_lite.fetch_via_rosreestr2coord: drop `delay` kwarg from Area()
(removed upstream in v5); keep it in our function signature for
backward-compat, comment why.
- nspd_geo worker: add explicit time.sleep(rate_ms/1000) after lib-branch
fetch — in v4 the library throttled internally via delay, in v5
rate limiting is the caller's job. Без этого получали Area.__init__()
unexpected kwarg `delay` на каждом target.
Drop use_rosreestr2coord switch:
- Removed urllib-vs-lib choice everywhere. We always use community
rosreestr2coord library — авторы регулярно обновляют WAF-tricks,
наш urllib-fetcher (fetch_geoportal) уже неактуален.
- admin_scrape.py: Pydantic schema, INSERT, SELECT, API response
cleaned of `use_rosreestr2coord`.
- nspd_geo.enqueue_geo_job: param dropped, INSERT shrunk.
- worker process loop: dropped `if use_lib:` branch + import of
fetch_geoportal.
- frontend/geo/page.tsx: removed checkbox + GeoJob.use_rosreestr2coord
field + POST body field.
DB column drop:
- data/sql/78_drop_use_rosreestr2coord.sql (NEW):
DROP COLUMN nspd_geo_jobs.use_rosreestr2coord + CREATE OR REPLACE
VIEW v_scrape_runs_unified (которая depended on the column).
- data/sql/77_nspd_geo_jobs.sql: cleaned historical DDL for fresh setups.
- Migration applied to prod (in-conversation via postgres MCP).
Frontend polish:
- Thematic ID changed from free-form number input to labeled select
(1=parcel / 2=quarter / 4=admin / 5=building / 7=zone / 15=complex).
- Auto-sync thematic_id from Job kind on change (override possible).
- ScrapeLogsPanel: extended union type with "nspd_geo" + fixed
/admin/scrape/geo to pass scraperType="nspd_geo" (was "nspd",
filtering empty legacy nspd_scrape_log table; real logs live in
nspd_geo_log via v_scrape_log_unified).
Verified: ruff ✓, tsc --noEmit ✓, migration ran (BEGIN..COMMIT clean).
Deploy order safe: prod column уже удалена → новый backend код, который
не INSERT'ит use_rosreestr2coord, совпадёт со схемой после deploy.
Two fixes for the NSPD geo bulk-fetcher:
1. rosreestr2coord was listed in pyproject.toml but missing from uv.lock,
so Docker's `uv sync --frozen` didn't install it. NSPD jobs failed every
target with `No module named 'rosreestr2coord'`. Ran `uv lock` to add it
(v5.3.3) plus requests + charset-normalizer transitive deps.
2. /admin/scrape/geo and /admin/scrape/objective lacked the log panel that
/admin/scrape (DomRF) had. Extracted ScrapeLogsPanel component over the
unified /api/v1/admin/scrape/all/logs endpoint with scraper_type filter
and wired into both pages.
Tests: ruff ✓, tsc --noEmit ✓
B1: дефолт months_window 12→24, расширение fallback 24→27 (max архив).
2024Q1+Q2 теперь реально влияют на recommend_mix (raw 784→3815 сделок
для Академического, ×4.9).
B2: per-bucket elasticity через MODE по семантике (is_studio+rooms),
а не PERCENTILE_CONT(0.5) по total_area. ЖК с mix studios+3к больше не
попадают в bucket "1-к" как среднее. Для Академического: 1-к получает
208 точек регрессии (vs 118), slope -1.17.
B3: share_window_months и elasticity_window_months в API scope —
прозрачность для UI tooltip про рассинхрон окон (24mo rosreestr vs
36mo sale_graph).
B4: cad_buildings.floors при INSERT больше НЕ приводится к INT —
NSPD отдаёт диапазоны "1-2", "2-3" которые теряются как NULL. Хранится
как TEXT, аналитика делает CAST через regex.
R1: data/sql/66_indexes_recommend.sql — 4 индекса для горячего пути
recommend_mix (sale_graph composite, kn_objects partial, kn_flats partial,
cad_buildings partial). Применены в проде через MCP.
R3: ON CONFLICT в insert_buildings теперь обновляет ВСЕ изменяемые
поля (purpose, address, floors, geom, registration_date) — раньше при
re-scrape апдейтились только cost/area/year, остальное игнорилось.
Velocity model:
- _active_competitors_count() с каскадным fallback (район+класс → район → регион)
- Per-bucket velocity = bucket_market_v / sqrt(N_competitors) — sqrt-нормировка
как proxy для эффективных конкурентов (power-law)
- Per-bucket market velocities из rosreestr (свой темп каждому сегменту,
поэтому mix-слайдеры теперь двигают total)
- Headline format: f"{months:.1f}" вместо int() (закрывает «за 0 мес»)
- competitors_count + competitors_scope в scope, warning-badge в UI
при rosreestr_fallback, methodology note показывает competitors
Scraper FK violations:
- _ensure_snapshot() ON CONFLICT DO NOTHING — закрыть FK на domrf_snapshots
для domrf_raw_endpoints (Phase A insert до создания snapshot row)
- _ensure_developers() pre-upsert уникальных (dev_id, dev_name) перед
upsert_objects, ON CONFLICT (developer_id) DO NOTHING — закрыть FK
fk_kn_obj_developer
- SAVEPOINT (db.begin_nested) во всех 5 upsert helpers + _log_failure
заменяет db.rollback() — теперь сбой одного row не сносит транзакцию
Bug Bug_Velocity_Mix_Static_OPEN, Bug_Velocity_Unrealistic_OPEN — закрыты
(numbers conservative до полного 442-sweep + sale_graph data fill).
Scraper resume:
- /admin/scrape/queue autoclean уменьшен с 60min started_at до 10min
COALESCE(heartbeat_at, started_at). Не режет валидные long sweeps.
- Phase A пишет heartbeat после каждого objStatus fetch (3min Phase A
больше не рискует быть отмеченной как zombie при новом 10min threshold).
Recommend (Уровень 1 калькулятор):
- POST /api/v1/analytics/recommend/mix с velocity baseline (sale_graph),
price elasticity (regr_slope/r2 на sale_graph, fallback -1.5),
inverse mode (target_months → required price_factor), liquidity score
24mo, headline.
- /analytics/recommend: RecommendVelocityPanel (price slider 0.85..1.15
+ 4 KPI Чек/Срок/Темп/Ликвидность + методология эластичности),
RecommendLiquidityChart (cumulative 0..36 mo с пунктиром на 24).
- BucketsTable: +колонки Темп и Срок, цены/выручка масштабируются по
priceFactor live.
- Все слайдеры и target_months считаются клиентски — никаких round-trip.
Fix: SQLAlchemy не парсит :cls::text — заменено на CAST(:cls AS TEXT).
3 pages (market, PRINZIP drilldown, developers leaderboard) on top of
existing v_developer_full_metrics + domrf_realization views. ECharts on
the frontend, FastAPI router /api/v1/analytics on the backend.