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.
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.
Dark cockpit Scenarios tab wired to useParcelForecastQuery (ForecastEnvelope):
scenario cards (base/aggr/conserv), demand-vs-supply chart + horizons table,
scenario compare table, confidence panel, recommended product (квартирография
+ target price + success-score), scoring transparency. Calm polling skeleton
while the async forecast is pending — never an error.
Hero invest-score/buy-signal now surface real values once the forecast resolves
(scoring.overall×10; buy-signal from exec_summary.key_numbers.deficit_index,
derived independent of the overall score), keeping the "после прогноза"
placeholders while pending. advisory · §22 caption shown in both states.
All forecast field paths verified against types/forecast.ts. Dark theme stays
scoped under .pticaRoot[data-theme=dark]. TS strict, no any.
New non-destructive route /site-finder/analysis/[cad]/ptica rendering the existing
/analyze data in the «ПТИЦА» operator-terminal cockpit. The light analysis page,
Section1-6, globals.css and package.json are untouched.
PR#1 scope: app-shell (topbar + 4 tabs + left rail), hero (dark Leaflet map +
parcel passport KV-grid + Buildability radial gauge + invest-score block), and the
Development Scan card grid — wired to useParcelAnalyzeQuery / ParcelAnalysis.
Data honesty: real fields render live (passport egrn.*, district, utilities.summary,
median_price, pipeline_24mo); absent fields show muted placeholders with source
captions (buildability/risk = derived proxy «предв.»; invest/buy-signal «после
прогноза»; КСИТ/height/ОКС/economy «—») via typed ptica-adapt.ts — no fake numbers
presented as live.
Dark theme is a CSS-module scoped under .pticaRoot[data-theme=dark] (cannot leak to
the light app); IBM Plex Mono added via next/font (no dep/lockfile change). Leaflet
via dynamic(ssr:false). TS strict, no any. Deferred to follow-up PRs: Scenarios
(forecast), Reports+Compare tabs, 16 detail drawers, Three.js 3D massing.
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.
Critical demo-blocker: POST /analyze returns 202 {status:fetching} for parcels
whose geometry isn't cached yet (#93 on-demand НСПД fetch). useParcelAnalyzeQuery
treated 202 as success → the stub (no score/competitors) reached render → Section 3/4
threw TypeError → white screen (no error boundary). Repro confirmed on prod.
- useParcelAnalyzeQuery: 202-aware — poll /fetch-status (2s, 2-min cap), re-POST
on ready (status-checked, no stub on 202 race), throw HTTPError on
not_in_nspd/failed/invalid_format; horizon preserved in both POSTs; retry: false
- error boundaries: app/site-finder/analysis/[cad]/error.tsx + app/error.tsx —
no white screen ever; calm RU message in prod, error detail in dev
- Section3/Section4: guard competitors/score against partial payloads
- CadInput: drop hardcoded default cad (empty + placeholder), raw hex → tokens
Map parcel-select already fixed by 3cea915. Part of EPIC #958.
Segmented control «Горизонт прогноза» driving the analyze query (?horizon=)
and Section 6's target highlight. Default 12.
- HorizonSelector: radiogroup + a11y radios, tokens-only, tabular-nums,
disabled while a re-analyze is in flight
- useParcelAnalyzeQuery(cad, horizon=12): horizon in queryKey + ?horizon=;
keepPreviousData so switching horizon doesn't blank the page to loading
- AnalysisPageContent: horizon state + forecast-poll invalidation on change
- Section6Forecast/ForecastHorizonsBlock: selectedHorizon → target row («Целевой»)
Part of EPIC #958.
Marker click opens an interactive Leaflet popup with a primary "Открыть анализ"
action that navigates to /site-finder/analysis/<cad> (encodeURIComponent —
matches the [cad] route's single-decode contract) and records the visit in
RecentParcels. Hover tooltip enriched with status + actionable hint. Existing
ParcelDrawer preview kept, reachable via the popup's secondary "Подробнее".
ESLint + tsc clean.
User asked для доступа к старой версии перед SF redesign (A1-A11 in progress).
Файл backup лежал в frontend/src/app/_legacy/site-finder-page-tabs.tsx —
папка с префиксом _ Next.js игнорирует как route, доступа через URL нет.
Перенос в frontend/src/app/legacy/site-finder/page.tsx делает его accessible
как /legacy/site-finder. Все импорты компонентов (OverviewTab, EnvironmentTab,
LandTab, MarketTab, KpiCard, CadInput, FetchingState, SiteMap, WeightProfilePanel)
существуют — старая страница рабочая без модификаций.
После полной миграции на новый SF (A12 done + acceptance audit) папку
frontend/src/app/legacy/ можно удалить.