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.
The ПТИЦА dark cockpit (made default in ba05a4c) is being redesigned from
scratch. Roll the live /site-finder/analysis/[cad] route back to the original
AnalysisPageContent (Section1-6) so pilots/investor see the stable design while
the new cockpit is reworked.
Non-destructive: only the route's rendered component + metadata/fallback are
reverted. All ptica code stays in the repo and the cockpit remains viewable at
/__preview/ptica-full for redesign reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Сценарии: 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.
Один объект на avito_imv + yandex_valuation с разными ext_item_id создаёт
дубликаты в house_placement_history и double-count'ится в помесячной медиане.
Решение: CTE с DISTINCT ON (round(area_m2), floor, price, price_date) с
приоритетом avito_imv через CASE sort. Легитимные разные квартиры (разные
площадь/этаж/цена) не затрагиваются.
За флагом estimate_price_trend_dedup_enabled (дефолт True в settings).
False → точно старое поведение (backward-compat). Graceful: без изменений
в логике min_points и возврата None при недостатке данных.
Тесты: 6 новых тест-кейсов в test_estimator_price_trend_dedup.py (SQL содержит
DISTINCT ON, flag OFF не содержит, Source1 preferred, данные возвращаются корректно).
Ужесточает валидацию ДКП-сделок в _is_plausible_deal:
- floor < 1 или floor=0 → drop (floor=-5/0/999 из битого парсера)
- area_m2 задана и <= 0 → drop (нулевая/отрицательная площадь из битого парсера)
- price_rub задана и <= 0 → drop (нерыночная/техническая сделка)
- floor=None → допустимо (graceful, нечем судить)
Все новые параметры area_m2/price_rub keyword-only с дефолтом None →
backward-compatible для существующих вызовов. Caller _fetch_deals обновлён
для передачи area_m2/price_rub из данных сделки.
Тесты: 16 новых тест-кейсов в test_deals_sanitize.py (floor=-5, floor=999,
floor=None ok, area_m2=0 drop, price_rub=-1 drop и пр.).
Добавляет _is_price_sane() в cian_valuation.py: если sale_price_rub вне
[cian_valuation_min_rub, cian_valuation_max_rub] (дефолты 500k/500M),
или low_price > high_price, или любой bound отрицательный — результат
не кэшируется и возвращается None (graceful). Защита от garbage-ответов
API (999_999 < min, 9_999_999_999 > max).
Новые settings: cian_valuation_min_rub=500_000, cian_valuation_max_rub=500_000_000.
Тесты: 11 новых тест-кейсов в test_cian_valuation.py. Пофиксен pre-existing
KeyError в test_cache_hit_returns_cached (missing low_price/high_price в mock).
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).
The scoped reset `.pticaRoot button { font: inherit }` had specificity (0,1,1),
beating single-class button rules like .navItem (9px), .tab (11px), .detailBtn
(9px), .ptab — so every cockpit button was forced back to the root 13px. The
oversized labels overflowed the 96px left rail (icons/text shoved right of
centre, «неровное») and bloated tabs/buttons across the cockpit.
Wrap the selector in :where(button) → specificity (0,1,0); the per-component
classes (later in source) now win their font-size, matching the prototype's
bare `button {}`. Verified on a live local render: rail labels 13→9px and every
item re-centres at 47.5px; tabs 11px; detail buttons 9px.
_save_yandex_history_items теперь пропускает items с area_m2 is None или
area_m2 <= 0 (битый парс «0,5 м²» и пр.) перед сохранением в
house_placement_history. Грязь копилась в БД и искажала price_trend.
Estimator защищён NULLIF на уровне SQL, но фильтрация на входе надёжнее.
Лог кол-ва отброшенных (info). Батч не падает из-за одного битого item.
Anchor-путь имеет hard floor от comp_min_ppm2×(1-tol). Radius-путь аналогичной
защиты снизу не имел — quarter-index-вниз или corridor-clamp могли занизить
median неоправданно (asymmetry).
Добавлен floor: если итоговый median_ppm2 < dkp_low_ppm2 × factor → поднять
до floor + лог. Только radius-путь (anchor_tier is None); без dkp_raw → no-op.
За флагами estimate_radius_floor_enabled (default True) и
estimate_radius_floor_factor (default 0.8).
_get_asking_sold_ratio теперь вызывается ПОСЛЕ anchor/IMV-blend/quarter-index/
corridor-clamp, получая ФИНАЛЬНЫЙ median_ppm2 для tier-placement (#928 audit).
До фикса: tier резолвился по pre-anchor radius-медиане (~105k), но ratio
применялся к post-anchor headline (~300k) — несовпадение tier'а в premium-
сегменте. После: ratio берётся из того же tier, что и финальный headline.
Graceful: нет ratio → expected_sold_* = None, headline не меняется.
The «Инсоляционная матрица» preview cube now gently rotates (rotateY
turntable with a constant rotateX tilt for depth) so the 3D module reads
as alive without opening the drawer. Pure CSS; honours prefers-reduced-motion.
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.
Resolve numeric ПЗЗ limits (max_far/КСИТ, max_height_m, max_floors,
max_building_pct, min_parcel_area_m2) by parcel centroid via the coordinate
resolver get_or_fetch_zone_regulation (cache-first; bounded 3s live geoportal)
and merge into the /analyze nspd_zoning object. Powers the ПТИЦА cockpit's
real КСИТ/height/density (was placeholder). Flag-gated
(enable_zoning_regulation_in_analyze, default on) and hot-path-safe: any
exception/timeout → null fields, analyze never 500s or slow-fails (3s cap).
Additive — existing nspd_zoning keys untouched. 198 tests pass.
Note: zone_index_at is a live WFS call per analyze even on warm cache
(~0.3-0.5s healthy / 3s cap degraded) — optimization tracked as follow-up.
_get_zoning() read non-existent props (top-level reg_numb_border / zone_code /
type_zone / name) so nspd_zoning.zone_code & zone_name were NULL on every parcel.
Real dump props (verified prod, 796 features) carry only the registration number,
duplicated across descr / label / externalKey / options.reg_numb_border (no human
zone name exists in the NSPD dump). Map both fields to that reg-number with safe
nested narrowing (isinstance options dict) + legacy fallbacks; honest — no
fabricated name. Surfacing reg_numb_border also unblocks the КСИТ crosswalk (#1843).
Regression tests use the real prop shape (full / nested-only / opaque→None).