Commit graph

164 commits

Author SHA1 Message Date
750d34d5cb feat(concept): Concept UI Stage 1d — ввод участка + варианты (#57)
Заменяет concept/page.tsx TODO-stubs полным generative-флоу: draw-полигон или
кадастр→geom ввод участка, форма параметров ConceptInput, POST /concepts через
useMutation, табы вариантов с placement-картой + ТЭП KPI + финмодель + экспорт
(GeoJSON/CSV client-side, DXF/PDF на /concepts/export graceful 404). Типы зеркалят
concept.py; переиспользует site-finder Leaflet + analytics Section/KpiCard паттерны.
tsc + lint + build clean.

Closes #57
2026-06-13 16:42:19 +00:00
d737307022 feat(site-finder): НСПД verify deep-links + landing GenDesign vs НСПД таблица (#97 #100)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-frontend (push) Successful in 4m29s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 1m5s
#97: nspdLinks helper (nspdMapUrl/pkkSearchUrl, encodeURIComponent) + NspdVerifyLink
компонент (var(--accent), rel=noopener) → одноклик-сверка с НСПД/ПКК в Section1
(кадастр/ЕГРН) + Section2 (инж.сети) карточках analyze.

#100: landing — сравнительная секция «GenDesign и НСПД» (7-row таблица value-prop:
цены ДДУ/остатки/скорость/инвест-балл/прогноз/квартирография/экспорт сверх бесплатной
гос-карты) + footer deep-links.

Closes #97
Closes #100
2026-06-13 15:53:02 +00:00
84d08940c6 fix(analytics): свежая ипотечная ставка + recommend last_updated + drop dead route (#236 #237 #251)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Successful in 3m9s
Deploy / build-backend (push) Successful in 4m12s
Deploy / build-worker (push) Successful in 5m31s
Deploy / deploy (push) Successful in 1m19s
#236: _current_mortgage_rate сортировал cbr_mortgage_series по TEXT-полю period
лексикографически ('Январь'>'Февраль') → возвращал stale-точку. Сортировка по
parsed (year, month) + regex-guard на malformed period-строки. Остаётся на
subsidized weighted series (НЕ domrf market — иная метрика). Verified prod:
Февраль 2026 (было Январь 2026).

#237: scope.last_updated = MAX(snapshot_date) domrf_kn_objects в recommend_mix +
подпись «данные на <дата>» в recommend page.

#251: удалён never-implemented GET /parcels/{parcel_id} (501) + ParcelDetail схема
(frontend bare 1-сегментный route не зовёт).

Closes #236
Closes #237
Closes #251
2026-06-13 15:22:22 +00:00
414601c1d2 test(analytics): vitest для ObjectSaleChart union-months alignment (#1246)
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 1m6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 56s
CI / frontend-tests (pull_request) Successful in 58s
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m37s
union axis sorted, per-month alignment с nulls, регрессия-guard (хвост позднего
типа не клипается), happy-path unchanged, empty-state. 5 кейсов.
2026-06-13 18:20:41 +05:00
e2c8c0e97b fix(analytics): выровнять ObjectSaleChart серии по union месяцев всех типов (#1246)
Some checks failed
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been cancelled
ObjectSaleChart строил ось X из месяцев ТОЛЬКО первого типа и привязывал серии
позиционно. domrf_kn_sale_graph фетчится per-type независимо от DOM.РФ → типы
могут нести разные наборы месяцев — позиционная привязка тогда смещает бары/
линию цены и молча клипает лишние точки. Текущие prod-данные выровнены (457/457
объектов, идентичные месяцы), поэтому live-поломки нет, но код полагался на
coincidence, не invariant. Ось из union всех месяцев + маппинг значений каждого
типа по месяцу через Map (null где отсутствует), как PrinzipVelocityChart. +vitest.

Closes #1246
2026-06-13 18:20:25 +05:00
16e505326d perf(site-finder): honour AbortSignal в poll-цикле useParcelAnalyzeQuery (#1248)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
queryFn игнорировал TanStack v5 AbortSignal: first POST /analyze, poll GET
/fetch-status и re-POST не передавали signal, цикл спал на non-abortable
setTimeout без проверки signal.aborted. При unmount / смене cad|horizon цикл
жил до 2-мин кэпа, дёргая GET /fetch-status каждые 2с + POST /analyze на каждый
ready — зомби-поллинг. Проброс signal во все 3 fetch, sleep через shared
abortableSleep (extracted из useSiteAnalysis #1242), throw AbortError перед
итерацией. +3 vitest (abort + happy-path).

Closes #1248
2026-06-13 13:19:50 +00:00
99c9a130bc feat(forecast): расширить горизонт прогноза до 24 мес (ТЗ §12.1)
All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Successful in 46s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Successful in 46s
CI / backend-tests (push) Successful in 6m35s
CI / backend-tests (pull_request) Successful in 6m30s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 2m14s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / build-worker (push) Successful in 4m6s
Deploy / deploy (push) Successful in 1m40s
API отвергал ?horizon=24 (422), хотя ТЗ §12.1 называет 6/12/18/24, а движок
УЖЕ считает 24 на каждом ране: _DEFAULT_HORIZONS=(6,12,18,24) во всех 6 точках
стека (orchestrator/forecast-task/demand_supply_forecast/scenarios/
special_indices/report_assembler), PIPELINE_HORIZON_MONTHS=24.
_hidden_release_fraction клампит h/18→1.0 на 24 (без переполнения),
future_supply._horizon_weight расширяет окно чисто — скрытых ≤18 потолков нет.
Чистое расширение валидатора-enum, не новая математика.

Backend: _ALLOWED_FORECAST_HORIZONS → {6,12,18,24}, Query/docstring/error-msg.
Frontend: HorizonSelector HORIZONS=[6,12,18,24] (тип horizon=number, union не нужен;
прочие потребители data-driven через meta.horizons/forecasts_by_horizon).
Тесты: API принимает 24/отвергает 30; движок-тесты доказывают h=24 осмыслен
(поля посчитаны, demand(24)>demand(18), hidden созрел, индексы в диапазонах).

Closes #944 (Q1 горизонт 24)
2026-06-13 17:27:35 +05:00
d8a2ad7854 fix(auth): отобразить роль analyst в UserMenu, расширить Role union в useMe (#1237) 2026-06-13 15:03:15 +05:00
fbafb1bf68 fix(landing): mirror Pydantic schema в PilotRequestModal validation (#1240)
Backend PilotRequestInput требует name min_length=2 и др., но modal
проверял только !form.name.trim(). 1-char name ('Я') проходил, backend
возвращал 422, apiFetch клал raw FastAPI JSON в Error.message, modal
рендерил его буквально для public lead form.

Patch: PILOT_LIMITS + PILOT_EMAIL_RE константы (синхр с backend),
per-field onBlur+live валидация с RU-сообщениями, minLength/maxLength
+ aria-invalid/aria-describedby, submit disabled при ошибках,
formatSubmitError → 4xx/5xx → human text. 26 vitest кейсов.

Closes #1240
2026-06-13 15:02:50 +05:00
1e2fa35c8e fix(admin): persist JSON extra_config при свёрнутой textarea (#1239)
Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
2026-06-13 08:20:25 +00:00
01d00ec183 fix(custom-pois): invalidate canonical parcel-analyze + parcel-poi-score keys (#1241)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
useCustomPois mutations инвалидировали ['analyze', cad], а такой key
ни один useQuery не регистрирует. Canonical keys: ['parcel-analyze', cad,
horizon] и ['parcel-poi-score', cad]. Все 3 onSuccess были silent no-ops
→ скор+breakdown stale после правок кастомных POI.

Patch: analyzeKey → ['parcel-analyze', parcelCad] (prefix-only, TanStack
covers all horizons), добавлен poiScoreKey, оба инвалидируются вместе.
82/82 frontend тестов зелёные.

Closes #1241
2026-06-13 08:15:05 +00:00
67d24e9bf7 fix(analytics): ObjectSaleChart tooltip показывает полный ₽/м², не /1000 (#1238)
Some checks are pending
Deploy / changes (push) Waiting to run
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Серия price делила data на 1000 без valueFormatter в tooltip → ось говорила
'тыс ₽/м²' но точки рисовались как 'обычные числа', tooltip показывал
'146' вместо '146 250 ₽/м²' (×1000 underreporting). Patch: убрал /1000
делитель, ось 'тыс ₽/м²' → '₽/м²', добавлен tooltip valueFormatter
'146 250 ₽/м²' через toLocaleString('ru'). 82/82 frontend тестов зелёные.

Closes #1238
2026-06-13 08:10:38 +00:00
931273628a fix(site-analysis): per-call AbortController, не shared cancelledRef (#1242)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m9s
Deploy / deploy (push) Successful in 1m10s
Конкурентные analyze делили один cancelledRef: новая мутация ставила
ref=false и воскрешала спящий poll-цикл предыдущей — лишние fetch-status/
POST analyze и setFetchingState(null) затирали баннер активного запроса.

Patch: shared boolean ref → per-call AbortController, signal проброшен
в apiFetch/apiFetchWithStatus, добавлен abortableSleep чтобы цикл
просыпался мгновенно на cancel. setFetchingState(null) guarded
signal.aborted — зомби-цикл не может перезаписать состояние актуальной
мутации. 82/82 frontend тестов зелёные.

Closes #1242
2026-06-13 07:54:10 +00:00
0a8785d548 fix(site-finder): MiniMap не клипает layer-controls — рыночные слои доступны (#1218)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
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
2026-06-13 05:59:42 +00:00
03e5d07907 fix(site-finder): adapt backend egrn_block → frontend ParcelEgrn (#1217)
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / build-worker (push) Has been skipped
Deploy / changes (push) Successful in 7s
Deploy / build-backend (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
Бэкенд /analyze отдаёт egrn_block c snake_case-ключами
permitted_use_text/land_category/ownership_type/parcel_status/
last_egrn_update_date (parcels.py:2151-2171), фронт ждёт ParcelEgrn с
vri/category/owner_type/status/last_updated (site-finder-api.ts:255).
Section1ParcelInfo делал `data.egrn as ParcelEgrn` — каст глушит TS;
6 из 10 строк таблицы ЕГРН в prod пустые. ExportButtons тем же кастом
писал пустые ячейки в CSV. egrn={} (truthy) глушил buildFallbackEgrn.
Mock-фикстура была в старой фронт-форме — маскировала баг в dev.

Patch:
- `adaptEgrn(raw, cad)` в site-finder-api.ts: маппит backend-ключи →
  frontend, пустой `{}` → null. Тестируется в изоляции.
- AnalyzeResponse.egrn: ParcelEgrn|null → unknown (wire-shape).
- Section1ParcelInfo: `adaptEgrn(data.egrn, cad) ?? buildFallbackEgrn(cad)`.
- ExportButtons: пропс `egrn` (адаптированный) вместо bad-cast'а.
- mock parcel-analyze.json — на реальную backend-форму.
- 7 vitest-кейсов: backend→frontend mapping, {}→null, null/scalar→null,
  partial nullable→em-dash, threading cad, пустые строки=missing.

80/80 frontend тестов зелёные. tsc + lint clean.

Closes #1217
2026-06-13 05:36:52 +00:00
9a2cf32787 fix(api): handle 204 No Content в apiFetch (#1219)
Some checks failed
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been cancelled
apiFetch безусловно звал response.json() после !response.ok-check, что
падало SyntaxError'ом на пустом теле 204. DELETE /custom-pois/{id} и
DELETE /admin/site-finder/weight-profiles/{id} объявлены status_code=204
→ mutation promise реджектился → onSuccess/invalidateQueries не срабатывали
→ POI/профиль виден в UI до случайного рефетча, хотя реально удалён на
сервере. Silent fail для DELETE кастомных POI на SiteMap (#1219, P2).

Fix: на 204 или пустом теле resolve'имся с undefined (аналог семантики
apiFetchWithStatus). Сигнатура Promise<T> сохранена — 80 не-204 callers
backward-compatible; два DELETE-хука уже типизированы <unknown>/<void>
и корректно игнорируют возврат.

6 новых vitest кейсов (api.test.ts): 204, empty-body-200, 200+JSON, 404,
500, regression guard. 73/73 frontend тестов зелёные, lint+tsc clean.

Closes #1219
2026-06-13 05:13:44 +00:00
df9d54b532 feat(site-finder): add opportunity-ЗУ + red-line map layers (§12.1-13, #958)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m40s
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 1m4s
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
2026-06-09 07:19:53 +00:00
4adfef2ad0 feat(site-finder): grounded parcel-chat panel UI (#958)
All checks were successful
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (push) Successful in 45s
CI / frontend-tests (pull_request) Successful in 42s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m43s
Deploy / deploy (push) Successful in 1m4s
CI / changes (pull_request) Successful in 6s
Chat panel on the analysis screen consuming POST /api/v1/chat/ask:
preset-intent quick-buttons (summary/what_to_build/why_forecast/risks/
scenarios), history-aware turns (cap 10), advisory badge + grounded_in
provenance + llm_used marker. Plain-text answer rendering (no HTML
injection); loading/pending/error states with retry. TanStack useMutation
(no useEffect-for-HTTP), tokens-only, Lucide icons, TS strict. Types inline
(codegen backend unreachable in env); no new dependency.

Refs #958
2026-06-08 18:08:01 +05:00
cdb6035e39 chore(frontend): add vitest runner + WKT parser unit tests
Some checks failed
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
Deploy / deploy (push) Blocked by required conditions
CI / changes (pull_request) Successful in 5s
CI / backend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
- Wire vitest (jsdom, globals, @ alias->src, jest-dom matchers); add
  test / test:watch scripts and dev deps (vitest 2.1, @vitejs/plugin-react,
  jsdom, @testing-library/{react,dom,jest-dom,user-event}); regen lockfile
  (both package.json + package-lock.json, alpine-musl binaries verified).
- Revive 4 orphaned tests: jest.fn->vi.fn (Drawer); fix outdated subtitle
  DOM assertion (HeadlineBar); drop stale jest setup comments.
- Extract WKT->GeoJSON parser (wktToGeometry/splitTopLevel/parseRing) from
  MarketLayers into src/lib/wkt.ts (pure, behavior-preserving refactor) +
  add EWKT SRID-prefix tolerance; cover with src/lib/__tests__/wkt.test.ts
  (16 cases incl. lon/lat order, holes, multipolygon, EWKT, garbage->null).

57 tests green; type-check green; build green. CI wiring is a devops follow-up.

Refs #999.
2026-06-07 16:46:27 +05:00
7756ed4631 feat(forecast): show compute date / horizons / schema in Section 6 footer
All checks were successful
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m58s
Deploy / deploy (push) Successful in 1m2s
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.
2026-06-07 16:08:31 +05:00
f5aa7ed191 feat(sf): 6.6 in-app future-supply pressure + pipeline competitors (§13.3)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m21s
Deploy / deploy (push) Successful in 1m32s
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.
2026-06-07 10:55:59 +00:00
7131bed1f4 feat(forecast): expose DOCX & PPTX export buttons in forecast UI
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / backend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
Deploy / build-frontend (push) Successful in 28s
Deploy / deploy (push) Successful in 1m0s
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.
2026-06-07 15:12:52 +05:00
a66c35ae49 feat(forecast): render scoring transparency (§13.6) as in-app subsection 6.5
All checks were successful
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m2s
Deploy / changes (push) Successful in 6s
Deploy / deploy (push) Successful in 1m2s
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.
2026-06-07 10:08:28 +00:00
54a501b90a feat(forecast): render product_tz (ТЗ на ЖК) as in-app subsection 6.4
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m56s
Deploy / deploy (push) Successful in 1m14s
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.
2026-06-07 09:55:54 +00:00
3423a1db74 feat(site-finder): toggleable market layers on analysis map (#999)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
Deploy / deploy (push) Successful in 1m5s
CI / changes (push) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m52s
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.
2026-06-07 14:35:24 +05:00
a66a554462 feat(site-finder): forecast deficit-by-horizon chart in Section 6 (#998)
All checks were successful
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (push) Has been skipped
CI / backend-tests (pull_request) Has been skipped
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m51s
Deploy / deploy (push) Successful in 1m19s
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.
2026-06-07 14:19:17 +05:00
858b5f4511 feat(site-finder): forecast export control in Section 6 (#959)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m59s
Deploy / deploy (push) Successful in 1m2s
«Скачать отчёт .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.
2026-06-07 08:41:05 +00:00
f7eb98b971 fix(site-finder): handle 202 on-demand geometry in analyze + error boundaries (#1001)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m52s
Deploy / deploy (push) Successful in 1m1s
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.
2026-06-05 16:12:21 +05:00
740210d9ea feat(site-finder): horizon selector (6/12/18) on analysis screen (#996)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m49s
Deploy / deploy (push) Successful in 1m0s
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.
2026-06-05 15:42:58 +05:00
4ec762a202 feat(site-finder): forecast Section 6 (прогноз/сценарии/confidence) on analysis screen (#998)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m56s
Deploy / deploy (push) Successful in 1m4s
Renders the §22 SiteFinderReport (future_market/scenarios/confidence) as a new
Section 6 on the modern analysis page (/site-finder/analysis/[cad]), mirroring the
Section1-5 pattern. Polls GET /parcels/{cad}/forecast (202 pending → 200 ready) via
TanStack Query. Render-only (horizon selector = separate #996).

- 6.1 прогноз по горизонтам (deficit_index, MOI, demand/supply, ставка)
- 6.2 сценарии base/aggressive/conservative + rate_path
- 6.3 уверенность: level pill + явный список drivers (confidence.factors)
- types/forecast.ts typed against real prod report; fixture parcel-forecast.json (run #68)
- reuses design tokens + Badge/KpiCard; advisory disclaimer always shown

Part of EPIC #958. Deps 955-A2/A4 (backend §13, merged).
2026-06-05 14:09:55 +05:00
3cea915e48 feat(site-finder): clickable parcel markers open analysis from the map
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m7s
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.
2026-06-04 12:50:37 +05:00
654ce7b6c8 fix(frontend): pilot-only юзер → auto-redirect на /trade-in/ вместо «Доступа нет»
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
2026-05-30 21:18:19 +03:00
56a7a36c9a feat(auth): UserMenu — личный кабинет top-right (PR D) (#590)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 4s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m45s
Deploy Trade-In / deploy (push) Successful in 38s
Deploy / build-frontend (push) Successful in 3m0s
Deploy / deploy (push) Successful in 58s
2026-05-26 10:48:57 +00:00
c08e4d7811 feat(rbac): pilot scope → только /trade-in/** (#587)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 26s
Deploy / build-backend (push) Successful in 28s
Deploy / build-worker (push) Successful in 28s
Deploy / build-frontend (push) Successful in 27s
Deploy Trade-In / deploy (push) Successful in 36s
Deploy / deploy (push) Successful in 55s
Decision 2026-05-26: pilot не видит landing/analytics/site-finder/concept, только Trade-In.
2026-05-26 07:40:44 +00:00
7a2b055b35 feat(rbac): frontend route-guard + nav filter (#586)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m42s
Deploy Trade-In / deploy (push) Successful in 39s
Deploy / build-frontend (push) Successful in 2m55s
Deploy / deploy (push) Successful in 1m1s
PR B (frontend) to backend RBAC #585. RouteGuard wraps app, NoAccessScreen on 403 from /me, TopNav filtered by role.
2026-05-26 06:51:45 +00:00
a2953e97c4 refactor(security): убрать UI ввода admin token (backend не требует с PR #437) (#442)
All checks were successful
Deploy / build-frontend (push) Successful in 2m46s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 57s
2026-05-23 12:06:37 +00:00
43a8ec426e fix(ui): &lt;Link&gt; вместо &lt;a&gt; для internal nav routes (build error fix)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m25s
Deploy / deploy (push) Successful in 57s
2026-05-23 11:31:59 +00:00
65c77b96b6 feat(ui): добавить Trade-In / Концепция / Админ в landing nav (#438)
Some checks failed
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Failing after 1m32s
Deploy / deploy (push) Has been skipped
2026-05-23 11:11:21 +00:00
f242d9bc44 feat(sf-fe-a12): Landing redesign + PilotCTA с real B3/B4 data (v2 rebased) (#367)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Successful in 1m27s
Deploy / build-worker (push) Successful in 2m35s
Deploy / build-frontend (push) Successful in 4m41s
Deploy / deploy (push) Successful in 45s
Hero + 5 KPI cards from GET /api/v1/landing/stats (B4, real DB), Pricing card 30k руб/мес, FAQ accordion, PilotRequestModal -> POST /api/v1/pilot/request (B3).

Round 2 fixes:
- B4 fail-loud: LandingStatsOut.stale flag, zeroed fallback, frontend skeleton
- B3 UUID contract: id:string, display tracking GD-{first8hex}
- PilotRequestModal: replaced manual overlay with <Drawer side="bottom"> (A3 #341) — focus trap, ESC, scroll-lock, focus restore
- Design tokens: all hex literals replaced with var(--*)
- HeadlineBar component used instead of hand-rolled dark div

Closes #82
Supersedes #366
2026-05-18 05:16:57 +00:00
lekss361
12030c4e76 fix(sf-legacy): фильтр стейл competitors (>1 года Сданные → не конкурент)
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).
2026-05-18 07:29:26 +03:00
lekss361
6350e3f59a feat(sf-legacy): expose EGRN + utilities в Обзор tab
Бэкенд /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).
2026-05-18 07:21:53 +03:00
f6ea27c4f5 Merge pull request 'fix(sf-legacy): velocity=0 → '—' в MarketTab (consistency со «Срок продаж»)' (#362) from fix/legacy-market-velocity-zero-as-null into main
All checks were successful
Deploy / deploy (push) Successful in 39s
Deploy / changes (push) Successful in 4s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m39s
Reviewed-on: #362
2026-05-18 04:16:51 +00:00
lekss361
237b391dfe fix(sf-legacy): treat velocity=0 as 'no data' in MarketTab
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 Срок продаж.
2026-05-18 07:14:22 +03:00
607be225b0 feat(sf-fe-a11): Section 5 «Атмосфера / воздух» (#361)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m50s
Deploy / deploy (push) Successful in 40s
2026-05-18 04:07:48 +00:00
1bbd4e835d fix(sf-fe): decode cad in Server Component (B5 400 regression after #356) (#359)
Some checks failed
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / deploy (push) Blocked by required conditions
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
2026-05-18 04:07:03 +00:00
lekss361
31474deca8 feat(frontend): expose legacy Site Finder at /legacy/site-finder
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/ можно удалить.
2026-05-18 07:04:45 +03:00
91026dcc6f feat(sf-fe-a10): Section 4 «Оценка участка» (#358)
All checks were successful
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / deploy (push) Successful in 45s
Deploy / changes (push) Successful in 4s
Deploy / build-frontend (push) Successful in 3m3s
2026-05-18 03:57:25 +00:00
4d651f00ef feat(sf-fe-a8): Section 3.2 Планировки + 3.3 Остатки/Velocity + CompetitorTable drawer pattern (#357)
All checks were successful
Deploy / build-backend (push) Has been skipped
Deploy / deploy (push) Successful in 48s
Deploy / changes (push) Successful in 4s
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m43s
2026-05-18 01:14:32 +00:00
68b1968826 feat(sf-fe-a7): Section 3.1 «Настройки выборки» + Server/Client split (rebased) (#356)
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy / deploy (push) Successful in 40s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m45s
2026-05-18 00:55:05 +00:00
307a75f333 feat(sf-fe-a6): Section 2 «Сети и точки подключения» — real B5 utilities data (#354)
All checks were successful
Deploy / deploy (push) Successful in 41s
Deploy / changes (push) Successful in 5s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 2m44s
2026-05-18 00:27:55 +00:00