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.
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.
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.
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.
Бэкенд /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).
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
- 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
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
После backend PR #276 (site_status в /analyze) — frontend показывает
статус строящиеся/сданные с цветным badge и фильтр-tabs наверху таблицы.
TS type extended manually (backward compat) — codegen подхватит после
backend deploy.
Closes (epic part) #271 item 3
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
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).
* 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>
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>
* 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>
* 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>
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>
- ObjectBuilding type + useObjectBuildings hook
- Objects detail page: render Корпуса ЖК table (cad_num, floors, area,
purpose, name, address) when buildings_count > 0
- Developers page: drop hardcoded compareIds, use top-3 from
useTopDevelopers sorted by jk_count DESC (fallback to legacy static
array when API has no data yet)
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).