Section 1 mini-map (/site-finder/analysis/[cad] — единственная карта
страницы) оборачивал SiteMap в height:320 + overflow:hidden. SiteMap
рендерит карту фиксированной высотой 420px + ПОД ней (flow) кнопку
"Добавить POI", легенду и CpLayerControlPanel с тумблерами
competitors/pipeline/risk/opportunity/redlines. Нижние 100px карты и
ВСЕ контролы обрезались clip-обёрткой → недостижимы кликом.
visibleMarketLayers init = {"competitors"} — единственный способ
включить остальные слои (#999 zone-of-risk, #958 §12.1-13 opportunity
parcels / red lines) — те самые скрытые чекбоксы → слои физически
невозможно было включить на странице анализа, хотя данные пробрасывались
props'ами ради них.
Patch (Вариант A из issue):
- SiteMap.tsx: новый prop mapHeight?: number (default 420 — backward-
compat для /site-finder/[cad]).
- MiniMap.tsx: снято height:320 + overflow:hidden; передаём mapHeight=280
(компактнее но не клипает); только width-constraint (maxWidth 400)
на обёртке.
2 новых vitest: mapHeight=280 пробрасывается в SiteMap (mocked stub),
clipping-стили отсутствуют. 82/82 frontend тестов зелёные. tsc + lint clean.
Closes#1218
Render the §12.1-13 geometry already exposed on /analyze but previously
dropped by the MiniMap adapter:
- Перспективные ЗУ (nspd_opportunity_parcels.geom_wkt) — viz-3 polygons
- Красные линии застройки (nspd_red_lines.geom_wkt) — warn dashed lines
Wired as toggles in the existing CpLayerControlPanel (Рынок group),
reusing wkt.ts (extended with LINESTRING/MULTILINESTRING for red lines).
Empty/invalid geometry renders nothing gracefully; popups RU plain-text.
§22 forecast (future_market/special_indices), ППТ-ПМТ planning polygons
and future-ЖК points carry no map-able geometry on the frontend yet — left
as backend follow-ups, not faked.
Refs #958
Surface report.meta provenance as a compact muted caption above the advisory
disclaimer: «Прогноз рассчитан: <дата> · горизонты N / M мес · схема X». The
freshness date sources from the run's persist time (envelope created_at, always
set) with meta.generated_at as fallback — the deterministic assembler leaves
generated_at null, but created_at is the real compute timestamp. Until now this
provenance lived only in exports; horizons appeared only via the chart axis.
New ForecastMetaLine.tsx: three independently-guarded segments (RU long date
with optional non-midnight time, horizons, most-muted schema); graceful null
when meta empty / 202-pending. Advisory disclaimer stays last.
Part of #958.
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.
Add «Скачать .docx» (Word) and «Скачать .pptx» (PowerPoint) buttons to
ForecastExportButtons, alongside the existing .md/.json/Telegram controls.
The backend /forecast/export already renders both formats; they were just
not reachable from the UI. Both reuse the existing response.blob() →
triggerDownload path (correct for binary zip-based OOXML — never .text(),
which would corrupt the archive). Per-format loading state; 404 →
«Отчёт ещё не готов». No new deps (icons from lucide-react).
Part of #958.
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.
ECharts (via ChartShell, SSR-safe) chart of deficit_index across horizons per
scenario (база/агр/конс, --viz-1..3) + dashed rate-path on a secondary axis +
баланс/целевой markLines, tooltip with units. Renders above the 6.1 table;
returns null on thin reports. Makes the §22 forecast signal glanceable for demos.
«Скачать отчёт .md / .json» + «Скопировать для Telegram» on the ready §22
forecast, wired to GET /parcels/{cad}/forecast/export. triggerDownload extracted
to lib/download.ts (reused by ExportButtons). Clipboard fallback for insecure
contexts; 404 → inline «Отчёт ещё не готов». Part of EPIC #959.
Docker auto-sets HOSTNAME to the container id; Next standalone's server.js
binds process.env.HOSTNAME, so it listened on the container hostname and the
compose healthcheck (TCP 127.0.0.1:3000) was refused → frontend container
chronically "(unhealthy)" (cosmetic: Caddy reaches it by service name and the
site serves 200). Pin HOSTNAME=0.0.0.0 — the canonical Next.js Docker fix.
Diagnosed on prod 2026-06-05.
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.
RouteGuard: при denied path главного фронта (корень / и пр.) если у роли
единственная рабочая зона — trade-in (isPathAllowed(/trade-in/)=true) →
window.location.replace('/trade-in/') вместо дед-энда NoAccessScreen.
Guard от петли: не редиректить если путь уже под /trade-in. admin/multi-scope
с разрешённым / не затронуты. Вариант 2 из issue (без Caddy/backend).
Refs #799
User: 'плюс проверь какие данн чтобы было то что хочет макс хотя бы похожи
что не старше года иначе жк не конкурент'.
Анализ /analyze для 66:41:0204016:10:
Total competitors: 20
Строящиеся: 6
Сданные: 14 (12 из них ready_dt >1y → стейл)
Maxim spec: ЖК старше года не конкурент. Активные = строящиеся
ИЛИ ready_dt в будущем ИЛИ сданы ≤1 года назад.
MarketTab фильтрует data.competitors в relevantCompetitors:
- site_status === 'Строящиеся' → активен
- ready_dt в будущем → активен
- ready_dt в пределах последнего года → активен
- иначе скрыт
activeCount + CompetitorTable теперь используют отфильтрованный список.
Section label показывает 'N из M — фильтр' для прозрачности.
После deploy для тестового cad: activeCount упадёт с 20 → 9
(6 строящихся + 1 future ≤1y + 2 past ≤1y).
Бэкенд /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).
User reports on /legacy/site-finder/[cad]:
Велосити 0.0 м²/мес · Срок продаж —
which is contradictory — if velocity were truly zero, sales period
would be infinity. Root cause: backend returns 0 (not null) when
competitors are unmapped to Objective ground truth (OBJ-3). The
condition for 'unavailable' must catch both null and zero.
Fix: getSalesPeriodMonths already treats velocitySqm <= 0 as null
(line 41), but the HeadlineBar + KPI card read raw velocity and
display '0.0 м²/мес' for zero. Same coercion applied at the source:
velocityPerMonth = (raw != null && raw > 0) ? raw : null.
Both HeadlineBar and KPI card now show '—' for zero velocity,
matching the '—' shown for Срок продаж.
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/ можно удалить.
Wire analysis/[cad]/page.tsx: replace SidebarPlaceholder with
AnalysisSidebar (sticky, IntersectionObserver scrollspy on sections
1/2/3/3.1/3.2/3.3/4/5), header with AnalysisBreadcrumb (SiteFinder
→ cad → Анализ) and UserAvatar (gd_org_id from localStorage, SSR-safe).
Add lucide-react to package.json (was used in A1 but missing from deps).