Commit graph

63 commits

Author SHA1 Message Date
67b65c8d8a feat(site-finder): bridge финмодели (DCF) в кокпит Investment Clearance (epic #1881 PR-4)
All checks were successful
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Successful in 9m54s
Investment Clearance в site-finder кокпите больше не «—»: синтезируем лёгкий
ТЭП из buildability участка (площадь + предельные параметры зоны НСПД/ПЗЗ —
КСИТ/%застройки/этажность) → прогоняем через полноценную финмодель (каскад
затрат PR-1 + калибр.цена PR-2 + помесячный DCF PR-3) → GDV/Cost/Profit/ROI/
IRR/NPV/PBP в кокпите.

Backend:
- new app/services/site_finder/parcel_financial.py (ЧИСТЫЕ функции, без БД):
  synthesize_teap_from_buildability (GFA=area×max_far, degradation pct+floors;
  residential=GFA×eff; apartments/parking — нормативы teap.py, не дублируются)
  + synthesize_parcel_financial (housing_class из цены, development_type из
  этажей, land_cost=кадастровая). None когда нельзя строить МКД / нет зонинга /
  нет площади.
- parcels.py: 1 вызов в try/except (hot-path-safe → None при сбое) + ключ
  financial_estimate в /analyze. Схема не тронута (AnalyzeResponse extra=allow,
  codegen не нужен). +18 тестов.

Frontend:
- ParcelFinancialEstimate тип (site-finder.ts, hand-typed).
- adaptInvestmentClearance(financial)/adaptFinanceDrawer(financial): реальные
  числа (GDV/Cost/Profit/ROI/IRR + NPV + срок окуп.) или честный placeholder
  когда null. Локализация класса/типа (high_rise→высотная). Прокинут analysis
  через PticaBottomGrid/drawer-registry. +6 тестов, 144 passed.

HEAVY caveat везде: ОРИЕНТИРОВОЧНАЯ модель по МАКС.застройке НСПД, НЕ реальная
концепция; land=кадастровая (не рыночная); график — типовой; IRR proxy-флаг.
mypy strict clean (generative.*), ruff/tsc/lint clean.

Закрывает эпик #1881 (финмодель = полноценная DCF, видна в кокпите).
Follow-up: финансирование (кредит/займы), §22 sales-pace, гео-радиус калибровки.

Refs #1881
2026-06-23 22:44:04 +05:00
adf3ec7c00 fix(site-finder): velocity fallback 4500→750, coverage/proxy disclosure, market_trend staleness honesty (audit #1871 P2)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 1m5s
CI / openapi-codegen-check (pull_request) Successful in 1m59s
CI / backend-tests (pull_request) Successful in 10m9s
Три honesty/correctness-фикса из audit-issue #1871 (🟠 P2). Все факты
подтверждены prod-DB + разведкой. ETL не трогаем (отдельная задача).

velocity.py:
- _EKB_MEDIAN_FALLBACK 4500→750 (audit-verified медиана ЕКБ 593-766;
  4500 завышало знаменатель нормализации 7.6× → занижало velocity_score
  при DB-error fallback, latent). Консервативно 750 (верх диапазона).
- VelocityResult + as_dict() раскрывают: objective_coverage_pct,
  proxy_used, proxy_sqm_per_deal — теперь видно что объём rosreestr-пути
  фабрикуется count×45 м²/сделка при низком покрытии Objective (<50%).
  Проброшено во все 4 конструктора, additive.

parcels.py (market_trend producer):
- Различать «источник устарел» от «нет сделок». rosreestr_deals
  MAX(period_start_date)=2026-01-01 (stale 5.5 мес) → recent-окно пусто →
  раньше голый null. Теперь status ∈ {ok, source_stale, no_deals} +
  as_of_date + label + recent_deals_count. MAX(deal_date) добавлен в CTE.
  Consumer scoring читает recent_deals_count защищённо (без регрессии).

frontend (types + MarketTrendBlock + VelocityBlock + ptica market-drawer
+ Section3 verdict + legacy guard):
- staleness/no_deals → честный caveat вместо фейкового тренда.
- velocity → caption «объём оценочный (прокси N м²/сделка, покрытие P%)».
- formatAsOfMonth парсит YYYY-MM напрямую (TZ-safe).
- Типы MarketTrend/Velocity расширены (optional, backward-compat;
  отсутствие status = "ok").

Тесты: +3 velocity unit + extended as_dict contract. 17 velocity passed,
138 frontend vitest passed.

Refs #1871
2026-06-23 12:04:08 +05:00
dcccebf2c2 fix(forecasting): honest scenarios collapse when β rate-sensitivity gate fails (audit #1871 P1)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / frontend-tests (pull_request) Successful in 1m17s
CI / openapi-codegen-check (pull_request) Successful in 2m11s
CI / backend-tests (pull_request) Successful in 10m19s
Когда demand_normalization honesty-gate отбивает β rate-sensitivity
(`sensitivity.confidence=='low'`, ЕКБ-регрессия не проходит n≥30/R²≥0.1/slope<0),
все 393 прод-отчёта получают coefficient=1.0 для всех трёх сценариев
conservative/base/aggressive. По дизайну backend честно деградирует, но фронт
рисует 3 разноцветные карточки/линии одинаковых чисел без caveat.

Backend (scenarios.py, report.py, report_assembler.py, orchestrator.py):
- compute_scenarios теперь возвращает (list, collapsed: bool, reason: str|None)
- _detect_collapsed(): math.isclose(rel_tol=1e-9, abs_tol=1e-6) сравнивает
  projected_demand_units И deficit_index на всех горизонтах между
  conservative и aggressive — расхождение в любой метрике на любом h → False
- _COLLAPSE_REASON_LOW_BETA — единственный источник истины каноничного
  русского предложения
- ReportScenarios получает поля scenarios_collapsed + scenarios_collapse_reason
- demand_normalization.py НЕ ТРОНУТ — поведение корректно по контракту

Backend экспортёры (report_pdf.py, excel.py):
- PDF: при collapse — тёмная плашка-параграф вместо таблицы трёх столбцов
- Excel: ОДНА строка «base» + caveat-ячейка вместо трёх идентичных столбцов

Frontend (forecast.ts, ScenariosBlock, ScenarioCards, ScenarioCompareTable,
ForecastChart, Section6Forecast, ptica.module.css):
- Тип ReportScenarios расширен двумя optional-полями
- ScenariosBlock (light): headline-bar + одна grid-карточка base + caveat
- ScenarioCards (ptica dark): sub-component ScenarioBaseCard, одна карточка
  + collapse-note, ScenarioCompareTable return null
- ForecastChart: effectiveScenarios filter — одна линия viz-1 «Базовый»
  вместо трёх перекрывающихся
- Section6Forecast: caveat выше графика, читается ТОЛЬКО из
  scenarios_collapse_reason (источник истины — backend constant)

Тесты: +18 новых (TestMetricsEqual×6, TestDetectCollapsed×7,
TestComputeScenariosCollapseDetection×4 + 1 проверка graceful + corner).
102 forecasting passed, 88 cross-module passed.

Refs #1871
2026-06-22 19:06:22 +05:00
8c43410ad2 feat(ptica): live 3D massing in Инсоляция card + honest Recommended Product verdict
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m59s
- MassingScene: preview prop — viewport-only, hands-off autorotate (OrbitControls
  input disabled, autoRotate on), canvas pointer-events:none; teardown preserved.
- InsolationCard: replaces the abstract SVG cube with a live autorotating massing
  built from real parcel area + НСПД max_far (farIsReal). «Открыть 3D» still opens
  the full interactive drawer.
- RecommendedProductCard: degenerate mix no longer shows «сигнал недоступен» —
  it shows the real advisory verdict «Избегать» + reason (затоварка · индекс
  дефицита из mix.deficit_index) + product_tz.gate_caveat. Meaningful mixes still
  render quartirography bars. «Подробнее» → product drawer (full reasons).
- drop dead iso-cube CSS (pticaIsoSpin/.isoBoxPreview/.isoPreview*); add
  .massingPreviewViewport + verdict styles (tokens, dark-scoped).
- forecast type: product_tz.gate_caveat.

Clickability sweep: no dead controls found; cockpit already cleanly wired.
2026-06-21 03:11:15 +05:00
5587154327 fix(ptica): populate scan cards with real /analyze data + product tabs + map zoom
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 54s
CI / openapi-codegen-check (pull_request) Successful in 1m53s
Verified against a real captured /analyze+/forecast fixture rendered locally:
much of the cockpit's «—» was unmapped data, not missing data.

- РЫНОК: map Поглощение (market_pulse.avg_velocity_m2), add Конкурентов
  (market_pulse.competitors_total), Уровень спроса (market_trend.label);
  keep Медиана + Новых проектов. Prototype row order.
- СРЕДА (adaptEnvironmentCard now takes analysis): Шум (noise.estimated_db),
  Воздух PM2.5 (air_quality.pm2_5), Ветер (wind dir + max_speed), Водоёмы
  (hydrology nearest). Honest «—» where genuinely absent.
- ГРАДОСТРОИТЕЛЬСТВО: Пятно застройки = area × max_building_pct/100 (real).
- RECOMMENDED PRODUCT: prototype class tabs (обж_class present in mix),
  default = recommended class; per-class honest empty-state preserved.
- MAP: dark-styled ZoomControl (+/-) + ScaleControl, matching prototype.
- types: optional MarketPulse/MarketPrice on ParcelAnalysis (no any).

Economy/Investment-clearance stay «после финмодели §23» (not built); the
0%/«нельзя»/all-«избегать» verdict is the parcel's real result, not a bug.
2026-06-21 02:18:20 +05:00
167fcbace2 feat(ptica): wire real ПЗЗ regulation (КСИТ/height/zone/density) into cockpit
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
CI / openapi-codegen-check (pull_request) Successful in 1m46s
Read nspd_zoning.{max_far,max_height_m,max_floors,max_building_pct,
regulation_zone_index,regulation_source} (backend PR #1847) into the urban
scan-card, urban/buildability/potential drawers and the 3D massing КСИТ-target.
Defensive: every read is optional-chained + != null guarded → identical "—"
placeholders when fields absent, so the cockpit is unchanged until #1847 deploys,
then auto-lights-up. КСИТ density = area × max_far; пятно = area × max_building_pct
(percent 0..100, verified vs backend parser); sellable labeled estimate. 3D maxFar
= nspd_zoning.max_far ?? 3.5 (finite/>0 guarded). NspdZoning type +7 optional fields.
TS strict, no any.
2026-06-20 23:33:41 +05:00
6fe72d2374 Merge pull request 'feat(site-finder): продуктовые решения по фидбеку analyze (#1741-1745)' (#1749) from fix/sf-product-decisions into main
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Reviewed-on: #1749
2026-06-18 08:43:43 +00:00
8cd0620543 feat(site-finder): продуктовые решения по фидбеку analyze (#1741 #1742 #1743 #1744 #1745)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / changes (push) Successful in 9s
CI / frontend-tests (push) Successful in 1m43s
CI / frontend-tests (pull_request) Successful in 1m59s
CI / openapi-codegen-check (push) Successful in 2m43s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI / backend-tests (push) Successful in 9m28s
CI / backend-tests (pull_request) Successful in 9m28s
После ролевой дискуссии (аналитик×комдир×собственник):
- #1741 секции в 2 группы «Участок»→«Стройка и рынок» (Section1→4→2→3→5→6), gate-баннер вынесен НАД группой (убран дубль из Section4Estimate), nav-группы
- #1742 объяснение выбора класса: фраза-причина + мини-таблица 3 классов по deficit_index («+1 острый дефицит, −1 затоварка»). POI «обеспеченность»/market-overstock relabel — N/A (в UI не выводятся)
- #1743 «Остатки и скорость»→«Как продаётся рынок рядом» + headline-вердикт + «Темп продаж»/«Будущее предложение конкурентов (24 мес)» (убран англицизм Velocity-score/Pipeline) + интерпретированные единицы
- #1744 горизонт: caption+tooltip «пересчёт ~10-30с» + статус «Пересчитываем…»; колонки прогноза «…, квартир» + caveat advisory
- #1745 фаза1: таблица «Прогноз по всем форматам» (студия/1-5к × индекс дефицита/прогноз спроса/сигнал); backend пробросил per-format projected_demand_units + signal. Слайдеры/рубли — фаза2 после бэктеста #951

Verify: tsc 0 ошибок; py_compile; what_to_build/recommendation/report/exporter тесты pass.

Closes #1741
Closes #1742
Closes #1743
Closes #1744
Closes #1745
2026-06-18 12:18:04 +05:00
b1c0ea1268 fix(site-finder): 5 дефектов из боевого фидбека analyze (#1736 #1737 #1738 #1739 #1740)
Some checks failed
CI / changes (push) Successful in 9s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Failing after 1m50s
CI / frontend-tests (pull_request) Failing after 1m48s
CI / openapi-codegen-check (pull_request) Failing after 2m39s
CI / openapi-codegen-check (push) Failing after 2m46s
CI / backend-tests (pull_request) Successful in 9m25s
CI / backend-tests (push) Successful in 9m30s
- #1736 MiniMap: проброс useConnectionPoints → точки подключения на карте analyze (были только в /legacy)
- #1737 confidence: пронесено имя сервиса → RU-ярлык (Рынок/Будущее предложение/…) вместо «Компонент вкладывающий сервис»
- #1738 pipeline: self-exclusion субъекта (ST_DWithin 80м) — проект не считает сам себя будущим конкурентом
- #1739 PDF: snapshot_pdf обёрнут в try/except+logger.exception (причина 500 видна) + format=pdf в forecast export + font_url fallback
- #1740 gate↔recommendation: при can_build_mkd=False — gate_caveat на обоих рекомендаторах (противоречие явное, не молчит)

Verify: py_compile 5/5, tsc 0, ruff clean, pytest confidence/forecast 95 passed.

Closes #1736
Closes #1737
Closes #1738
Closes #1739
Closes #1740
2026-06-18 11:47:21 +05:00
0b20d98b45 feat(site-finder): ЗОУИТ visual layer на Leaflet карте (#255)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m49s
Deploy / deploy (push) Successful in 1m6s
ZouitLayer (GeoJSON-полигоны из geom_geojson, severity blocker/warning/info по keyword,
popup тип+имя) + ZouitLayerControlPanel (collapsible toggle per-severity, счётчик mappable)
+ SiteMap integration (default blocker+warning ON, info OFF, фон под POI). Graceful: нет
geom_geojson → слой/панель скрыты, текстовый NspdZouitOverlapsBlock остаётся. 10 vitest.

Closes #255
2026-06-13 17:41:08 +00:00
84d08940c6 fix(analytics): свежая ипотечная ставка + recommend last_updated + drop dead route (#236 #237 #251)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Successful in 3m9s
Deploy / build-backend (push) Successful in 4m12s
Deploy / build-worker (push) Successful in 5m31s
Deploy / deploy (push) Successful in 1m19s
#236: _current_mortgage_rate сортировал cbr_mortgage_series по TEXT-полю period
лексикографически ('Январь'>'Февраль') → возвращал stale-точку. Сортировка по
parsed (year, month) + regex-guard на malformed period-строки. Остаётся на
subsidized weighted series (НЕ domrf market — иная метрика). Verified prod:
Февраль 2026 (было Январь 2026).

#237: scope.last_updated = MAX(snapshot_date) domrf_kn_objects в recommend_mix +
подпись «данные на <дата>» в recommend page.

#251: удалён never-implemented GET /parcels/{parcel_id} (501) + ParcelDetail схема
(frontend bare 1-сегментный route не зовёт).

Closes #236
Closes #237
Closes #251
2026-06-13 15:22:22 +00:00
f5aa7ed191 feat(sf): 6.6 in-app future-supply pressure + pipeline competitors (§13.3)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m21s
Deploy / deploy (push) Successful in 1m32s
Surface the §16-traceability evidence behind the supply forecast that
previously lived only in exports: future_market.future_supply (§9.3
supply-pressure breakdown — open/hidden stock, absorption, months of
pressure, future units, pressure index) and future_market.future_competitors
(§9.7 relevance-weighted pipeline, sorted by relevance desc). New
ForecastFutureSupplyBlock as subsection 6.6 (+ section-6-6 nav),
complementing 6.1's per-horizon aggregates.

- Fix FutureSupply/FutureSupplyBreakdown/FutureCompetitor optionality in
  forecast.ts to match the backend (FutureSupplyPressure/Competitor): null
  metrics typed + rendered «нет данных», never a fabricated 0.
- Supply panel gates on computed metrics (index/pressure), NOT open/hidden
  unit stocks — so the empty-supply_layers state (pre Monday-worker, every
  parcel today) hides the panel instead of falsely showing "0 ед."; the
  competitor table still renders. hasSupplySignal shared in forecast-helpers.
- RU labels reuse report_pdf/excel export wording. Graceful on partial/202.

Part of #958; surfaces #950 future-supply data.
2026-06-07 10:55:59 +00:00
a66c35ae49 feat(forecast): render scoring transparency (§13.6) as in-app subsection 6.5
All checks were successful
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m2s
Deploy / changes (push) Successful in 6s
Deploy / deploy (push) Successful in 1m2s
Surface the product-score + special-index breakdown behind the overall KPI
as two token-bar groups in Section 6 (6.5), sidebar anchor section-6-5.
Product scores use a 0..1 quality gradient anchored on the documented 0.5
balance point (>=0.55 strong / 0.45-0.55 mid / <0.45 weak; risk scores are
pre-inverted so higher=better); special indices use a neutral accent
(value = signal strength, direction non-uniform). Null values render as
«нет данных», never a 0-bar that implies a real zero. Graceful on
absent/partial/202-pending.

- types/forecast.ts: ReportScoring + nested (dict-keyed scores/indices)
- ForecastScoringBlock.tsx: two bar groups, a11y, null-distinct rendering
- forecast-helpers.ts: scoreVariant/scoreBarWidthPct + RU label maps
- Section6Forecast + sidebar nav: wire 6.5

Part of #958.
2026-06-07 10:08:28 +00:00
54a501b90a feat(forecast): render product_tz (ТЗ на ЖК) as in-app subsection 6.4
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m56s
Deploy / deploy (push) Successful in 1m14s
Adds «6.4 Рекомендация по продукту» to Section6Forecast — the §13.4
product recommendation (class, unit-mix, commercial, USP, §16 reasons)
that previously existed only in the export renderers. Unit-mix is shown
as token-colored horizontal bars; since the assembler emits no per-format
share (pct), bars are honestly labeled as a deficit-by-format signal, not
a build-share. Graceful on thin/202-pending/partial reports.

- types/forecast.ts: ReportProductTz + nested interfaces, optional on ForecastReport
- ForecastProductTzBlock.tsx: per-field guards, role=img bars, reasons disclosure
- forecast-helpers.ts: deficitBarWidthPct
- Section6Forecast + sidebar nav: wire 6.4

Part of #958.
2026-06-07 09:55:54 +00:00
3423a1db74 feat(site-finder): toggleable market layers on analysis map (#999)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
Deploy / deploy (push) Successful in 1m5s
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m52s
Add Конкуренты / Будущие проекты / Зоны риска Leaflet layers to the Site Finder
analysis map (MiniMap→SiteMap) with per-layer toggles in CpLayerControlPanel
(Конкуренты on by default; pipeline + risk off). Competitors/pipeline render as
distinct CircleMarkers from analyze lat/lon (#1074); risk zones as GeoJSON
polygons parsed from geom_wkt via a small inline WKT parser (no new deps).
Graceful on missing coords / empty arrays / thin responses. Coordinate order
verified (GeoJSON lon-lat for <GeoJSON>, Leaflet lat-lon for CircleMarker).

3 of 4 layers; future-ЖК (newbuilding) pending newbuilding_listings API exposure
(#1108 built the table). Part of #999.
2026-06-07 14:35:24 +05:00
4ec762a202 feat(site-finder): forecast Section 6 (прогноз/сценарии/confidence) on analysis screen (#998)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m56s
Deploy / deploy (push) Successful in 1m4s
Renders the §22 SiteFinderReport (future_market/scenarios/confidence) as a new
Section 6 on the modern analysis page (/site-finder/analysis/[cad]), mirroring the
Section1-5 pattern. Polls GET /parcels/{cad}/forecast (202 pending → 200 ready) via
TanStack Query. Render-only (horizon selector = separate #996).

- 6.1 прогноз по горизонтам (deficit_index, MOI, demand/supply, ставка)
- 6.2 сценарии base/aggressive/conservative + rate_path
- 6.3 уверенность: level pill + явный список drivers (confidence.factors)
- types/forecast.ts typed against real prod report; fixture parcel-forecast.json (run #68)
- reuses design tokens + Badge/KpiCard; advisory disclaimer always shown

Part of EPIC #958. Deps 955-A2/A4 (backend §13, merged).
2026-06-05 14:09:55 +05:00
lekss361
6350e3f59a feat(sf-legacy): expose EGRN + utilities в Обзор tab
Бэкенд /api/v1/parcels/{cad}/analyze возвращает много новых B5 fields
(egrn, utilities, gate_verdict, nspd_zouit_overlaps, ...), но старая Обзор
вкладка показывает только subset (district, score, POI). User просит чтобы
legacy показывал не только старые данные, ближе к Maxim-style макету.

Добавил 2 conditional блока в OverviewTab после District:
- EGRN compact: address / ВРИ / категория / статус / право (если есть)
- Сети рядом: substation/pipeline/water/...  с distances + охранная зона
  ЛЭП warning

Type-only: ParcelAnalysis в types/site-finder.ts расширен полями
egrn (8 optional fields) + utilities (summary array + охранная зона flag).
Поля nullable optional — graceful empty state per cad.

Не трогает новый SF (/site-finder/*) и не меняет существующие блоки
Обзора (district / score / POI / isochrones).
2026-05-18 07:21:53 +03:00
90eb5af883 feat(trade-in): TI-3 frontend /trade-in page + 5 components + hooks (#317)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m45s
2026-05-17 16:58:53 +00:00
f8c382f885 feat(22k): object page — full exposure 9 blocks (sales/quartirography/metro/photos/docs/specs/checks) (#310)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m35s
Deploy / build-worker (push) Successful in 2m37s
Deploy / build-frontend (push) Successful in 3m2s
Deploy / deploy (push) Successful in 56s
2026-05-17 15:56:39 +00:00
lekss361
e1a8ad9395 fix(sf-17): rosreestr velocity fallback via cad_quarter (~100% EKB coverage)
When Objective mapping coverage falls below 50% of competitors in the radius,
fall back to rosreestr_deals JOIN on the parcel's cadastral quarter. Audit shows
237/237 EKB quarters (100%) have rosreestr data for the last 12 months, compared
to <20% Objective coverage before bulk mapping.

- velocity.py: add _compute_rosreestr_fallback(), _OBJECTIVE_COVERAGE_MIN_RATIO
  constant, velocity_source field on VelocityResult (objective/rosreestr_fallback/none)
- parcels.py: extract cad_quarter from cad_num, pass to compute_velocity
- site-finder.ts: add velocity_source field to Velocity interface
- VelocityBlock.tsx: badge "Источник: квартальные сделки" when rosreestr_fallback

Epic #271 item #17
2026-05-17 16:50:19 +03:00
lekss361
989d4e5b7d fix(sf-20): adaptive district threshold 30→15 with data_confidence flag
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
  (Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
  data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
  emoji star removed (ui-ux.md), empty-state text min 30→15
2026-05-17 16:34:52 +03:00
lekss361
b7ce4a589c fix(sf-10): velocity LEFT JOIN + velocity_data_available flag
Replace INNER JOIN on objective_complex_mapping with LEFT JOIN approach:
competitors without mapping now return velocity=0 with
velocity_data_available=False instead of being silently dropped.

UI: VelocityBlock shows 'нет данных velocity' badge when flag is False
and hides the gauge (meaningless zero). TS type updated (additive optional
field, backward compat).

Audit (EKB, region_cd=66): mapped=129, unmapped=1387, total=1516.

Closes #271 item #10
2026-05-17 15:51:43 +03:00
lekss361
bcd7dc8f75 fix(sf-09): iterative cap algorithm + cap_skipped flag + unit tests
Review verdict MINOR (PR #282) — single-pass cap_and_redistribute
не гарантировал invariant max ≤ MAX_BUCKET_SHARE_PCT. Surplus от clamped
поднимал free выше cap (например {1k:75, studio:15, 2k:10} → studio=39).

- backend/app/services/site_finder/best_layouts.py
  - _cap_and_redistribute → iterative while-loop до сходимости
  - Returns (result_map, cap_skipped) — флаг для pathological all-clamped
  - Float work map, Hamilton финальный pass для invariant sum=100
- backend/app/schemas/parcel.py
  - LayoutTzRecommendation.cap_skipped: bool = False
- backend/tests/services/site_finder/test_best_layouts.py
  - 6 parametrized tests: invariants, 7 failing cases from review,
    no-dominant unchanged, empty, cap_skipped propagation, normal-case
- frontend/src/types/best-layouts.ts — cap_skipped: boolean
- frontend/src/components/site-finder/BestLayoutsBlock.tsx
  - banner условие сменилось с maxPct > 60 на rec.cap_skipped (honest signal)
2026-05-17 15:21:51 +03:00
102ef88d56 Merge pull request 'fix(sf-03): CompetitorTable Status column + filter tabs' (#278) from fix/sf-03-competitor-table-status into main
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m58s
Deploy / deploy (push) Successful in 48s
2026-05-17 10:50:27 +00:00
lekss361
98323027aa fix(sf-03): CompetitorTable Status column + filter tabs
Some checks failed
CI / backend (pull_request) Failing after 49s
CI / frontend (pull_request) Successful in 1m55s
После backend PR #276 (site_status в /analyze) — frontend показывает
статус строящиеся/сданные с цветным badge и фильтр-tabs наверху таблицы.

TS type extended manually (backward compat) — codegen подхватит после
backend deploy.

Closes (epic part) #271 item 3
2026-05-17 13:41:15 +03:00
lekss361
247c252b04 fix(sf-05): clamp sold_pct_of_supply at 100% + add is_oversold flag
Symptom: sold_pct_of_supply = sum_deals_24mo / supply_count_snapshot * 100
yields >100% (e.g. 199%) for fast-selling small formats due to incompatible
time windows (24-month deals vs point-in-time supply snapshot).

Option A (hotfix): clamp to 100.0, expose is_oversold=True for UI badge.

- best_layouts.py: compute sold_pct_raw, clamp with min(..., 100.0),
  set is_oversold = raw > 100%
- parcel.py TopLayoutRow: add is_oversold: bool field
- best-layouts.ts TopLayoutRow: add is_oversold: boolean
- BestLayoutsBlock.tsx: show warn badge ">100%" when is_oversold=True
- tests: two new cases — raw 199% clamps + is_oversold=True;
  raw 50% passes through + is_oversold=False

Closes (epic part) #271 item 5
2026-05-17 11:57:29 +03:00
e3eb8c873f Merge pull request 'feat(#254,#114): custom POI UI (Leaflet click-to-add) + score breakdown stacked-bar' (#258) from feat/254-custom-pois-ui into main
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m2s
Deploy / deploy (push) Successful in 39s
2026-05-17 06:47:08 +00:00
lekss361
6ecd7a9269 feat(#254,#114): custom POI UI + score breakdown stacked-bar viz
Task A (#254): Leaflet click-to-add custom POIs
- sessionId.ts: getOrCreateSessionId() from localStorage
- types/customPoi.ts: CustomPoi, CustomPoiCreate, CustomPoiUpdate
- hooks/useCustomPois.ts: useCustomPois / useAddCustomPoi / useUpdateCustomPoi / useDeleteCustomPoi (TanStack Query, X-Session-Id header)
- CustomPoiLayer.tsx: LayerGroup with CircleMarker (green/red/gray by weight), popup with edit/delete
- CustomPoiToggleButton.tsx: crosshair mode toggle button
- CustomPoiAddModal.tsx: modal form (name, 26 categories, weight -5..+5, notes)
- CustomPoiEditModal.tsx: same form for editing existing POI
- SiteMap.tsx: MapClickHandler (useMapEvents), integrated all custom POI components
- page.tsx: useCustomPois hook + passes customPois + parcelCad to SiteMap

Task B (#114): ScoreBreakdownStackedBar
- ScoreBreakdownStackedBar.tsx: horizontal stacked bar by category with positive/negative split, toggle between category and factor views, custom POIs highlighted orange with dashed outline
- OverviewTab.tsx: renders ScoreBreakdownStackedBar below ScoreBreakdownPanel

Requires backend PR with /api/v1/custom-pois endpoints merged before POI CRUD works.
2026-05-17 09:35:35 +03:00
lekss361
990c109756 fix(#234): NSPD harvest ETA badge + SETNX dedupe (Sub-PR C)
Backend (quarter_dump_lookup.py):
- _acquire_harvest_lock: Redis SETNX TTL=120s на quarter, защищает от burst
  N concurrent analyze, ставящих N одинаковых harvest task в очередь
- _trigger_harvest: использует lock перед apply_async, возвращает False если
  lock уже взят (другой запрос триггернул раньше)
- make_empty_result/EMPTY_DUMP_RESULT: новое поле harvest_eta_seconds в
  nspd_dump dict, типичный harvest_quarter = 60с
- /analyze: пробрасывает поле через nspd_dump dict (нет typed schema —
  response_model=None для /analyze endpoint, dict уходит как есть)

Frontend (NspdFreshnessBadge, NspdZoningBlock):
- Countdown «НСПД: загрузка ~Nс» вместо бесконечного спиннера
- После остановки countdown (remaining=0) NspdZoningBlock показывает
  «загрузка дольше обычного» + ссылку на ПКК вместо infinite skeleton

Tests: 5 новых unit + 2 для empty_result schema (всего +7, pass)

Closes #234 (UX-side; data-side resolves когда Sub-PR B + D merged).
2026-05-17 09:13:06 +03:00
lekss361
fdb54834e5 fix(nspd): rename migration 89→98, fix red lines ST_Area→ST_Length (#220)
- data/sql/98_*: rename from 89 to avoid collision with existing 89_drop_dead_brin
- _get_red_lines: ST_Length(ST_Intersection(planar)::geography) instead of
  ST_Area(ST_Intersection(::geography, ::geography)) — fixes PostGIS 3.4
  tolerance error and returns correct non-zero length for LINESTRING intersections
- Rename intersection_area_sqm → intersection_length_m across schema, TS types,
  frontend component, and tests; add test_get_red_lines_db_exception_returns_empty

Addresses review-bot feedback on PR #220.
2026-05-16 19:33:14 +03:00
lekss361
ed3c128528 feat(nspd): TIER 4 opportunity layers + red lines (#94 PR2 of 4)
- NSPDClient: QUARTER_OPPORTUNITY_LAYERS (auction/scheme/free/future/oopt)
  + QuarterDump.opportunity field
- nspd_sync: harvest_quarter accepts include_opportunity, denorm cols
  has_auction_parcels + opportunity_count in UPSERT
- quarter_dump_lookup:
  - _get_opportunity_parcels (sort by distance, early-exit on count=0)
  - _get_red_lines (query existing dump.red_lines core path, layer='red_lines')
- SQL 89_*: has_auction_parcels + opportunity_count + partial index
- Pydantic: OpportunityParcel + RedLine schemas
- /analyze: nspd_opportunity_parcels + nspd_red_lines fields
- Frontend: NspdOpportunityBlock + NspdRedLinesBlock + LandTab integration
- Tests: 28 total (11 PR1 + 17 PR2), all pass

Red lines uses existing core harvest path (layer 879243 already in dump.red_lines
from PR1+core) — no duplicate harvest, no red_lines_count_v2 column needed.

Part of #94
2026-05-16 19:06:22 +03:00
lekss361
1595157ebf feat(nspd): enable TIER 3 risk zones harvest + extract in analyze (#94 PR1)
- harvest_stale_quarters fanout passes include_risks=True to all queued tasks
- _get_risk_zones() with ST_Intersects + ST_Area intersection calc
- _extract_features_by_layer() generic helper for future PR2+ reuse
- RiskZone Pydantic schema; /analyze returns nspd_risk_zones list
- NspdRiskZonesBlock component with severity color-coding (red/yellow/orange)
- 11 unit tests for extract/intersect/null-area/db-exception paths

Part of #94 (TIER 3 risk layers — flooding, landslide, burns, erosion etc)
2026-05-16 16:11:59 +03:00
e450546178 feat(site-finder): NSPD frontend integration в LandTab (#202) (#212)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Successful in 1m58s
Deploy / deploy (push) Successful in 29s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
2026-05-16 11:55:31 +00:00
256909d28b feat(layouts): PDF ТЗ endpoint + BestLayoutsBlock UI (#113 PR D) (#199)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m38s
Deploy / build-worker (push) Successful in 3m3s
Deploy / build-frontend (push) Successful in 2m8s
Deploy / deploy (push) Successful in 38s
2026-05-16 09:53:05 +00:00
lekss361
faa99abb94
feat(site-finder): per-room bucket breakdown в VelocityBlock (#166)
VelocityBucketStat + optional by_room_bucket fields.
'По комнатности' aggregate table + per-competitor inline mini-row.
Backward compat via optional field presence guards.

Closes #161 (frontend part)

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 10:12:42 +03:00
lekss361
8fa1d005fd
feat(velocity): migrate D2 sales source → objective_corpus_room_month (#158)
* feat(velocity): migrate D2 sales source from stale sale_graph to objective_corpus_room_month

Closes #156

* fix(velocity): cross-stack contract + NULL safety per pre-push review

3 pre-push code-reviewer findings fixed:

1. TS union extended ('objective'|'sale_graph'|'rosreestr_fallback')
   + UI conditions handle both objective и sale_graph как valid sources.
2. COALESCE(deals_total_vol_m2, deals_total_count * 45.0) — NULL safety
   for DKP-only rows (vol_m2 nullable, count > 0).
3. room_bucket parking filter — verified false positive (все 5 buckets
   apartments: студия/1/2/3/4+).

Refs: PR #157 pre-push code-reviewer

* fix(types): add 'objective' to velocity_source union (cherry-pick miss)

Cherry-pick от закрытого PR #157 потерял этот файл. Frontend type-check fails
с 'no overlap' error. Restoring.

Refs: PR #158 frontend CI failure

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 09:34:00 +03:00
lekss361
088c1ab2cf
feat(site-finder): VelocityBlock в MarketTab (#34 sub-PR 2/2 FINAL) (#147)
* feat(site-finder): VelocityBlock в MarketTab (#34 sub-PR 2/2 FINAL)

D2 velocity-score frontend: score gauge, period meta, top 5 competitors table.
TS types (Velocity/VelocityCompetitor/VelocityPeriod) добавлены.
Guard "velocity" in data для backward-compat pre-#146.

tsc 0 errors, ESLint 0 warnings.

Closes #34

* fix(site-finder): align VelocityBlock TS types with backend response shape

Per #147 bot review — cross-stack contract mismatch:

Backend velocity.py (PR #146 merged) returns:
  obj_id, name, dev_name, obj_class, district_name, distance_m, total_sqm_period

Frontend types had:
  obj_id, name, class, distance_m, total_sqm

Result: c.class undefined → never showed class chip; c.total_sqm undefined →
Math.round(NaN) = NaN, 'не число' в table column для всех 5 строк.

## Fix

- VelocityCompetitor.class → obj_class
- VelocityCompetitor.total_sqm → total_sqm_period
- Added optional dev_name, district_name fields
- Updated VelocityBlock.tsx — все 3 references (guard, chip render, table cell)

Refs: #34, #147

---------

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 01:44:16 +03:00
lekss361
95647a1c82
feat(site-finder): G5 gate verdict banner in OverviewTab (#32 sub-PR 2/2 FINAL) (#143)
GateVerdictBanner с 4 visual states + expand/collapse details.
Types: GateVerdict/GateBlocker/GateWarning/GateVerdictSource/GateVerdictLabel.
Renders at top of OverviewTab driven by backend aggregator (#142).

tsc 0 errors, ESLint 0 warnings.

Vault: code/modules/Module_Gate_Verdict_Banner.md NEW.

Closes #32

Co-authored-by: lekss361 <claudestars@proton.me>
2026-05-15 01:09:34 +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
2e83d62237 feat(site-finder): center distance bonus display + success_recommendation block 2026-05-11 22:40:46 +03:00
lekss361
c31da62e8d feat(analytics): recommend_mix v3.1-v3.4 - noise + 2D competitors + 24m cap + success-driven 2026-05-11 22:19:41 +03:00
lekss361
5b03d6d799 feat(site-finder): isochrones UI + networks VKH + OSM substations 2026-05-11 21:48:21 +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
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