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

23 KiB
Raw Blame History

Trade-In MVP — Roadmap «Максимум»

Решение 19.05 вечером: убираем привязку к срокам, делаем максимально хорошо. План структурирован по архитектурным слоям, не по дням. Катимся сверху-вниз — каждый следующий слой требует, чтобы предыдущий был стабилен.

Видение продукта

Что есть у Брусники: 4-стр PDF с медианой цен, диапазоном, аналогами и сравнением «их обмен vs самопродажа». Один white-label (Брусника). Один продукт.

Что мы делаем поверх:

  1. Парсим больше источников (6 vs ~5 у Брусники): Авито, Циан, Дом.Клик, Я.Недвижимость, Н1, Дом.РФ
  2. Кликабельные ссылки на каждый аналог — Брусника не даёт
  3. Real-time freshness — индикатор «обновлено N минут назад» на каждом источнике
  4. Объяснимый confidencehigh (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

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.15high («Найдено 12 аналогов, разброс цены ±8%»)
  • n_analogs ≥ 5 OR IQR/median < 0.25medium
  • иначе → 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
  • «Данные собраны 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) — но это позже