Fold the generative concept generator (3 strategy variants: placement map +
ТЭП + finance) into the light analysis report (/site-finder/analysis/{cad}) as
a new section after «6. Прогноз», so Концепция lives in one place alongside the
report instead of a separate flow (epic #1953, #1965 Stage 1).
Frontend-only: reuses ConceptParamsForm / ConceptVariantsResult and the
/concepts mutation (useCreateConcept) as-is — no duplication, no backend change.
Inputs are seeded from the analysis the report already has (no re-entry of the
cadastre/polygon): polygon ← extractPolygon(geom_geojson); housing_class /
development_type ← financial_estimate inferred values; land_cost ←
egrn.cadastral_value_rub; falls back to comfort/mid_rise.
Runs on demand (heavy generative call — never auto on mount); result block
(Leaflet map) is lazy-mounted via dynamic({ ssr: false }) and the section is
collapsed by default. When financial_estimate is null (ЗОУИТ/СЗЗ/нет зоны) it
shows an honest banner but still allows a manual run, since /concepts is
independent of the financial_estimate gate. Graceful empty state when geometry
is missing/unsupported. Stage 2 (interactive 3D + /concepts/recompute) left as
a seam, not built.
The "Сети" section table summarised engineering networks and distances, but
users could not see those points on the map. Three root causes:
- Connection-points layer was always empty: NSPD engineering_structures
(cat 36328) arrive as Polygon/MultiPolygon in EPSG:3857 (prod: 587 Polygon
+ 298 MultiPolygon, zero Point), but extractLatLon only handled Point in
EPSG:4326. Extend it to compute the outer-ring centroid for
Polygon/MultiPolygon and reproject 3857→4326 (frontend math, no backend
change), so the 10+ connection points render as markers with popups.
- Map vs table coverage mismatch: the table counts "В 2 км" while the OSM
utility-infrastructure layer fetched only 500 m (1 object inside 500 m vs
153 within 2 km). Raise the hook default to a shared
UTILITY_COVERAGE_RADIUS_M = 2000 so the map matches the table column.
- Coherence microcopy: clarify that the map shows the same networks as the
table (2 km), plus NSPD connection points and protection zones.
Adds unit tests for extractLatLon polygon-centroid + 3857→4326 reprojection.
Refs #1961
Replace dev-jargon with plain RU across the §22 forecast report (6.2/6.3/6.5
+ deficit/затоварка legend). Source of truth = backend reason strings + the
frontend RU maps; text/render-only, no scoring/forecast math touched.
Backend (source of truth):
- scenarios.py _COLLAPSE_REASON_LOW_BETA: «β rate-sensitivity не прошёл gate …»
→ «чувствительность к ставке не оценена на коротком ряде ЕКБ → один базовый
сценарий вместо трёх».
- confidence_engine.py _coverage_factor: drop «domrf↔objective» jargon, say it
affects будущее предложение/конкуренцию. New _history_factor: «глубина истории
N мес» + на что влияет + связь с 6.2 (короткий ряд → один сценарий).
Frontend (both Section-6 families — live analysis page + ptica cockpit):
- Deficit legend: −1 затоварка / 0 баланс / +1 острый дефицит + actionable
трактовка; MOI tied to «сколько месяцев район распродаёт предложение».
- 6.2 heading «Почему один сценарий, а не три» over the collapse reason.
- 6.3 render confidence.rationale + weakest-link rule («скорее завысим
недоверие, чем недооценим риск»); FACTOR_RU gains confounded_window/
advisory_cap; factor notes shown.
- 6.5 «Вес»→«Оценка»; overall verdict vs 0.5; «Риск избытка предложения»→
«Запас по предложению»; §-refs moved from reason into tooltip; 6 special-
index 1-line «что это + куда лучше» descriptions; 0.00-score reasons shown.
Tests: confidence_engine (history/coverage notes), stripSectionRefs vitest.
Refs #1963
Locked/no-access users (403/denied path/expired session/trial ended) were
trapped: NoAccessScreen had no logout control, and «Выйти» lived only inside
UserMenu, which does not render on the no-access screen. They could not switch
accounts without clearing browser basic-auth manually.
- Extract logout() into shared `lib/logout.ts` (single source of truth).
- UserMenu now imports it instead of a local copy (behaviour unchanged).
- NoAccessScreen renders an always-available «Выйти» accent button for every
variant.
Mirrored across both frontends (tradein-mvp/frontend + main frontend) to keep
the MIRROR invariant in sync.
Light report (Section1ParcelInfo) only rendered the dead top-level
zoning.data_available=false (closed-PKK6 path) and never showed the
resolved nspd_zoning, so the resolved regulation (synth #1891) was
invisible to the user («мы выгружали НСПД — где они?»).
- Add «Градрегламент (НСПД)»-card to Раздел 1 (right column, под ЕГРН):
тер.зона, КСИТ/max_far, пред. высота, этажность, коэф. застройки,
КСИТ-ёмкость/пятно (от площади ЕГРН), источник + список разрешённых
ВРИ зоны. Empty-state когда регламент не определён (gap-зоны / не-ЕКБ).
КСИТ-microcopy расшифровывает жаргон на первой странице.
- New pure mapper adaptNspdRegulation (nspd-regulation.ts) reused by
ptica-adapt so light report и /ptica остаются consistent.
- coerceFloat: max_far/max_building_pct приходят с провода СТРОКАМИ
("2.4"/"80") — фикс латентного бага cockpit'а (typeof===number
глушил реальный строковый КСИТ в massing/insolation).
- Widen NspdZoning.max_far/max_building_pct to string|number, add
main_vri; expose nspd_zoning on ParcelAnalyzeResponse.
- Unit test для field-mapping + coercion + empty-state.
Four frontend display fixes for the Site Finder analysis report (audit #1953);
all consume fields the (already-deployed) backend now returns.
- #1954: ЕГРН «Обременения» row was hardcoded «—». Wire top-level
`encumbrance` block {has_zouit, zouit_count, zouit_types} via
formatEncumbrance → «ЗОУИТ: N (types)» / «не выявлено (НСПД)».
- #1955: show obj_class per success bucket («Студии 15-30 · Комфорт») so
rows with identical area-labels are distinct; fix top-row match + React key
to (bucket, obj_class).
- #1957: NspdZouitOverlapsBlock renders human type_zone + reg_numb_border,
adds a СЗЗ caveat banner (СанПиН 2.2.1/2.1.1.1200-03) and «СЗЗ вашего
участка» highlight; switched raw hex to UI tokens.
- #1958: ForecastChart axis names → nameLocation:middle + nameGap (no legend
overlap); grey flat-state callout when the deficit line is degenerate
(scenarios_collapsed or all points clamped at ±1).
Tests: formatEncumbrance + isDeficitDegenerate (15 new). Real segment
differentiation is backend #1959 (separate).
FIX A (#1955) «Что хорошо продаётся»: убран фантомный класс 'Comfort'.
- Миграция 172: v_bucket_success_score COALESCE(obj_class,'Comfort')
→ COALESCE(obj_class,'не указан'). Английский литерал заполнял 397 NULL
и сливался отдельным классом от русского 'Комфорт' → визуальные дубли
бакетов в UI. Источник уже канонически-русский (проверено на проде),
synonym-mapping не нужен.
- parcels.py: obj_class протаскивается в success-ranking query + dict.
- TS SuccessRankingBucket.obj_class добавлен.
FIX B (#1960) «Медиана рынка» = 64k (квартальная росреестровская n=1 ДКП):
- district.median_price_per_m2 больше не COALESCE(median_12m, ekb_ref) в SQL.
Basis-приоритет (newbuild-first): Objective по имени района →
geo_radius (Objective в 3км) → ekb_districts reference →
квартальная росреестровская медиана ТОЛЬКО при deals_count≥5.
Для 66:41:0205010:287: 64k → 132690 (geo_radius, newbuild-consistent).
- median_price_basis добавлен в payload + TS type (nullable median).
- Frontend null-guards для нового nullable median.
Tests: +4 (geo_radius basis, objective-приоритет, deals-guard, obj_class
passthrough); обновлены district-моки в 9 analyze-тестах под новую
SQL-сигнатуру.
New price tier objective_geo_radius: ST_DWithin median of Objective new-build prices (objective_lots ⋈ complexes) within 3km of parcel centroid, between quarter-MV and district_reference. Closes the name-match gap (5 of 9 EKB districts had no Objective name-match). data/sql/168 functional GIST index (prod EXPLAIN 114ms→28ms). Degenerate-centroid guard + honest RU price_source captions. Deep-review ✅.
Refs #1881
VAT charged on full gross margin → only on parking value-added; residential (реализация жилья + услуги застройщика по ДДУ) exempt per пп.22/23.1 п.3 ст. 149 НК; input VAT embedded in gross СМР. Σ cashflow == net_profit invariant preserved. PDF + concept-UI caveats synced. +3 tests, deep-review ✅.
Refs #1881
- add <h1> page title to analysis page (иерархия стартовала с h2 — a11y)
- emoji → Lucide во всех вердикт/баннер/статус UI (ui-conventions: emoji
запрещены): GateVerdictBanner (VerdictColor.icon string→ComponentType,
sourceHint→ReactNode, SectionLabels), + найдены ещё 3 файла:
OverviewTab (⚠ ЛЭП/трамвай), GeotechRiskBlock (🟡 вечная мерзлота → Snowflake,
⚠ загрязнение), HydrologyBlock (⚠ паводок, 🏞💦🪷🚣💧 subtype-map → Waves/Droplet).
Все иконки декоративные → aria-hidden, семантика дублируется текстом.
Семантические цвета через var(--success/--warn/--danger) + hex-fallback.
- расшифровка аббревиатур при первом упоминании (ui-microcopy):
Section2 «строительство в охранной зоне (ОЗ)... зоне с особыми условиями
использования территорий (ЗОУИТ), тип 5»; Section4 title-tooltip для ЗОУИТ.
- ForecastChart: axis-chrome hex (:65/67/68) задокументирован как canvas-renderer
исключение (echarts canvas не резолвит CSS var() — var() сломал бы chrome,
как и VIZ-series). Цвета не тронуты, только комментарий.
НЕ в scope: ForecastChart CI-band (±p25/p75 — backend не отдаёт, отдельная
задача); ★ в WeightProfilePanel <option> (SVG в option невозможен);
токенизация legacy-hex в Geotech/Hydrology (отдельный pass).
138 frontend vitest passed, tsc/lint clean. lucide-react уже в deps.
Refs #1871
CI frontend-tests упал: honesty-тест импортировал фикстуры из
src/app/%5F%5Fpreview/ptica/__fixtures__/ — эта dev-only директория в
.git/info/exclude, поэтому отсутствует в Linux CI checkout (vite:import-analysis
'Failed to resolve import'). Локально (macOS) резолвилось т.к. файлы на диске.
Переключил импорт на tracked src/lib/mocks/parcel-{forecast,analyze}.json
(NEXT_PUBLIC_USE_MOCKS source, в git) — report.exec_summary.key_numbers.
deficit_index присутствует, shape совместим. 138/138 vitest passed.
Refs #1871
Когда 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
Полный откат «Site Finder v2» с прода — пилот-беты редизайна
кокпита ПТИЦА. Удалён роут /site-finder/analysis/{cad}/v2, все
компоненты ptica-v2/* (18 файлов), беta-ссылка из AnalysisPageContent.
Сохраняется как было: старый Section1-6 на /site-finder/analysis/{cad},
PticaMapInner и старый /ptica-роут (через 301 на parent), ptica-adapt,
mocks — всё нетронуто. Прод-URL и UX не меняются для пилотов.
Старый ptica-кокпит, на основе которого делался v2, был забракован
визуально, новая попытка тоже не дошла до 1:1 с эталоном. Откат
к стабильному состоянию.
PR #1868 добавил ресурсную легенду (электр/вода/канал/тепло/газ) поверх
карты, но PticaMapInner уже отрисовывает свою POI/категорийную легенду
top-left высотой ~215px. PR #1869 попытался разнести их (top → bottom-left)
— безуспешно: моя легенда всё равно попадала в нижний хвост POI-overlay.
Прагматичный исход: ресурсная легенда ДУБЛИРУЕТ данные карточки ИНЖЕНЕРИЯ
(те же расстояния и статусы). Убираю её совсем и оставляю per-theme заголовок
карты (тот фикс был полезный и не конфликтует) — POI-overlay PticaMapInner
несёт уникальные счётчики POI, ради которых карта и нужна.
Удалены: V2MapLegend.tsx и блок CSS-правил .mapLegend* в v2.module.css.
— V2MapLegend overlay над cockpit-картой (электр/вода/канал/тепло/газ)
с nearest_m + статус-точка, источник utilities.summary (subtype-buckets
зеркалят backend _nearest() aliases; electric расширен power_line, gas
включает legacy generic pipeline).
— Per-theme заголовок карты: dark «СПУТНИК · СВЕЖИЙ СНИМОК», light
«СПУТНИК · СХЕМА ПОДКЛЮЧЕНИЙ» (toggle через CSS [data-theme], без JS).
— Точечный light-theme contrast в v2.module.css: kv/eng значения,
drawer eyebrow, .more / .ver / активная вкладка с cyan→accent-blue,
status-dot halo off на белом. Dark не тронут.
Scope: только ptica-v2/* и v2.module.css. PticaMapInner (шарится со
старым ptica) не тронут.
The ПТИЦА dark "operator terminal" skin was applied to the main
/site-finder map page in b3fd053 ("dark Site Finder"), but the
2026-06-21 rollback (#1865) only reverted the analysis route — the
entry-landing stayed dark.
Точечно откатывает 8 файлов entry-landing (page.tsx + 7 entry-компонентов)
до состояния b3fd053~1 и удаляет осиротевший site-finder.module.css.
НЕ трогает: PticaMapInner / ptica.module.css (используются и старым ptica,
и Site Finder v2 cockpit) — карта аналитики остаётся glyph-pin как была.
Port the approved ptica-v2 prototype to a new non-destructive route
/site-finder/analysis/{cad}/v2 — dark/light cockpit shell, real Leaflet
map (reuses PticaMapInner), 6 scan cards, lower/bottom grids, 11 drawers
and grounded chat. All values flow through the existing ptica-adapt layer
(real /analyze data or honest «—»). One beta link added on the analysis page.
- Сценарии: add section 6.6 «будущее предложение и конкуренты» (FutureSupply)
rendering report.future_market.future_competitors after ScoringTransparency
(order 6.1→6.6). Columns ЖК/Класс/Квартир/Дистанция/Поглощение, real data,
honest empty-state. Reuses .dtable.
- Отчёты: add «Параметры» panel (карта/финмодель/прогноз §22); §22 row tied to
the real forecast-ready flag. Reuses StatusRow.
Verified on the all-tabs harness; no console errors. tokens-only, no fabrication.
Prototype's first ГРАДОСТРОИТЕЛЬСТВО row is ВРИ — prod lacked it. Add it from
the real egrn.permitted_use_text (e.g. «многоквартирные дома»); honest «нет
данных ЕГРН» fallback. Value wraps cleanly in the narrow card (verified, no
overflow).
Multi-region audit vs the prototype styles.css/dashboard.html → 22 CSS fixes +
markup. Headline: .lowerGrid/.bottomGrid align-items:start so the sparse ОКС
«нет данных» card no longer stretches into a huge void. Plus restore missing
prototype bits: topbar icon-buttons (Экспорт→Отчёты, Полный экран→fullscreen,
Настройки aria-disabled «скоро»), rail help-dot, app-footer (honest disclaimer,
no fabricated model/timestamp), invest-score sparkline (decorative, aria-hidden),
green/amber value tones, engineering status-row dots (from real nearest_m),
verdict-card gradient, scan-card status-rows, .canvas/.hbar/.mixHead spacing.
All new CSS scoped under .pticaRoot[data-theme=dark] (status-row classes
descendant-scoped under .scanCard — no collision with the drawer's). Environment
card stays honest kvrow placeholders (no fake dots). a11y: aria-labels on icon
buttons. TS strict, no any. tsc/lint/prettier/build green. code-reviewer ✅.
The cockpit rail had 6 text-only items whose scrollToAnchor targeted
non-existent IDs (clicks did nothing) and diverged from the reference.
Replace with the prototype's 7 icon+label items (Участок/Потенциал/Продукт/
Экономика/Риски/Сравнение/Отчёты, icons ported from the kit). Discriminated
union RailItem {kind:'scroll'|'tab'}: scroll items activate the analysis tab
then scrollIntoView the matching section on the next frame; tab items switch
the top tab. Add ptica-hero/-potential/-product/-economy/-risks section IDs;
port the .nav-item active state (cyan accent + glow + left bar).
Fix two regressions vs the prototype:
1. PticaMapInner: render real data layers over the dark base by REUSING the
existing analysis-map components — POI by category, competitors/pipeline
(MarketLayers), connection points + colored polylines (ConnectionPointsLayer),
ЗОУИТ (ZouitLayer), custom POI add/edit/delete (CustomPoiLayer + mutation hooks).
Add Спутник/Схема base toggle (Esri default + --map-filter-sat); legend rows
toggle layers; «Точки подключения» driven by real CP data. Isochrones left
«скоро» (ORS is on-demand, not in /analyze).
2. /site-finder/analysis/[cad] now renders the ПТИЦА cockpit — every parcel entry
opens ПТИЦА, not the old design. [cad]/ptica redirects to the parent (single
cockpit copy). Old AnalysisPageContent kept in repo; legacy UI at /legacy/site-finder.
3. Surface 3D massing: new «Инсоляция · 3D-масса» scan card + 3D legend row + 3D
map tool all open the insolation/future3d drawers.
SSR-safe (Leaflet only behind dynamic ssr:false); real POI from score_breakdown;
no redirect loop. tsc/lint/prettier/build green. code-reviewer APPROVE.
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.
PticaDrawer: proper modal focus management —
- focus-trap: Tab/Shift+Tab wrap inside the dialog (focusables re-queried per
Tab so it survives lazy panels/sub-tabs), focus-already-outside pulled back in;
- focus-restore: the trigger element is captured on open and re-focused on close
(guards null + detached node);
- onClose held in a ref so the effect depends on [open] only — fixes focus
thrashing when the host's URL/searchParams change mid-open.
Esc/scrim close, body-scroll-lock, role=dialog/aria-modal preserved.
Cleanups (code-review follow-ups):
- MassingScene: drop duplicate initial mass build (floors/sections effect owns
it); corrected the effect-ordering comment.
- Static inline styles → CSS-module classes (scan/gauge/score/spaced rows).
- DrawerPrimitives: remove !important on .dtabActive via specificity
(.dtabs button.dtabActive).
- Remove unused SoonCard component + its CSS.
tsc/lint/prettier clean; next build green (route bundled). ptica-only, no any.
Port the prototype massing.js to a client-only React component (MassingScene)
behind a dynamic(ssr:false) wrapper (MassingViewer), wired into the future3d +
insolation drawers, replacing the СКОРО SoonCards.
Generative mass from parcel area (real geometry_suitability.area_ha) × КСИТ-цель
3.5 (labelled regulation-default · НСПД — honest, real max_far not in /analyze),
OrbitControls (orbit/zoom/auto-rotate), time-of-day sun with shadows (insolation
preview), live GFA/КСИТ-факт metrics (green ≤3.5 / amber over). Full renderer/
scene/RAF dispose on drawer close; graceful WebGL-unavailable fallback.
Adds three@0.184.0 + @types/three (package.json + package-lock.json in sync).
SSR-isolated (three imported only in MassingScene). TS strict, no any.