Commit graph

45 commits

Author SHA1 Message Date
lekss361
cce2bf3f3e fix(site-finder): address PR #95 auto-review minor feedback
Backend (cadastre_fetch.py):
1. (race condition) Advisory lock `pg_try_advisory_xact_lock(hashtext(cad_num))`
   обёрнут вокруг шагов "check active job → enqueue" в find_or_enqueue_fetch.
   Lock transaction-scoped, released at COMMIT. Параллельные запросы на тот
   же cad: первый получает lock и enqueue; второй lock=false → re-check
   active job (уже виден после первого COMMIT) → возвращает тот же job_id.
   Docstring обновлён, упоминание SELECT FOR UPDATE удалено.

Backend (parcels.py):
3. (threadpool exhaustion) _INLINE_FETCH_WAIT_S снижен 25 → 15s с подробным
   комментарием: tradeoff про default Starlette anyio threadpool (40 slots)
   и concurrent burst saturation. 15s баланс: НСПД avg 5-15s для quarter,
   ~70% fast path; остальные 30% получают 202 + polling без блока.

Data (87_on_demand_indexes.sql):
2. (missing index) New migration:
   - `nspd_geo_targets_cad_num_idx ON nspd_geo_targets(cad_num)` — для
     find_active/recent_completed_job (existing UNIQUE composite не покрывает
     WHERE cad_num=:c).
   - `nspd_geo_jobs_source_status_idx ON nspd_geo_jobs(source_kind, status)`
     composite для filter auto_on_demand + queued/running.
   Idempotent (CREATE INDEX IF NOT EXISTS), не блокер при текущем размере,
   критично при росте on-demand traffic.

Frontend (useSiteAnalysis.ts):
4. (UI flicker) setFetchingState(null) перемещён ПОСЛЕ `await second
   apiFetch`. Иначе между clear и resolve есть момент когда mutation
   isPending=true + fetchingState=null → пустой экран ~1 RTT.

NOT addressed (rebuttal):
5. (Tailwind convention) — проверил: в проекте нет ни globals.css, ни
   @tailwind directives. ВСЕ существующие site-finder components используют
   inline styles (ConfidenceBadge, GeotechRiskBlock, ScoreBreakdownPanel etc).
   Tailwind в deps но не wired up. Keep inline styles для consistency.
