trade-in/v2: подключить боевые ручки к новому UI (/trade-in/v2) #2036

Closed
opened 2026-06-28 09:50:28 +00:00 by lekss361 · 3 comments
Owner

Дочерняя задача к PR #2035 (вёрстка /trade-in/v2 от Макса, пока markup на fixtures.ts).

Цель: заменить sample-fixtures на реальные данные. Текущий API покрывает малую часть макета — ниже разрыв и список ручек.

Что есть сейчас

  • POST /api/v1/trade-in/estimate, GET /api/v1/trade-in/estimate/{id}AggregatedEstimate { median_price_rub, range_low/high_rub, median_price_per_m2, confidence, n_analogs, period_months, analogs[], actual_deals[], expires_at }
  • AnalogLot { address, area_m2, rooms, floor, total_floors, price_rub, price_per_m2, listing_date, days_on_market, photo_url }

A. Расширить ответ estimate

  • 3 раздельные оценки (сейчас один median): «Рекомендованная цена в объявлении» (ad-median+range+₽/м²), «Ожидаемая сделка / получите» (= ad − торг, % к рынку, range), «ДКП · Росреестр» (deal-median+range+₽/м²+n_deals).
  • CV (коэф. вариации) по выборке + «источников X/Y» + per-source breakdown (ЦИАН / Я.Недвижимость / Росреестр / Авито / Домклик / Restate / N1 — count + статус «есть/нет данных»).
  • Координаты для карты: AnalogLot.lat/lon + геокод субъекта (центр-пин + пины аналогов/сделок, радиус-кольца).
  • На AnalogLot: source (Avito/CIAN/…), distance_m. На deal-строках: цена объявления до сделки + торг % + ссылка (Росреестр/объявление).
  • GET /estimate/{id} должен вернуть снимок входных параметров (сейчас не возвращает → на restore object-params неизвестны, v1 TODO в app/trade-in/page.tsx).
  • Параметр radius (300/500/1000/2000 м) во входе + поддержка на бэке.

B. Новые ручки

  • Коэффициент локации: GET /trade-in/location-coef?address|coords → { coef, positives[], negatives[], формула base×coef }. Источник: OSM POI / транспорт / шум-эко слои.
  • История продаж в этом доме (house-level): лоты с ценой нач→посл + экспозиция.
  • ДКП-сделки по улице: { count, median ₽/м², range, rows[] (площадь, цена сделки, ₽/м², дата, ask-до-сделки + торг %) }.
  • Аналитика дома: средняя экспозиция / средний торг / доля снятых (N из total); «срок продажи по тирам цены» ±5/+10% (есть превью SellTimeSensitivity в .design-sync); история цен ₽/м² по годам (серии Avito/Яндекс); scatter цена×срок.
  • Предыдущие оценки (кэш/история юзера): list { address, time, sources, freshness } + KPI (всего оценок, средняя цена, % повторных адресов).
  • PDF-отчёт: GET /estimate/{id}/report.pdf (WeasyPrint) — кнопка «Скачать PDF-отчёт».
  • Геокодер-автокомплит адреса (Yandex / Nominatim) для инпута.
  • CRM (Создать сделку / привязать к контакту / экспорт) — отдельный эпик, опционально.

C. Фронт-wiring (после ручек)

  • Заменить fixtures.ts на TanStack-хуки (lib/trade-in-api.ts); loading / empty / error состояния (skeleton без shimmer); форма 01 → POST estimate; URL ?id; npm run codegen для типов.

Заметки

  • Многое может частично существовать в бэкенд-сервисах (scrapers, rosreestr_deals, backtest spine) — перед реализацией grep кода + проверить какие данные уже считаются, не дублировать.
  • Объём большой → декомпозировать на под-issue по секциям (A/B-пункты) при планировании.

Дизайн-исходник: МЕРА Оценка.dc.html (образцы Макса). Вёрстка-эталон полей: frontend/src/components/trade-in/v2/fixtures.ts.

