gendesign/tradein-mvp/docs/ROADMAP.md
lekss361 02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest

Первый запуск на сервере (вручную, один раз):
  - создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
  - docker network create gendesign_shared (если нет)
  - docker compose -p gendesign-tradein up -d
  - docker compose -p gendesign exec caddy caddy reload
2026-05-21 00:25:39 +03:00

439 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Trade-In MVP — Roadmap «Максимум»
> Решение 19.05 вечером: убираем привязку к срокам, делаем максимально хорошо.
> План структурирован по архитектурным слоям, не по дням. Катимся сверху-вниз —
> каждый следующий слой требует, чтобы предыдущий был стабилен.
## Видение продукта
**Что есть у Брусники:** 4-стр PDF с медианой цен, диапазоном, аналогами и сравнением «их обмен vs самопродажа». Один white-label (Брусника). Один продукт.
**Что мы делаем поверх:**
1. **Парсим больше источников** (6 vs ~5 у Брусники): Авито, Циан, Дом.Клик, Я.Недвижимость, Н1, Дом.РФ
2. **Кликабельные ссылки** на каждый аналог — Брусника не даёт
3. **Real-time freshness** — индикатор «обновлено N минут назад» на каждом источнике
4. **Объяснимый confidence**`high (12 аналогов, разброс ±8%)` вместо «качество данных может быть не очень»
5. **POI окружение** — школы / метро / магазины с decay по расстоянию (переиспользуем код gendesign Site Finder)
6. **Тренд цен** — график за 6/24 мес. в этом районе
7. **Map view** — карта с точками аналогов + heatmap
8. **Multi-tenant white-label** — PRINZIP / Практика / любой бренд через query param
9. **JSON API + iframe widget** — для интеграции в чужие CRM
10. **Похожие сделки в том же доме** — точные совпадения если есть
11. **AI narrative summary** — короткое описание квартиры от LLM
12. **Прогноз срока продажи** — median days_on_market по выборке
13. **Score breakdown explainability** — почему именно эта цена (как Site Finder)
14. **Photo upload** на cover PDF
15. **Shareable URL** + QR-код в PDF
---
## Слой 1 — Core data layer (фундамент)
Без него ничего не работает. Каждый последующий слой строится поверх.
### 1.1 Postgres schema
- `listings` — объявления (id, source, source_url, lat, lon, address, rooms, area, floor, total_floors, year_built, house_type, repair_state, price, price_per_m2, photo_urls jsonb, listing_date, scraped_at, dedup_hash, raw_payload jsonb, geom GEOMETRY(Point, 4326))
- `deals` — фактические сделки (тот же набор + deal_date, source = 'rosreestr' пока)
- `geocode_cache` — address → lat/lon кэш (избегаем повторных Yandex hits)
- `audit_log` — каждый POST /estimate (ip, user_agent, input, estimate_id, ts)
- `house_metadata` — кэш данных ЕКБ Геопортала по координатам/кадастру (этажи, год, материал, кол-во квартир)
- Индексы: GIST на geom, btree на (rooms, scraped_at), unique на dedup_hash
### 1.2 Geocoder + Suggest
- `app/services/geocoder.py` — Yandex Geocoder через httpx + кэш в `geocode_cache`
- `app/services/suggest.py` — Yandex Suggest для frontend autocomplete (proxy через backend, не светим API key в браузере)
- Fallback к Nominatim если Яндекс упал / лимит
### 1.3 Scraper framework
- `app/services/scrapers/base.py` — общий BaseScraper
- Retry через `tenacity` (3 попытки, exp backoff)
- Rotation: пул из 5-10 User-Agent + Accept-Language
- Sleep 5-10 sec между запросами (без бана)
- Поддержка Playwright если scrapy не справляется (Циан иногда JS-render требует)
- Дедупликация: `dedup_hash = sha256(source + source_url + price)` уникальный
- Каждый scraper возвращает list[ScrapedLot] — единая Pydantic схема
---
## Слой 2 — Парсеры источников
### 2.1 Авито (приоритет 1 — стартуем с него)
- URL: `/ekaterinburg/kvartiry/prodam?radius=N&geoCoords=lat,lon&list_view=1`
- Парс HTML через `selectolax` (быстрее BeautifulSoup)
- Поля: address, rooms, area, floor/total_floors, price, listing_date, photo, deeplink
- Edge case: «частный риэлтор» vs «агентство» — фильтруем спам
### 2.2 Циан ✅ ДЕПЛОЕН
- URL: `https://ekaterinburg.cian.ru/cat.php?...`
- ВАЖНО: Циан банит httpx по TLS fingerprint → `curl_cffi(impersonate="chrome120")`
- 2025+ верстка БЕЗ `__NEXT_DATA__` — парсим DOM по `data-mark` атрибутам:
`OfferTitle`, `MainPrice`, `Description`; адрес из множественных `data-name="GeoLabel"`
- Multi-room scrape: `fetch_around_multi_room()` по rooms=1/2/3/4 → ~108 unique lots
- Cron каждый час: `45 * * * * /opt/tradein-mvp/deploy/cron-scrape.sh cian fast`
### 2.3 Росреестр сделки
- Дамп из prod gendesign `rosreestr_deals` (6.83M строк, partitioned по кварталам)
- SSH туннель → `pg_dump --table rosreestr_deals --where "region_code = 66"` → restore локально
- Регулярный refresh: weekly Celery task
### 2.4 Дом.Клик
- API: `https://api.domclick.ru/...` (публичный, без auth, JSON)
- Поля: те же + ипотечные опции (мы пока игнорим)
### 2.5 Я.Недвижимость ✅ ДЕПЛОЕН
- URL: `https://realty.yandex.ru/ekaterinburg/kupit/kvartira/?rgid=240970&offerType=SELL`
- State в `<script id="initial_state_script">` JSON; path `state.map.offers.points`
- DATE_DESC возвращает одинаковые 20 лотов независимо от geoLat/geoLon
- Multi-room (`STUDIO/1/2/3/PLUS_4`) даёт 5× expansion → ~100 unique/anchor
- Deep mode (5 rooms × 3 sorts × 2 pages = 30 запросов / ~150s) → ~300 unique/anchor
- EXACT coords из `offer.location.point` (precision: EXACT) — без anchor jitter
- Cron каждые 3 часа: `0 */3 * * * /opt/tradein-mvp/deploy/cron-scrape.sh yandex deep`
### 2.6 Н1.ru (региональный, Урал)
- HTML scrape, очень простая структура
- Сильнее представлен в ЕКБ чем федералы
### 2.7 Дом.РФ (наш.дом.рф)
- API `kn-api` уже есть в gendesign (Playwright scraper)
- Только новостройки (ДДУ) — для вторички не критично, опционально
### 2.8 ЕКБ Геопортал (citymap.ekburg.ru)
- WMS/WFS API для зданий
- По lat/lon отдаёт: этажность, год постройки, материал, кол-во квартир, кадастровый номер
- Кэш в `house_metadata`
---
## Слой 3 — Aggregation model
### 3.1 Замена `_mock_estimate()` на реальный SQL
```sql
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY price_per_m2) AS median_ppm2,
percentile_cont(0.25) WITHIN GROUP (ORDER BY price_per_m2) AS q1_ppm2,
percentile_cont(0.75) WITHIN GROUP (ORDER BY price_per_m2) AS q3_ppm2,
COUNT(*) AS n_analogs
FROM listings
WHERE ST_DWithin(geom, ST_MakePoint(:lon, :lat)::geography, 800)
AND rooms = :rooms
AND area BETWEEN :area * 0.85 AND :area * 1.15
AND scraped_at > NOW() - interval '14 days'
```
### 3.2 Confidence score (объяснимый)
- `n_analogs ≥ 10 AND IQR/median < 0.15``high` («Найдено 12 аналогов, разброс цены ±8%»)
- `n_analogs ≥ 5 OR IQR/median < 0.25``medium`
- иначе → `low` («Только 3 аналога в радиусе 800м, рекомендуем проверить ручной поиск»)
### 3.3 Outlier detection
- Tukey fence: цена вне `[Q1 - 1.5*IQR, Q3 + 1.5*IQR]` → исключаем
- Логи: сколько отфильтровали
### 3.4 House match (приоритетные аналоги)
- Если есть листинг с тем же кадастровым номером дома → отдельно показываем как «в этом же доме»
- Соседние дома (тот же квартал) — следующий приоритет
### 3.5 Price trend
- Group by month, last 6/12/24 мес.
- Linear regression slope → "цены растут на N% / год"
### 3.6 Прогноз days_on_market
- Median + percentile по выборке
- «Аналогичные квартиры продаются за 30-90 дней»
---
## Слой 4 — Дифференциация (фишки выше Брусники)
### 4.1 POI окружение
- Reuse `app/services/poi_score.py` из gendesign
- Категории: school / kindergarten / metro_stop / bus_stop / shop_mall / shop_supermarket / park / hospital / pharmacy
- Distance decay (gendesign формула)
- Bonus к привлекательности района (не к цене, отдельно)
### 4.2 Map view
- Leaflet с пинами на каждый аналог
- Цветовая heatmap цен по м²
- Кликабельные popup'ы
### 4.3 AI narrative summary
- LLM call (Claude / GPT) → 2-3 предложения о квартире
- Промпт: «На основе следующих данных опиши квартиру: ...»
- Пример: «Двушка на Малышева в кирпичном доме 1985 года, требует ремонта. Цена в верхнем квартиле по району из-за центра.»
### 4.4 White-label
- Query param `?brand=prinzip|praktika|generic`
- В DB табличка `brands` (slug, logo_url, primary_color, footer_text)
- Header + PDF cover читают из brand
### 4.5 Shareable + QR
- URL `/trade-in?id=<uuid>` ✓ done
- В PDF cover — QR-код этого URL (через `segno` lib)
### 4.6 Photo upload
- `<input type="file" multiple accept="image/*">`
- Upload в `static/photos/<estimate_id>/`
- Первая фото → cover PDF
---
## Слой 5 — UX
### 5.1 Address autocomplete
- Yandex Suggest (через backend proxy)
- Debounced 300ms
- Показывать full address с городом
### 5.2 Кликабельные analogs
- В `AnalogsTable.tsx``<a href={listing.source_url} target="_blank">`
- Логотип источника рядом (Авито/Циан/...)
### 5.3 Loading states
- `EstimateProgress.tsx` уже есть скелет — заполнить:
- «Геокодируем адрес...»
- «Парсим Циан (12 объявлений)...»
- «Парсим Авито (28 объявлений)...»
- «Считаем медиану...»
- «Готово!»
### 5.4 Empty / error states
- 0 аналогов в 800м → fallback 2 км + warning
- Адрес не распознан → suggest альтернативы
- Парсер упал → grace degradation (показываем что есть)
### 5.5 Mobile + iPad
- Sticky form слева → переход в полноэкранный modal на mobile
- Tap-friendly buttons (min 44px)
- iPad: 2-column layout сохраняется
### 5.6 Dark mode (низкий приоритет)
- CSS vars уже подготовлены — добавить `@media (prefers-color-scheme: dark)` overrides
---
## Слой 6 — PDF
### 6.1 Кликабельные ссылки
- WeasyPrint поддерживает `<a href>` — просто добавить в шаблон
- В таблице аналогов адрес делаем линком
### 6.2 White-label header
- Logo + название бренда читается из `brands` таблицы
- PDF cover: бренд-цвет accent
### 6.3 График цен (стр. 3)
- Render chart как SVG через Python (`matplotlib` → SVG) или send to backend chart-renderer
- Линия median + диапазон Q1-Q3 + точки сделок
### 6.4 Карта на cover
- Static map image от Yandex Static Maps API
- Пин на квартире + circle радиуса поиска
### 6.5 QR-code
- `segno.make(shareable_url).save(buffer)` → embed в HTML template
### 6.6 Freshness footer
- «Данные собраны 19.05.2026 23:15. Источники: Авито (47), Циан (38), Росреестр сделки (12). Confidence: high.»
---
## Слой 7 — Auth + Analytics + Monitoring
(отложено из MVP, добавим перед demo Геныча/Тёмы)
### 7.1 Простой password gate
- Middleware: проверка cookie/header с shared secret
- Set-cookie на 30 дней при правильном вводе
- HTML form `/login` с одним полем «Пароль»
### 7.2 Audit log
- Каждый POST /estimate → INSERT в `audit_log` (ip, ua, input, output uuid)
- Каждый GET /estimate/{id}/pdf → INSERT
- Каждый click на оригинал листинга — если хотим точно (frontend → POST /analytics/event)
### 7.3 Analytics dashboard
- `/admin/analytics` — простая страница: топ-адреса, статистика по дням, conversion (form_open → form_submit → pdf_download)
- Или PostHog self-hosted (если хочется UI без писательства)
### 7.4 Rate limiting
- slowapi (FastAPI-friendly) — 10 POST /estimate per minute per IP
### 7.5 Monitoring
- Backend logs → файл + опционально Loki
- Health checks Caddy перепингует backend каждые 30 сек
---
## Слой 8 — Deployment & Production
### 8.1 Smoke test
- `make smoke-test` bash: 5 курлов на разные адреса ЕКБ → проверка HTTP 200 + confidence != low
- Запускать в CI и перед каждым деплоем
### 8.2 Backup strategy
- Cron pg_dump → S3 daily (или local если S3 нет)
- Test restore раз в неделю
### 8.3 Production deploy
- Опция A: deploy в существующий gendesign monorepo (просто merge изменений TI-2/TI-3 + новые парсеры) → выкатить на gendsgn.ru/trade-in
- Опция B: отдельный subdomain `tradein.gendsgn.ru` через отдельный docker stack на том же VPS
- Опция C: совсем отдельный VPS / отдельная репа в Forgejo
Решим когда дойдём — пока локально.
### 8.4 Multi-tenant infrastructure
- White-label через subdomain: `prinzip.tradein.gendsgn.ru` / `praktika.tradein.gendsgn.ru`
- Caddy wildcard cert + tenant detection из Host header
---
## Слой 9 — Advanced (после core готов)
### 9.1 ML-модель цены
- XGBoost regression на исторических сделках Росреестра
- Features: rooms, area, floor/total_floors, year, distance_to_center, POI score
- Сравнение с simple median: насколько лучше / стабильнее
### 9.2 iframe widget
- `<iframe src="https://tradein.gendsgn.ru/embed?brand=prinzip&width=400">`
- Минимальная форма + результат
- Для интеграции в чужие CRM
### 9.3 JSON API для интеграций
- API key auth
- POST /api/v2/trade-in/estimate с rate limits
- Документация Swagger
### 9.4 Push notifications
- «В вашем районе 2-к продалась за 11.5M — на 8% ниже вашей оценки»
- WebPush или Telegram bot
### 9.5 PRINZIP CRM integration
- API call в их 102K заявок
- Авто-оценка по конверту заявки
### 9.6 Птица integration
- Подсветка домов где ≥20% квартир в Циан/Авито
- Карта расселения + потенциальная стоимость участка
### 9.7 Multi-region
- После ЕКБ — Пермь, Челябинск, Новосибирск, Москва, СПб
- Yandex Geocoder покрывает всё
---
## Что уже есть (стартовая позиция)
✅ Backend: FastAPI + `/estimate` (mock) + `/estimate/{id}` + PDF endpoint + Pydantic schemas
✅ Frontend: Next.js page + 5 компонентов (EstimateForm/Result/Progress/PriceRangeBar/AnalogsTable)
✅ Infra: docker-compose (caddy + frontend + backend + postgres) запущен на localhost:8080
✅ Postgres: таблица `trade_in_estimates` создаётся автоматом
✅ PDF: WeasyPrint 4-page (cover/listings/deals/offer) работает
✅ Shareable URL: `/trade-in?id=<uuid>`
✅ Reference docs: PDF Брусники + PDF Птицы в `docs/`
## Что сделано ночью 19→20 мая 2026
### Слой 1 — Core data layer
-**1.1** PostgreSQL schema: listings + deals + geocode_cache + audit_log + house_metadata + brands + PostGIS GIST индексы + auto-update geom trigger
-**1.2** Geocoder service (Yandex когда дашь key + Nominatim fallback) с кэшем 90 дней. Live тест: «ул. Малышева 30 Екатеринбург» → (56.8333, 60.5945), confidence exact
-**1.3** Scraper framework — BaseScraper async context + tenacity retry + 5 ротируемых User-Agent + ScrapedLot единая Pydantic схема + save_listings с дедуп
### Слой 2 — Парсеры
-**2.1 Авито парсер** — код готов (`scrapers/avito.py`), 2 стратегии (mobile API + HTML scrape). С маки через V2Box банится (302/429) — заработает с VPS Beget.
- 💾 **Seed данные** — 120 листингов + 60 сделок на 10 центральных улицах ЕКБ, реалистичное распределение (12 студий / 30 1к / 45 2к / 25 3к / 8 4к) с jitter цен и координат для тестирования
### Слой 3 — Aggregation
-**3.1** SQL aggregation поверх listings + deals: PostGIS `ST_DWithin` 800м (fallback 2 км) + комнаты ± 0 + площадь ±15% + Tukey IQR outlier filter + median/Q1/Q3 + confidence high/medium/low с человекочитаемым объяснением
### Слой 5 + 6 — Дифференциация vs Брусники
-**5.2** Frontend AnalogsTable теперь:
- **Source badges** цветные (avito/cian/domklik)
- **Distance column** (167м / 628м / 1.2км)
- **Кликабельные строки** — открывают оригинальное объявление в новой вкладке
- **Confidence explanation плашка** под медианой «Найдено 15 аналогов, разброс ±7%»
- **Source pills** + **freshness indicator** «● обновлено N мин назад»
-**6.1** PDF Listings/Deals таблицы: source badges + distance + кликабельные `<a href>` адреса (WeasyPrint)
-**6.5** **QR-код на cover** PDF (`segno` lib) — шёрить отчёт через QR scan
-**6.6** Freshness footer в PDF + список источников
### Слой 4 — White-label
-**4.4** Multi-tenant brands в Postgres (generic / prinzip / praktika) + `GET /api/v1/brand/{slug}` endpoint + `/pdf?brand=prinzip` query param. PDF header адаптируется (PRINZIP → чёрный header, generic → синий)
## Что НЕ сделано пока
### Слой 2 — Парсеры (отложено до prod deploy)
- 🟡 ЦИАН парсер — фреймворк готов, нужен код. Прод-IP обойдёт ban
- 🟡 Росреестр дамп из gendesign-postgres-1 (через SSH туннель)
- 🟡 ДомКлик, Я.Недвижимость, Н1, Дом.РФ
### Слой 3 — Advanced aggregation
- 🟡 House match — приоритетные аналоги в том же доме
- 🟡 Price trend regression (last 6mo slope) для «цены растут N%/год»
- 🟡 Прогноз days_on_market для «продаётся за 30-90 дней»
### Слой 4 — Доп. дифференциация
- 🟡 POI окружение (reuse gendesign) — школы/метро/магазины с decay
- 🟡 Map view (Leaflet) — пины + heatmap цен
- 🟡 AI narrative summary (LLM call) — описание квартиры
- 🟡 Photo upload на cover PDF
### Слой 5 — UX
- 🟡 Yandex Suggest autocomplete (нужен Yandex key)
- 🟡 Loading states «Парсим Циан... Авито... Расчёт...»
- 🟡 Empty state fallback
- 🟡 Mobile / iPad responsive
### Слой 6 — PDF extra
- 🟡 График цен на странице 3
- 🟡 Карта c пином на cover (Yandex Static Maps API)
- 🟡 Photo на cover (если загружено)
### Слой 7 — Auth + Analytics
- 🟡 Shared password middleware
- 🟡 Audit log INSERT с каждого POST /estimate
- 🟡 PostHog / analytics dashboard
- 🟡 Rate limiting
### Слой 8 — Deploy
- 🟡 Production deploy (gendsgn.ru/trade-in OR tradein.gendsgn.ru subdomain)
- 🟡 Backup cron pg_dump → S3
- 🟡 Smoke-test bash script
- 🟡 CI с авто-парсингом для freshness
### Слой 9 — Advanced
- 🟡 ML модель цены (XGBoost)
- 🟡 iframe widget
- 🟡 JSON API + key auth
- 🟡 PRINZIP CRM integration (102K заявок)
- 🟡 Птица integration (расселение)
- 🟡 Multi-region (Москва, СПб)
---
## Приоритет реализации
Иду сверху вниз, но внутри слоя могу параллелить независимые таски (например, 2.1 Авито и 2.2 Циан — независимые).
**Критический путь** (минимум чтобы вообще работало):
1.1 → 1.2 → 1.3 → 2.1 → 3.1 → 3.2 → 5.2 → 6.1
Всё остальное — incremental quality wins поверх.
---
## Решения 19.05.2026
- ✅ Геокодинг: **Yandex Geocoder + Suggest API**
- ✅ Источники к MVP: **Авито + Циан + Росреестр (дамп с prod)**, затем +ДомКлик/Я.Нед/Н1/Дом.РФ
- ✅ Auth: отложен в Слой 7 (добавим перед demo)
- ✅ Срок: убран, делаем максимально хорошо
- ✅ Деплой: пока локально, продакшен решим когда дойдём
## Что нужно от Антона
1. **Yandex Geocoder API key** — registration на developer.tech.yandex.ru (10 мин). Альтернатива — переиспользуем gendesign'овский если есть
2. Подтверждение SSH доступа к prod (gendesign_deploy ключ уже работает) для `pg_dump rosreestr_deals`
3. **Logo PRINZIP** (svg/png) — когда дойдём до Слоя 6.2, не блокирует старт
4. Решение по production deploy (Слой 8.3) — но это позже