- 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
23 KiB
Trade-In MVP — Roadmap «Максимум»
Решение 19.05 вечером: убираем привязку к срокам, делаем максимально хорошо. План структурирован по архитектурным слоям, не по дням. Катимся сверху-вниз — каждый следующий слой требует, чтобы предыдущий был стабилен.
Видение продукта
Что есть у Брусники: 4-стр PDF с медианой цен, диапазоном, аналогами и сравнением «их обмен vs самопродажа». Один white-label (Брусника). Один продукт.
Что мы делаем поверх:
- Парсим больше источников (6 vs ~5 у Брусники): Авито, Циан, Дом.Клик, Я.Недвижимость, Н1, Дом.РФ
- Кликабельные ссылки на каждый аналог — Брусника не даёт
- Real-time freshness — индикатор «обновлено N минут назад» на каждом источнике
- Объяснимый confidence —
high (12 аналогов, разброс ±8%)вместо «качество данных может быть не очень» - POI окружение — школы / метро / магазины с decay по расстоянию (переиспользуем код gendesign Site Finder)
- Тренд цен — график за 6/24 мес. в этом районе
- Map view — карта с точками аналогов + heatmap
- Multi-tenant white-label — PRINZIP / Практика / любой бренд через query param
- JSON API + iframe widget — для интеграции в чужие CRM
- Похожие сделки в том же доме — точные совпадения если есть
- AI narrative summary — короткое описание квартиры от LLM
- Прогноз срока продажи — median days_on_market по выборке
- Score breakdown explainability — почему именно эта цена (как Site Finder)
- Photo upload на cover PDF
- 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_cacheapp/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; pathstate.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.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 (через
segnolib)
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-testbash: 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_DWithin800м (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 (
segnolib) — шёрить отчёт через 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=prinzipquery 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)
- ✅ Срок: убран, делаем максимально хорошо
- ✅ Деплой: пока локально, продакшен решим когда дойдём
Что нужно от Антона
- Yandex Geocoder API key — registration на developer.tech.yandex.ru (10 мин). Альтернатива — переиспользуем gendesign'овский если есть
- Подтверждение SSH доступа к prod (gendesign_deploy ключ уже работает) для
pg_dump rosreestr_deals - Logo PRINZIP (svg/png) — когда дойдём до Слоя 6.2, не блокирует старт
- Решение по production deploy (Слой 8.3) — но это позже