Дочерняя задача к PR #2035 (вёрстка `/trade-in/v2` от Макса, пока markup на `fixtures.ts`). Цель: заменить sample-fixtures на реальные данные. Текущий API покрывает малую часть макета — ниже разрыв и список ручек. ## Что есть сейчас - `POST /api/v1/trade-in/estimate`, `GET /api/v1/trade-in/estimate/{id}` → `AggregatedEstimate` { `median_price_rub`, `range_low/high_rub`, `median_price_per_m2`, `confidence`, `n_analogs`, `period_months`, `analogs[]`, `actual_deals[]`, `expires_at` } - `AnalogLot` { address, area_m2, rooms, floor, total_floors, price_rub, price_per_m2, listing_date, days_on_market, photo_url } ## A. Расширить ответ estimate - [ ] **3 раздельные оценки** (сейчас один median): «Рекомендованная цена в объявлении» (ad-median+range+₽/м²), «Ожидаемая сделка / получите» (= ad − торг, % к рынку, range), «ДКП · Росреестр» (deal-median+range+₽/м²+n_deals). - [ ] **CV** (коэф. вариации) по выборке + «**источников X/Y**» + **per-source breakdown** (ЦИАН / Я.Недвижимость / Росреестр / Авито / Домклик / Restate / N1 — count + статус «есть/нет данных»). - [ ] **Координаты** для карты: `AnalogLot.lat/lon` + геокод субъекта (центр-пин + пины аналогов/сделок, радиус-кольца). - [ ] На `AnalogLot`: `source` (Avito/CIAN/…), `distance_m`. На deal-строках: цена объявления до сделки + **торг %** + ссылка (Росреестр/объявление). - [ ] `GET /estimate/{id}` должен вернуть **снимок входных параметров** (сейчас не возвращает → на restore object-params неизвестны, v1 TODO в `app/trade-in/page.tsx`). - [ ] Параметр **radius** (300/500/1000/2000 м) во входе + поддержка на бэке. ## B. Новые ручки - [ ] **Коэффициент локации**: `GET /trade-in/location-coef?address|coords` → { coef, positives[], negatives[], формула base×coef }. Источник: OSM POI / транспорт / шум-эко слои. - [ ] **История продаж в этом доме** (house-level): лоты с ценой нач→посл + экспозиция. - [ ] **ДКП-сделки по улице**: { count, median ₽/м², range, rows[] (площадь, цена сделки, ₽/м², дата, ask-до-сделки + торг %) }. - [ ] **Аналитика дома**: средняя экспозиция / средний торг / доля снятых (N из total); «срок продажи по тирам цены» ±5/+10% (есть превью `SellTimeSensitivity` в `.design-sync`); история цен ₽/м² по годам (серии Avito/Яндекс); scatter цена×срок. - [ ] **Предыдущие оценки** (кэш/история юзера): list { address, time, sources, freshness } + KPI (всего оценок, средняя цена, % повторных адресов). - [ ] **PDF-отчёт**: `GET /estimate/{id}/report.pdf` (WeasyPrint) — кнопка «Скачать PDF-отчёт». - [ ] **Геокодер-автокомплит** адреса (Yandex / Nominatim) для инпута. - [ ] **CRM** (Создать сделку / привязать к контакту / экспорт) — отдельный эпик, опционально. ## C. Фронт-wiring (после ручек) - [ ] Заменить `fixtures.ts` на TanStack-хуки (`lib/trade-in-api.ts`); loading / empty / error состояния (skeleton без shimmer); форма 01 → `POST estimate`; URL `?id`; `npm run codegen` для типов. ## Заметки - Многое может частично существовать в бэкенд-сервисах (scrapers, rosreestr_deals, backtest spine) — перед реализацией **grep кода + проверить какие данные уже считаются**, не дублировать. - Объём большой → декомпозировать на под-issue по секциям (A/B-пункты) при планировании. Дизайн-исходник: `МЕРА Оценка.dc.html` (образцы Макса). Вёрстка-эталон полей: `frontend/src/components/trade-in/v2/fixtures.ts`.
lekss361 added the
enhancement
scope/backend
scope/frontend
status/needs-analysis
tradein
labels 2026-06-28 09:50:48 +00:00
Author
Owner