6. (animate-spin) — требует Tailwind globals (см. #5). `<style jsx>`
   keyframes — built-in Next.js, работает.

Per auto-review on 2252236.
2026-05-12 08:58:46 +03:00
lekss361
2252236ce2 feat(site-finder): auto-fetch cadastre geometry on-demand (#93)
Когда пользователь вводит cad-номер которого нет в БД (cad_quarters_geom /
cad_buildings / cad_parcels_geom), вместо 404 «Загрузи через NSPD geo» (dead-
end для non-admin) — теперь backend автоматически инициирует NSPD fetch,
frontend показывает loading state с polling /fetch-status каждые 2с.

Backend:
- Новый модуль app/services/site_finder/cadastre_fetch.py с helpers:
  find_or_enqueue_fetch (atomic check + enqueue с дедупликацией по cad),
  fetch_status (smart polling endpoint — отличает not_in_nspd от failed),
  detect_thematic_id (3-сегм quarter / 4-сегм parcel / 5-сегм building),
  validate_cad_format.
- Reuse: enqueue_geo_job + process_nspd_geo_job (workers/tasks/nspd_geo).
  source_kind='auto_on_demand' отличает от bulk; rate_ms=200, priority=9.
- POST /parcels/{cad}/analyze graceful fallback: inline await до 25s
  (fast path), затем 202 + job_id + eta_seconds для polling, либо
  400/404/503 в зависимости от статуса (с Retry-After 60s на 503).
- GET /parcels/{cad}/fetch-status новый endpoint для polling.

Frontend:
- useSiteAnalysis расширен: fetchingState + cancel(). POST analyze + при 202
  polling каждые 2с (max 60 итераций = 2 мин cap). status=ready → re-trigger
  analyze; not_in_nspd/failed/invalid_format → typed errors.
- apiFetchWithStatus<T> + HTTPError в lib/api.ts — status-aware variant
  для 200 vs 202.
- FetchingState.tsx: animated spinner, progress bar (linear до etaSeconds,
  asymptote после), elapsed counter, cancel button. Светло-голубая
  scheme отличается от обычного pending skeleton.
- site-finder/page.tsx: FetchingState когда fetchingState активен; обычный
  pending skeleton — только при первичном analyze без 202.

Edge cases (per #93 acceptance):
- cad валидный в НСПД, fetch <25s → inline 200 OK
- cad валидный, fetch >25s → 202 + frontend polls → ready → analyze
- cad валидный, не в НСПД → 404 с понятным сообщением + формат hint
- cad invalid format → 400 + формат hint
- NSPD rate-limited / failed → 503 + Retry-After 60s
- Параллельные запросы на тот же cad → один job, оба клиента poll'ят (дедуп
  через find_active_on_demand_job).

Closes #93.
2026-05-12 08:47:22 +03:00
lekss361
f4a865060f
feat(site-finder): D4 pipeline 24mo — future competition (#36) (#90)
* feat(site-finder): D4 pipeline 24mo — future competition (#36)

Backend (parcels.py):
- Запрос к domrf_kn_objects в радиусе 5км с ready_dt BETWEEN NOW() AND NOW()+24mo.
- _aggregate_pipeline() — сводка: objects_count, flats_total, by_class (эконом/
  комфорт/бизнес/...), by_quarter (хронологически, для UI bar), severity
  (low <500 / medium <3000 / high) per spec, top_objects (десятка по flat_count desc).
- Поле analyze.pipeline_24mo. Backward-compat — optional.

Frontend:
- Pipeline24moBlock.tsx — severity badge + 3 summary numbers (объектов, квартир,
  горизонт/радиус), by-class chips, гистограмма bar по кварталам сдачи
  (нормирована на max), разворачиваемый top-N список с классом + датой сдачи.
- Добавлен в MarketTab выше "Market trend".
- TS типы: Pipeline24mo, PipelineObject, PipelineQuarterSlot.

Closes #36. Relates to #19 (Конкурентный 360 — закрыт ранее в #36 scope).

* fix(site-finder): address PR #90 auto-review feedback

Must-fix (3):
1. distance_m falsy guard: `if obj.get("distance_m") is not None` вместо
   `if obj.get("distance_m")` — centroid-on-building даёт 0.0 (falsy float),
   raw Decimal иначе упал бы в JSON serialization.
2. SQL plan note добавлен про seq scan ~3000 строк OK; при росте — нужен
   GIST/index на (latitude, longitude) — отдельный issue для database-expert
   (будет создан separately).
3. obj_class NULL bug помечен в docstring _aggregate_pipeline с reference на
   fixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN. D6/#38 — fix плановый.

Cleanup (3 из 5):
4. CLASS_LABEL.null dead key убран — JSON null приходит as absent key, не
   "null" string.
6. Magic numbers вынесены: PIPELINE_RADIUS_M=5000, PIPELINE_HORIZON_MONTHS=24,
   PIPELINE_SEVERITY_MEDIUM_THRESHOLD=500, PIPELINE_SEVERITY_HIGH_THRESHOLD=3000,
   PIPELINE_TOP_OBJECTS_LIMIT=10. SQL query теперь через f-string подставляет
   их (защищённое от injection — это int литералы).
8. obj.ready_dt formatting через fmtMonth() с new Date + toLocaleDateString —
   robust к datetime suffix vs date-only, fallback к substring(0,7) при NaN.

Не сделано (defer):
5. Async 3 HTTP calls (pre-existing pattern, нужен ThreadPoolExecutor refactor
   отдельным PR — затрагивает weather/air_quality fetch architecture).
7. ST_GeomFromText дважды — CSE справляется на этом масштабе.

Per auto-review on ade511b.

* fix(site-finder): address PR #90 auto-review minor feedback

1. TS PipelineObject.distance_m — `number | null` для отражения defensive
   Python guard (`if obj.get("distance_m") is not None`). Comment объясняет
   почему.
2. Pipeline SQL: `text(f"...")` → `text("...")` + parameters. radius_m и
   horizon_months через `:param` placeholders + `cast(:horizon_months || ' months'
   AS interval)`. Consistency с остальными SQL в файле, plus защита от
   accidental injection при будущих изменениях.
3. top_objects: explicit field selection вместо `dict(r) for r in rows`.
   Раньше leak'ило все колонки из CTE `SELECT *` (latitude/longitude/
   snapshot_date/region_cd/dev_id) в API response. Теперь только nominated
   fields: obj_id, comm_name, dev_name, obj_class, flat_count, ready_dt,
   distance_m. Schema clean.

Per auto-review on 4e431bf.

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-12 08:34:02 +03:00
lekss361
1f8fd77825
feat(site-finder): X1 score breakdown + verbal explain (#47) (#92)
Backend (parcels.py):
- POI scoring loop teper строит score_breakdown_detailed: per-factor list с
  verbal explain (через _verbal_for_poi helper) и группировкой
  (_POI_GROUP: Социалка / Торговля / Парки / Транспорт / Шум/трамвай / Локация).
- center_bonus добавлен как synthetic factor группы "Локация" с weight=1.0
  (decay не применяется — bonus IS the value).
- factor key включает enumerate idx — prevents React key collision когда
  два POI одной категории совпадают по округлённому расстоянию.
- Skip факторов с |contribution| < 0.01 (POI > 1км) — UI шуму не нужен.
- abs_total fallback на 1.0 — защита от division-by-zero для empty factors.
- Top-3 positives/negatives: explicit ascending sort для негативов
  ("most-negative first" очевидно из кода).
- score_by_group: stacked-bar данные с count + contribution_pct.
- group_totals type: dict[str, dict[str, float | int]] (count это int).

Frontend:
- Новый ScoreBreakdownPanel.tsx: stacked-bar по группам с tooltip + legend,
  топ-3 плюса (▲ зелёный) / топ-3 минуса (▼ красный) с verbal, отдельная
  строка "Снижают балл — Шум/трамвай: ..." для negative groups, разворачиваемая
  таблица всех факторов (sticky thead, scrollable).
- Интегрирован в OverviewTab под секцией "Балл".
- TS типы: FactorContribution, ScoreGroupTotal.

Closes #47.

NB: branch создан заново из-за rebase mess (см. PR #87 comments). Логически
эквивалентно но history clean.

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-12 08:17:00 +03:00
lekss361
09611dc690
feat(site-finder): P1 geometry suitability score (#45) (#89)
* feat(site-finder): P1 geometry suitability score (#45)

Backend (parcels.py):
- _polygon_suitability() — Shapely metrics on parcel WGS84 polygon:
  area_ha/m2 (projected via cos(lat) к UTM-like meters), perimeter_m,
  aspect_ratio (long/short side of MABR), convex_hull_ratio (area / hull area),
  min_inscribed_rect_dim_m.
- Composite suitability score 0..1:
  base = area_subscore (0 при <0.2 ha, linear → 1 при ≥0.5 ha)
  −0.3 если aspect_ratio > 5 (вытянутый)
  −0.3 если convex_hull_ratio < 0.65 (изрезанный)
  −0.5 если min_inscribed_rect_dim_m < 30 (узкий)
- Label: микро / подходящий / сложная форма / слабо подходит
- Recommendation: "для МКД 16+ нужно >0.3 ha + минимум 40м"
- В analyze response → geometry_suitability.

Frontend:
- Новый GeometrySuitabilityBlock.tsx с color-coded badge + метрики grid +
  penalties + recommendation
- Добавлен в LandTab (выше "Зонирование (ПЗЗ)")
- TS типы расширены: GeometrySuitability

Closes #45.

* fix(site-finder): address PR #89 auto-review minor feedback

Backend (parcels.py):
1. Фактическая ошибка фикс: recommendation теперь различает "строительный
   минимум 30м" (physical penalty trigger) и "комфорт МКД 12-16эт 40м"
   (recommendation level). Пользователю чётко видны 2 порога.
2. Lazy imports → module-level: `from shapely import wkt as _shp_wkt`,
   `from shapely.geometry import Polygon`.
3. MABR inner exception теперь логирует: `logger.debug("MABR computation
   failed, falling back to sqrt(area): %s", mabr_err)`.
4. Magic numbers вынесены в константы: _GEOM_MIN_AREA_HA, _GEOM_AREA_SCORE_FULL_HA,
   _GEOM_ASPECT_PENALTY_THRESHOLD, _GEOM_CONVEX_PENALTY_THRESHOLD,
   _GEOM_MIN_WIDTH_PHYSICAL_M, _GEOM_MIN_WIDTH_COMFORT_M, _GEOM_LABEL_MICRO_HA,
   _GEOM_LABEL_GOOD, _GEOM_LABEL_MEDIUM, плюс penalty константы.
5. Label "микро" теперь комбинируется с penalties: "микро, узкий" — UI видит
   обе проблемы. Pure "микро" остаётся когда нет penalty.

Frontend (GeometrySuitabilityBlock.tsx + types):
6. Заменил emoji ⚠ (U+26A0) на текстовый <strong>"Проблемы формы:"</strong> —
   стабильнее в WeasyPrint PDF-экспорте и cross-platform.
- TS типы: GeometrySuitabilityBaseLabel экспортирован, label расширен до
  `string` для допуска combo-labels; helper colorForLabel() парсит base часть.

Per auto-review on f4e7491.

* fix(site-finder): P1 #45 — extract 0.3 ha to _GEOM_AREA_COMFORT_HA

Auto-review нашёл: recommendation упоминала "от 0.3 га" но в constants block
не было 0.3 — magic literal в строке. Тот же класс проблемы что 30/40м inconsistency
из прошлого review, только в другом поле.

Fix: новая константа `_GEOM_AREA_COMFORT_HA = 0.3` с комментарием
"рекомендуемая комфортная площадь МКД (recommendation)". Размещена между
_GEOM_MIN_AREA_HA (физический минимум) и _GEOM_AREA_SCORE_FULL_HA (premium) —
третий semantic threshold. Recommendation теперь f-string использует константу.

Per auto-review on ae5e8de.

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-12 08:00:12 +03:00
lekss361
843314c040
feat(site-finder): P2 cad_buildings соседи + overlap check (#46) (#91)
* feat(site-finder): P2 cad_buildings соседи + overlap check (#46)

Backend (parcels.py):
- _parse_floors() helper для TEXT column (cad_buildings.floors хранится как
  строка, могут быть диапазоны "5-7"). Возвращает верхнюю границу.
- _neighbors_summary(db, geom_wkt, our_cad) — query соседей в 100м (GIST):
  cad_num, building_name, floors, year_built, cost_value, area, address, distance.
  Aggregate: avg_floors_100m, max_floors_100m, median_cost_per_m2_100m,
  count_buildings_100m. Outliers cost/m² фильтруются (1k < x < 500k).
- Overlap check: ST_Intersects + ST_Area(ST_Intersection) > 50 m² (transformed
  to UTM 32641 для метров). Если есть → has_existing_buildings: true +
  overlap_buildings list.
- В response → neighbors_summary.

Frontend:
- Новый NeighborsBlock.tsx: hard red warn block для overlap (с building names +
  overlap_m2 + "Инвестиции невозможны без сноса"); summary metrics (avg/max
  floors, median price); toggle "Показать N ближайших" → таблица.
- Border меняется на красный при has_existing_buildings — visual cue.
- Добавлен в LandTab выше "Зонирование (ПЗЗ)".
- TS типы: NeighborBuilding, OverlapBuilding, NeighborsSummary.

Closes #46. Closes #21 (cad_buildings в Site Finder фильтрах).

* fix(site-finder): address PR #91 auto-review minor feedback

Backend (parcels.py):
1. (medium) Aggregation loop _neighbors_summary теперь обёрнут в try/except
   (ValueError, TypeError) с fallback к data_available:False + log warning.
   Защищает от non-numeric cost_value/area придёт в строке (e.g. "N/A") —
   ранее весь endpoint падал 500.
2. Magic numbers вынесены: _COST_PER_M2_MIN=1000, _COST_PER_M2_MAX=500_000.
3. _parse_floors docstring + inline note про malformed parts ("5а-7" filter,
   multi-range "1-2-3" max acceptable degradation).

Frontend (NeighborsBlock.tsx):
5. Русский plural fix: pluralBuildings(n) helper — 1 здание, 2-4 здания,
   5+/11-14 зданий. Раньше "3 зданий" — теперь "3 здания".

Не сделано (defer):
4. ST_Area для overlap query — практически 0-5 buildings в ЕКБ, GIST scan OK.
6. Discriminated union для NeighborsSummary — refactor а не bug.
7. Vault entry для P2 — добавится batch'ем после merge всех текущих PR.

Per auto-review on 60d53bb.

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-12 01:30:53 +03:00
lekss361
3777a77a42
feat(site-finder): X2 confidence indicator + caveats (#48) (#88)
Backend (parcels.py):
- _compute_confidence() composite score 0..1 from 7 subscores: poi_freshness,
  geom_source (parcel vs quarter), district, market_trend (rosreestr_deals depth),
  competitors, environment (noise/air/weather availability), zoning (placeholder
  до G1).
- confidence_label: high (>0.75) / medium (0.4-0.75) / low (<0.4)
- confidence_caveats: list of конкретных проблем для UI
- confidence_breakdown: per-subscore 0..1 для прозрачности

Это stub-версия (полная — после G1/G2/D1/D2). Использует только текущие сигналы.

Frontend:
- Новый ConfidenceBadge.tsx — color-coded (green/yellow/red) badge с %
- Caveats для low — показываются сразу; для medium/high — под toggle
- Toggle "Подробнее" → breakdown per-subscore + полный список caveats
- Размещён в начале OverviewTab (выше "Район")
- TS типы расширены: confidence, confidence_label, confidence_breakdown, confidence_caveats

Closes #48.

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-12 01:04:25 +03:00
lekss361
2cbcea9e73 fix(zoning): NSPD blocks bot access — fallback to deep-links (PKK6 API closed 2024) 2026-05-11 22:56:50 +03:00
lekss361
ab0647e4d5 feat(site-finder): distance to EKB center + success quartirography recommendation 2026-05-11 22:35:21 +03:00
lekss361
8be539ddc6 feat(site-finder): ПЗЗ territorial zones from Rosreestr PKK6 + zoning in analyze 2026-05-11 21:51:51 +03:00
lekss361
923f250926 feat(site-finder): /parcels/{cad}/isochrones via OpenRouteService 2026-05-11 21:44:22 +03:00
lekss361
6ce634a9b9 feat(site-finder): utilities (power/pipeline) + fix Внешние факторы layout 2026-05-11 21:38:20 +03:00
lekss361
2855a01ca7 feat(site-finder): v3.5 - seasonal weather + hydrology + geotech risk 2026-05-11 21:32:07 +03:00
lekss361
7bb51aa838 feat(site-finder): v3.4 - weather enrichment (temp/precip/UV) + geology stub 2026-05-11 21:17:57 +03:00
lekss361
7900dc5238 fix(site-finder): WKT LINESTRING needs commas + rosreestr_deals real column names 2026-05-11 20:58:49 +03:00
lekss361
232c81eae9 feat(site-finder): v3.3 - score label + market_trend + multi-thematic bulk 2026-05-11 20:51:59 +03:00
lekss361
1e9d32ee3c feat(site-finder): v3.2 — noise + air quality + wind analytics 2026-05-11 20:42:37 +03:00
lekss361
f419900968 feat(site-finder): v3.1 — cad_parcels_geom, analyze fallback, POI lat/lon, OSM expand
- nspd_geo: add _save_parcel() for thematic_id=1 → cad_parcels_geom (UPSERT,
  ST_Transform from Web Mercator); _persist_target now handles 1/2/5
- parcels.py: analyze endpoint geom lookup extended with cad_parcels_geom as
  3rd fallback source (after cad_quarters_geom, cad_buildings); both SELECT
  and WKT subqueries updated
- parcels.py: POI score_breakdown items now include lat/lon for map markers
- poi_loader: OSM_CATEGORIES expanded — college+university→school,
  hypermarket→shop_supermarket; coverage +3 tag pairs
2026-05-11 19:52:19 +03:00
lekss361
a7d7dc9667 fix(site-finder): dedupe competitors — same domrf_kn_objects 3x snapshots bug 2026-05-11 19:04:29 +03:00
lekss361
f81bd710c0 feat(api): POST /admin/scrape/poi-sync — manual OSM POI sync trigger 2026-05-11 18:12:37 +03:00
lekss361
a4e8fff5ac feat(site-finder): POST /parcels/{cad}/analyze endpoint + UI
Backend:
- analyze endpoint: cad → geom lookup → POI within 1km → district
  context → competitors within 3km. Tram_stop carries negative weight.

Frontend /site-finder:
- CadInput regex-validated (default 66:41:0204016:10)
- SiteMap (Leaflet via next/dynamic) with parcel polygon + legend
- ScoreCard color-coded (>5 green, 2-5 yellow, <2 red) with tram-warning
- CompetitorTable top-20 with same-district highlight
- useSiteAnalysis hook + TS types
2026-05-11 18:11:58 +03:00
lekss361
43672a0068 fix(api): bulk geo includes all doc_types by default (only_ddu opt-in) 2026-05-11 16:12:58 +03:00
lekss361
fe2a881cec feat(api,analytics): bulk geo backfill + complex_buildings query
- POST /api/v1/admin/scrape/geo/bulk — splits pending Sverdlovsk cad-nums
  into N chunks (parallelism 1..10, default 5), creates N jobs and enqueues
  each in queue=geo. source_kind='rosreestr_pending_chunk' for tracking.
- analytics_queries.complex_buildings(db, obj_id) — returns list of buildings
  from cad_buildings (cad_num, floors, area, purpose, name, address, geom).
- object_detail: LEFT JOIN v_complex_buildings, adds buildings_count.
- top_developers: adds complexes_count via correlated subquery.
- GET /api/v1/analytics/object/{obj_id}/buildings → list[ComplexBuilding].
2026-05-11 15:56:17 +03:00
lekss361
2fee7739e8 feat(jobs): centralized job_settings API + DB-driven beat schedule
- JobSetting ORM model (JSONB extra_config) + Pydantic schemas
- GET/PUT /api/v1/admin/jobs/settings + /{job_type} (X-Admin-Token auth)
- celery_app.py builds beat_schedule from job_settings DB (env fallback
  retained for safety on first deploy / DB unreachable)
- nspd_geo task reads rate_ms from job_settings when per-job row has
  no override
- enqueue/resume geo jobs route to queue_name from job_settings
- Worker container: --queues=celery,scrape_kn,geo (one container,
  three named queues — kn sweep no longer blocks nspd_geo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:38:13 +03:00
lekss361
3919a40c49 refactor(nspd-geo): unify on rosreestr2coord v5, drop legacy column
Объединяю несколько связанных изменений вокруг 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.
2026-05-11 14:13:33 +03:00
lekss361
79e2e94e09 refactor(nspd): remove Playwright-based scraper, rosreestr2coord = default
УДАЛЕНО (~1700 строк):
* backend/app/services/scrapers/nspd_kn.py (Playwright + WAF-bypass)
* backend/app/workers/tasks/scrape_nspd.py
* frontend/src/app/admin/scrape/nspd/page.tsx
* 5 NSPD admin endpoints (POST /nspd, /nspd/release-lock,
  GET /nspd/runs, /logs, /coverage)
* NSPD beat schedule + worker_ready resume hook + nav tab + link
* _nspd_default_regions() helper

СОХРАНЕНО (история):
* nspd_scrape_runs / nspd_scrape_log таблицы (3 row + log) для аудита
* UNION ALL в v_scrape_runs_unified / v_scrape_log_unified —
  старые runs видны под фильтром scraper_type='nspd'
* settings scrape_nspd_* помечены DEPRECATED

CHANGED:
* nspd_geo_jobs.use_rosreestr2coord DEFAULT FALSE → TRUE
* UI checkbox "rosreestr2coord lib" по умолчанию = checked
* /admin/scrape/all + /admin/scrape/geo — единственный путь к NSPD-данным

NSPD-доступ теперь только через rosreestr2coord (community lib) —
upstream поддерживает обновления headers/WAF-tricks. Локальный
nspd_lite.py (urllib) остаётся как fallback (use_rosreestr2coord=FALSE).
2026-05-11 09:21:12 +03:00
lekss361
54dbc77348 feat(geo): NSPD bulk-fetcher без Playwright + resume-friendly UI
ОТКРЫТИЕ: stdlib urllib проходит WAF nspd.gov.ru (TLS-fingerprint stdlib
отличается от mainstream HTTP-clients). Это позволяет полностью убрать
Playwright + Chromium для NSPD-задач.

* nspd_lite.py: urllib + ssl._create_unverified_context(). 4 публичных
  fetcher'а (geoportal/quarter/parcel/building) + fetch_via_rosreestr2coord
  fallback на community-lib

* schema 77: nspd_geo_jobs (журнал) + nspd_geo_targets (cad-номера со
  статусом pending/done/failed). Resume-state в БД.

* tasks/nspd_geo.py: Celery task process_nspd_geo_job(job_id). Heartbeat
  каждые 5 items, WAF backoff 30s × 2^N (max 8 → paused). UPSERT в
  cad_quarters_geom / cad_buildings → идемпотентно.

* worker_ready hook: stale jobs (>10min без heartbeat) автоматически
  re-enqueue после redeploy. Никаких потерь прогресса.

* 4 admin endpoint + UI /admin/scrape/geo с формой запуска (manual_list /
  rosreestr_pending для авто-наполнения из ДДУ-кварталов), progress-bars
  и cancel/resume кнопками per job.

* settings: use_nspd_lite=True, nspd_lite_rate_ms=600.
* dep: rosreestr2coord>=4.0.0 (community lib для fallback).

TODO следующих шагов:
- smoke-test на проде через SSH (validation RU-IP не банит urllib)
- feature toggle USE_NSPD_LITE в scrape_nspd.py для переключения с старого
  Playwright-пути
- docker lean image (-300 MB после убирания Chromium)
- расширение SCRAPE_NSPD_DEFAULT_REGIONS на 66,74,72,59 (Челябинск/Тюмень/Пермь)
2026-05-11 08:53:28 +03:00
lekss361
52bcf9d30c feat(db): canonical complexes + unified views + naming consistency
* Task 2+3: complexes.obj_class и district_name из Objective
  (350 ЖК с классом, 1525 с district; objective приоритетнее)

* Task 1 (fuzzy match orphan Objective ↔ domrf_kn):
  - pg_trgm extension
  - auto-merge 14 high-confidence (similarity ≥ 0.85)
  - v_complex_match_review для остальных 82 кандидатов
  - complexes 1752 → 1740 · cross-source 114 → 124

* Task 4 (naming unification без RENAME COLUMN):
  - GENERATED ALWAYS AS (...) STORED для синонимов
  - region_id в 15 таблиц + 9 partitions
  - developer_id в 7, developer_name в 5
  - COMMENT 'DEPRECATED' на region_cd/region_code/dev_id/dev_name

* Task 5 (unified scrape dashboard):
  - GET /api/v1/admin/scrape/all/runs + /all/logs (поверх v_scrape_runs_unified)
  - new page /admin/scrape/all с filter табами + 4 stat-плитки
  - +tab «📊 Все скраперы» в admin layout

Файлы: data/sql/75_unification_naming.sql · backend/app/api/v1/admin_scrape.py
       · frontend/src/app/admin/scrape/all/page.tsx · admin/layout.tsx
2026-05-10 21:11:20 +03:00
lekss361
7c05d0a0d8 feat(objective): full sync pipeline + dynamic admin config
Objective API (api.objctv.ru) интегрирован как новый source of truth для
per-flat данных по новостройкам УрФО. Заменяет промежуточный Anton-SQLite
(legacy bootstrap-ETL остался как fallback в свёрнутом блоке UI).

Schema (data/sql/68_v2 — applied на проде, 6 таблиц):
- objective_lots          (UPSERT по lot_id; 303 677 rows)
- objective_corpus_room_month (long-формат месяц×корпус×room_bucket; 19 738 rows)
- objective_lots_history  (append-only weekly snapshots для elasticity)
- objective_complex_mapping (Objective ComplexName ↔ domrf_kn_objects.obj_id)
- objective_raw_reports   (jsonb страховка на смену схемы API)
- objective_scrape_runs   (журнал прогонов)
+ data/sql/72_objective_sync_config (single-row динамический конфиг)

Backend:
- services/scrapers/objective.py: ObjectiveClient — Bearer-токен (Redis +
  in-memory fallback), retry на 401/429/5xx, Retry-After header support
- services/objective_etl.py: ETL SQLite Антона → PG (legacy)
- services/objective_sync_config.py: read/update single-row config
- workers/tasks/scrape_objective.py:
  * sync_objective_group: 2 рабочих отчёта (corp_sum, lots_pf), inline-парсинг
  * sync_all_groups: wrapper, перебирает группы из БД-конфига с
    inter-group паузой; PATCH-merge explicit args > DB config
- workers/tasks/objective_etl.py: Celery task для legacy bootstrap
- workers/celery_app.py: beat читает cron из БД при старте (fallback на env)
- api/v1/admin_scrape.py: 5 новых endpoints для /objective/*

Frontend (frontend/src/app/admin/scrape/objective/page.tsx):
- PRIMARY blue блок «🌐 Наш sync» с input для override групп
- Collapsible «⚙️ Настройки» с формой (cron + 8 параметров) → PUT в БД
- Coverage-панель с PG counts + строка про SQLite Антона как legacy
- Collapsible «🛠 Bootstrap ETL» — legacy-инструмент

Beat schedule: вторник 06:00 МСК, ~10-15 мин на 4 группы (Свердл.обл +
Челябинск + Тюмень + Пермь = ~700K квартир УрФО). Расписание и параметры
меняются через админку без редеплоя (cron требует restart beat).

Эмпирические находки об API (probe 2026-05-10):
- 13 из 21 проверенных group_name доступны на тарифе (включая «Свердловская
  область», «Челябинск», «Тюмень», «Пермь», но НЕ «Свердловская обл» —
  формат имени строгий)
- ComplexName требует БЕЗ префикса «ЖК» и БЕЗ кавычек
- Поле «Банк» для всех 303k = NULL (тариф не отдаёт), но «Тип обременения»
  работает (36% строк = ипотека) → ipoteka_share возможен, банковская
  атрибуция — нет

Docker:
- bind-mount /opt/gendesign/site-finder:/data/anton-sqlite:ro в worker

GitHub backlog: добавлены #22-25 (recommend_mix v3 — 4 сабтаска по
улучшению алгоритма на основе фидбэка про POI / границы районов /
конкурентов / окно данных / success-driven mix).

Knowledge graph (memory/memory-gendesign.jsonl): обновлены entities
Objective_Integration_May07_2026, Schema_Objective_v2_May07,
Objective_API_Findings_May07, Module_Objective_Client + новый
Session_End_May07_2026.

TODO для прода: прописать OBJECTIVE_API_KEY=<key> в backend/.env +
docker compose restart worker beat.
2026-05-10 19:54:15 +03:00
lekss361
f80a2c1d05 fix(scrape): SQLAlchemy text() парсил ':00:' regex как named-param
В nspd_kn.py:get_pending_cads и admin_scrape.py:nspd_coverage regex-фильтры
quarter_cad_number !~ '^00:00:' и !~ ':0000000$' падали с psycopg.errors.
DataError — двоеточие в text() трактуется как ':param'. Заменил на NOT LIKE
с bindparams (быстрее regex и без issue с парсингом).

Эффект на проде:
- /api/v1/admin/scrape/nspd/coverage возвращал 500 → теперь 200
- Кнопка «Запустить sweep» больше не дизейбленa (pending=964 теперь видно)
- Сам scraper тоже бы валился на этом же баге при beat-запуске
2026-04-30 23:16:11 +03:00
lekss361
78b8d7aa00 add NSPD 2026-04-30 22:23:18 +03:00
lekss361
d1f2380f32 fix(sqlalchemy): убрать :name::type — psycopg3 их не парсит, ломает Phase A INSERT в kn_scrape_runs (run_region_sweep крашится сразу с ProgrammingError, task_received записан, но run_id никогда не создаётся). 4 места: domrf_kn.py (resume INSERT, new run INSERT, snapshot UPDATE) + admin_leads.py (date_to filter). Заменено на CAST(:name AS type). 2026-04-28 23:37:03 +03:00
lekss361
c3bc96535d fix(scraper): диагностируем «кнопка не работает» — Redis lock + task_received log
Симптом: POST /admin/scrape/kn возвращает task_id, но run_id не появляется
в kn_scrape_runs. Причина: зомби-worker оставил Redis-lock с TTL=2h, новые
задачи скипаются с reason='lock_held' молча.

Backend:
- _LOCK_TTL_SECONDS 7200 → 1800 (full sweep ~25-30 мин — потолок).
- force_release_lock() helper + новый POST /api/v1/admin/scrape/release-lock.
- TriggerKnRequest +force: bool — при true перед apply_async удалить lock.
- _log_task_received() / _log_task_skipped() — kn_scrape_log пишет «task
  подхвачен/skip» с run_id=NULL до основного flow. Теперь UI показывает
  whether worker реально получил задачу.

Frontend:
- Чекбокс  Force в форме запуска.
- Кнопка 🔓 Снять Redis-lock (отдельный endpoint).
- Уведомление о результате release.
2026-04-28 23:17:42 +03:00
lekss361
7a23aa9edc fix: heartbeat-based zombie autoclean (10min) + Phase A heartbeats; ship recommend velocity calculator
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).
2026-04-28 22:54:03 +03:00
lekss361
ecc0dbafd5 add predict 2026-04-27 21:28:02 +03:00
lekss361
12b1eb8169 add logs 2026-04-27 20:51:44 +03:00
lekss361
a56216aeb9 fix ui 2026-04-27 20:42:18 +03:00
lekss361
b3c15bb8ea fix ui lag 2026-04-27 20:26:55 +03:00
lekss361
5056438fcf add anal4 2026-04-27 19:45:47 +03:00
lekss361
522ecca5c4 ship #13-16: multi-target Dockerfile, per-object drill-in, CRM pipeline,
442-sweep instructions
#16 Dockerfile: runner (lean, 1.6 GB) + runner-with-chromium (2.9 GB).
   Compose + deploy.yml pushes 2 GHCR images. Backend saves 1.3 GB.
#15 5 backend object endpoints + /analytics/objects/[id] page (gallery,
   POI table, sale chart) + sparkline-driven PrinzipObjectsTable.
#14 prinzip_leads/deals + 2 MVs. Import script with PII-hashing &
   --inspect-only mode. 3 funnel endpoints + Sankey/Monthly charts.
#13 README section with UI/CLI/SQL for 442-object sweep.
2026-04-27 19:32:37 +03:00
lekss361
7c4197000f add monitor scrappers 2026-04-27 19:04:01 +03:00
lekss361
6d3fa8cbd2 add domrf extras parser (sale_graph + sales_agg + infra + photos) +
fix Docker build for playwright

- Regenerate uv.lock with playwright + tenacity (was missing → uv sync
  --frozen skipped them, hence playwright not in /app/.venv/bin/).
- Dockerfile: explicit Chromium runtime libs in apt (libnss3 + 14 more)
  + `playwright install chromium` BEFORE apt-lists cleanup.
- Schema 51_schema_kn_extras.sql: 5 new tables for time-series sales,
  current aggregates, POI infrastructure, photo metadata, failure log.
- Scraper: 4 new fetch helpers + upserts; photo binary download via
  Playwright APIRequest; failure-logging with full_url to kn_scrape_failures.
- CLI: --no-extras / --download-photos / --no-flats / --probe URL.
- Admin UI: extras checkboxes + failures table with copy-URL button.
- PRINZIP smoke: 28 objs, 5409 POI, 5150 photos metadata, sales_agg
  matches probe (Парк Победы 145/291 = 49%).
2026-04-27 18:35:46 +03:00
lekss361
79070bfe99 add interactive parser of наш.дом.рф ЖК+flats with stealth Playwright
- Discovered real endpoints via chrome-devtools: /сервисы/api/kn/object
  (offset/limit/place/objStatus) + /portal-kn/api/sales/portal/table.
  Server-side filter ignored — local filter by developer.companyGroup.
- Pure Playwright in-context fetch (httpx blocked by ServicePipe TLS-fp).
- Saved fingerprint state in data/playwright_state.json for server reuse.
- Celery beat (configurable cron + jitter) + admin trigger endpoint
  + /admin/scrape UI page.
- Schema migration: UNIQUE(id, snapshot_date) for versioned snapshots,
  kn_scrape_runs journal.
- Smoke-tested on PRINZIP: 28 ЖК / 826 квартир as expected.
2026-04-27 17:58:40 +03:00
lekss361
8d3a0874ef add interactive analytics dashboard for Sverdlovsk market and PRINZIP
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.
2026-04-27 16:55:30 +03:00
lekss361
b95559eac9 init 2026-04-25 13:45:19 +03:00