Аудит wiring /v2 → реальные ручки (итог)

Прошёлся по всем секциям tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts против lib/trade-in-api.ts (10 хуков) и backend/app/api/v1/trade_in.py (17 роутов). Бэкенд богатый — большая часть страницы покрывается существующими ручками, остаётся узкий реальный остаток. Декомпозиция на под-issue — следом.

Уже покрыто существующими endpoint'ами (раздел A/C — только FE-wiring + форматирование)

  • ResultPanel — 3 карточки: useEstimate → asking (median_price_rub/range_*/per_m2), ожидаемая сделка (expected_sold_price_rub/range/asking_to_sold_ratio → донат −%), ДКП (dkp_corridor).
  • resultMeta N/7 (sources_used.length, 7 — FE-константа) + confidence.
  • ranges: ads-бар (range_*+median) / deals-бар (useStreetDeals).
  • scatter (mini + detail): данные уже в analogs[]/actual_deals[] (price × days_on_market) — нужна только FE-геометрия.
  • ObjectSummary строки: useEstimate + useEstimateHouseAnalytics.kpi + useStreetDeals.
  • HistoryView: useEstimatePlacementHistory (история в доме) + useStreetDeals/useSalesVsListings (ДКП KPI/шапка/строки, discount_pct).
  • AnalyticsView: useEstimateHouseAnalytics (header + 3 KPI + price_history-полилинии) + useEstimateSellTimeSensitivity (4 тира + эталон ₽/м²).
  • SourcesView (РЫНОК): analogs[]/actual_deals[]AnalogLot.source/distance_m/source_url уже есть (сейчас захардкожены в JSX).
  • Hero/Footer: estimate_id→8-симв. id, expires_at→действителен до.
  • Автокомплит адреса GET /geocode/suggest, PDF GET /estimate/{id}/pdf, список оценок GET /history, квота GET /quota — существуют (нужны только хуки/ссылки).

Важно: хуки useEstimateHouseAnalytics/useStreetDeals/useSalesVsListings/useEstimatePlacementHistory/useEstimateSellTimeSensitivity уже написаны. Текущий app/v2/page.tsx просто не вызывает их — рендерит fixtures и прокидывает только колбэки (nav, drawerOpen).

Реальный остаток (раздел B — данных нет / нет поля)

  1. РАДИУС (300/500/1000/2000 м) — у POST /estimate нет параметра radius (фикс); analytics возвращают radius_m как output. FE-фолбэка нет → нужен net-new backend-параметр, иначе контрол скрыть.
  2. Коэффициент локации + POI (LocationDrawer) — полей/таблиц нет вообще (есть только DaData metro_nearest). Полностью фабрикованный блок. Нужен net-new OSM POI + скоринг + GET /location-coef. Самый дорогой пункт — кандидат на DEFER (или деградировать до фактов по метро).
  3. CV (коэф. вариации)yes-data-no-endpoint: estimator считает std/mean и выбрасывает. → добавить поле cv (FE-фолбэк: посчитать по analogs[].price_per_m2, приблизительно по top-N).
  4. Счётчики по источникам (7 лотов / 1 лот)yes-data-no-endpoint: sources_used[] даёт только active/inactive. → добавить source_counts: dict[str,int] (FE-фолбэк: groupBy по .source, занижает из-за усечения top-N).
  5. CacheView: created_at/status фолбэк FE; колонка ИСТОЧНИКОВ N/7 отсутствует в проекции /history; KPI «средняя цена» + «повторные адреса %» нет endpoint'а (FE-дерайв из списка).
  6. report.date — нет created_at на estimate (yes-data-no-endpoint, есть в /history).
  7. Профиль пользователя (name/org/email/initials) — /me отдаёт только role+username; reports берётся из /quota.
  8. CRM-дропдаун (действия) — endpoint'ов нет; при этом реальные CRM-поля ввода (ownership_type/has_mortgage/client_name/client_phone) есть в TradeInEstimateInput, но не собираются формой.
  9. Косметика без источника: object.city (парсить из адреса), streetView (embed из lat/lon), compass (источника нет → drop), гистограмма-бары (FE-биннинг, декоративно).

План декомпозиции (FE-first → backend)

FE (хук уже есть → чистый wiring + форматирование): page.tsx-шелл → ParamsPanel(форма+submit+?id=) → ResultPanel → ObjectSummary/Hero/Footer → HistoryView → AnalyticsView → SourcesView → PDF/quota/autocomplete → CacheView. Затем backend: cv+source_counts+created_at → проекция /history + cache-KPI → radius_m-параметр → location-coef (или DEFER) → профиль /me. Каждая секция = под-issue + branch + PR.

## Аудит wiring /v2 → реальные ручки (итог) Прошёлся по всем секциям `tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts` против `lib/trade-in-api.ts` (10 хуков) и `backend/app/api/v1/trade_in.py` (17 роутов). Бэкенд богатый — **большая часть страницы покрывается существующими ручками**, остаётся узкий реальный остаток. Декомпозиция на под-issue — следом. ### ✅ Уже покрыто существующими endpoint'ами (раздел A/C — только FE-wiring + форматирование) - [x] **ResultPanel — 3 карточки**: `useEstimate` → asking (`median_price_rub`/`range_*`/`per_m2`), ожидаемая сделка (`expected_sold_price_rub`/range/`asking_to_sold_ratio` → донат −%), ДКП (`dkp_corridor`). - [x] **resultMeta** `N/7` (`sources_used.length`, 7 — FE-константа) + `confidence`. - [x] **ranges**: ads-бар (`range_*`+median) / deals-бар (`useStreetDeals`). - [x] **scatter (mini + detail)**: данные уже в `analogs[]`/`actual_deals[]` (`price × days_on_market`) — нужна только FE-геометрия. - [x] **ObjectSummary** строки: `useEstimate` + `useEstimateHouseAnalytics.kpi` + `useStreetDeals`. - [x] **HistoryView**: `useEstimatePlacementHistory` (история в доме) + `useStreetDeals`/`useSalesVsListings` (ДКП KPI/шапка/строки, `discount_pct`). - [x] **AnalyticsView**: `useEstimateHouseAnalytics` (header + 3 KPI + price_history-полилинии) + `useEstimateSellTimeSensitivity` (4 тира + эталон ₽/м²). - [x] **SourcesView (РЫНОК)**: `analogs[]`/`actual_deals[]` — `AnalogLot.source/distance_m/source_url` уже есть (сейчас захардкожены в JSX). - [x] **Hero/Footer**: `estimate_id`→8-симв. id, `expires_at`→действителен до. - [x] **Автокомплит адреса** `GET /geocode/suggest`, **PDF** `GET /estimate/{id}/pdf`, **список оценок** `GET /history`, **квота** `GET /quota` — существуют (нужны только хуки/ссылки). > Важно: хуки `useEstimateHouseAnalytics/useStreetDeals/useSalesVsListings/useEstimatePlacementHistory/useEstimateSellTimeSensitivity` уже написаны. Текущий `app/v2/page.tsx` просто **не вызывает их** — рендерит fixtures и прокидывает только колбэки (`nav`, `drawerOpen`). ### ⛔ Реальный остаток (раздел B — данных нет / нет поля) 1. **РАДИУС (300/500/1000/2000 м)** — у POST /estimate нет параметра radius (фикс); analytics возвращают `radius_m` как output. FE-фолбэка нет → нужен net-new backend-параметр, иначе контрол скрыть. 2. **Коэффициент локации + POI (LocationDrawer)** — полей/таблиц нет вообще (есть только DaData `metro_nearest`). Полностью фабрикованный блок. Нужен net-new OSM POI + скоринг + `GET /location-coef`. **Самый дорогой пункт** — кандидат на DEFER (или деградировать до фактов по метро). 3. **CV (коэф. вариации)** — `yes-data-no-endpoint`: estimator считает std/mean и выбрасывает. → добавить поле `cv` (FE-фолбэк: посчитать по `analogs[].price_per_m2`, приблизительно по top-N). 4. **Счётчики по источникам (7 лотов / 1 лот)** — `yes-data-no-endpoint`: `sources_used[]` даёт только active/inactive. → добавить `source_counts: dict[str,int]` (FE-фолбэк: groupBy по `.source`, занижает из-за усечения top-N). 5. **CacheView**: `created_at`/`status` фолбэк FE; колонка `ИСТОЧНИКОВ N/7` отсутствует в проекции `/history`; KPI «средняя цена» + «повторные адреса %» нет endpoint'а (FE-дерайв из списка). 6. **report.date** — нет `created_at` на estimate (`yes-data-no-endpoint`, есть в /history). 7. **Профиль пользователя** (name/org/email/initials) — `/me` отдаёт только role+username; `reports` берётся из /quota. 8. **CRM-дропдаун (действия)** — endpoint'ов нет; при этом реальные CRM-поля ввода (`ownership_type/has_mortgage/client_name/client_phone`) есть в `TradeInEstimateInput`, но не собираются формой. 9. Косметика без источника: `object.city` (парсить из адреса), `streetView` (embed из lat/lon), `compass` (источника нет → drop), гистограмма-бары (FE-биннинг, декоративно). ### План декомпозиции (FE-first → backend) FE (хук уже есть → чистый wiring + форматирование): **page.tsx-шелл** → ParamsPanel(форма+submit+?id=) → ResultPanel → ObjectSummary/Hero/Footer → HistoryView → AnalyticsView → SourcesView → PDF/quota/autocomplete → CacheView. Затем backend: `cv`+`source_counts`+`created_at` → проекция /history + cache-KPI → `radius_m`-параметр → location-coef (или DEFER) → профиль /me. Каждая секция = под-issue + branch + PR.
lekss361 removed the
status/needs-analysis
label 2026-06-28 11:00:59 +00:00
Author
Owner

Статус эпика (2026-07-03, работа остановлена по команде user — handoff):

Остаток эпика = 4 под-issue, user снял DEFER с обоих BE («делай»):

  • #2040 (FE-3 AnalyticsView) + #2041 (FE-4 SourcesView) — воркер был запущен (ветка feat/tradein-v2-overlays-wiring, worktree C:/tmp/wt-v2overlays от main 03995458) и остановлен на фазе анализа, кода не написано (worktree чист). Спеки в issue актуальны; хуки из FE-1 и BE-поля cv/source_counts из BE-1 готовы к использованию.
  • #2046 (BE-4 /me профиль) — user: делать. Scout остановлен.
  • #2045 (BE-3 location-coef) — user: делать по образцу реализации в Птице (Site Finder backend) — «посмотри как реализовано». Scout остановлен.

Scout-workflow (4 карты: listings.phones #2217, client_* PII #1969, Ptica POI для #2045, /me для #2046) остановлен на середине; resumable: runId wf_4fdf06b4-191, script ~/.claude/projects/.../workflows/scripts/scout-pii-profile-poi-wf_4fdf06b4-191.js, read-only worktree C:/tmp/wt-scout-main.

FE-1/FE-2/FE-5/BE-1/BE-2 (#2038/#2039/#2042/#2043/#2044) — закрыты ранее. После доделки 4 оставшихся — закрыть эпик.

**Статус эпика (2026-07-03, работа остановлена по команде user — handoff):** Остаток эпика = 4 под-issue, user снял DEFER с обоих BE («делай»): - **#2040 (FE-3 AnalyticsView)** + **#2041 (FE-4 SourcesView)** — воркер был запущен (ветка `feat/tradein-v2-overlays-wiring`, worktree `C:/tmp/wt-v2overlays` от main 03995458) и остановлен на фазе анализа, **кода не написано** (worktree чист). Спеки в issue актуальны; хуки из FE-1 и BE-поля cv/source_counts из BE-1 готовы к использованию. - **#2046 (BE-4 /me профиль)** — user: делать. Scout остановлен. - **#2045 (BE-3 location-coef)** — user: делать по образцу реализации в Птице (Site Finder backend) — «посмотри как реализовано». Scout остановлен. Scout-workflow (4 карты: listings.phones #2217, client_* PII #1969, Ptica POI для #2045, /me для #2046) остановлен на середине; resumable: runId `wf_4fdf06b4-191`, script `~/.claude/projects/.../workflows/scripts/scout-pii-profile-poi-wf_4fdf06b4-191.js`, read-only worktree `C:/tmp/wt-scout-main`. FE-1/FE-2/FE-5/BE-1/BE-2 (#2038/#2039/#2042/#2043/#2044) — закрыты ранее. После доделки 4 оставшихся — закрыть эпик.
Author
Owner

Эпик закрыт — все 4 оставшихся под-issue сделаны, задеплоены, smoke-проверены:

  • #2040 + #2041 (AnalyticsView/SourcesView) — основной wiring оказался уже смёржен ранее (267585e3, 28.06); PR #2298 добил маленький остаток (реальный cv из #2043 + единый source-registry). Smoke: overlay рендерятся, консоль чистая.
  • #2046 (профиль /me) — display_name/org/email в /me, реализовано только реально известное (kopylov→«Копылов», остальное None без фабрикации). PR #2300. Smoke: подтверждено напрямую через /api/v1/me.
  • #2045 (location-coef/POI) — 2 PR: view на gendesign (#2299) + FDW-мост/скоринг-сервис/endpoint в tradein (#2314). Smoke: живой первый прогон моста загрузил 2926 POI, честный coef=0.987 с реальными факторами.

Открыт follow-up #2317 — frontend LocationDrawer ещё не подключён к готовому backend-эндпоинту (это не входило в scope BE-3), чтобы не потерять хвост.

Все PR прошли code-review (APPROVE) и деплоились со спейсингом (по одному, с проверкой деплоя предыдущего) — без rapid-merge коллизий с параллельным окном #2262.

Эпик закрыт — все 4 оставшихся под-issue сделаны, задеплоены, smoke-проверены: - **#2040 + #2041** (AnalyticsView/SourcesView) — основной wiring оказался уже смёржен ранее (267585e3, 28.06); PR #2298 добил маленький остаток (реальный `cv` из #2043 + единый `source-registry`). Smoke: overlay рендерятся, консоль чистая. - **#2046** (профиль /me) — `display_name`/`org`/`email` в `/me`, реализовано только реально известное (kopylov→«Копылов», остальное `None` без фабрикации). PR #2300. Smoke: подтверждено напрямую через `/api/v1/me`. - **#2045** (location-coef/POI) — 2 PR: view на gendesign (#2299) + FDW-мост/скоринг-сервис/endpoint в tradein (#2314). Smoke: живой первый прогон моста загрузил 2926 POI, честный `coef=0.987` с реальными факторами. Открыт follow-up **#2317** — frontend LocationDrawer ещё не подключён к готовому backend-эндпоинту (это не входило в scope BE-3), чтобы не потерять хвост. Все PR прошли code-review (APPROVE) и деплоились со спейсингом (по одному, с проверкой деплоя предыдущего) — без rapid-merge коллизий с параллельным окном #2262.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2036
No description provided.