gendesign/tradein-mvp
Light1YT da8125e695 feat(rbac): frontend route-guard + nav filter for pilot/admin scopes
PR B (frontend) к backend RBAC из PR #585. Реализует требования из
Telegram-сессии 2026-05-25: «сверху в баре странички в зависимости от
роли», «если доступа нет — слать нахуй», «человек без ролей вообще
ничего не видит».

Main frontend (`frontend/`):
- `src/lib/useMe.ts` — TanStack `useMe()` hook, `staleTime: Infinity`
  (роль не меняется в рамках сессии); читает GET /api/v1/me.
- `src/lib/isPathAllowed.ts` — JS port `backend/app/core/auth.py`
  `_glob_to_regex`. DSTAR/SSTAR sentinels, `/foo/**` matches `/foo` тоже.
- `src/lib/__tests__/isPathAllowed.test.ts` — parity tests vs backend
  `test_rbac.py::test_is_path_allowed_*` (admin everywhere, pilot blocked
  from /admin/**, unknown role denied).
- `src/components/auth/RouteGuard.tsx` — wraps app в layout. На 403 от /me
  → `<NoAccessScreen variant="user">`. На denied path → `<NoAccessScreen
  variant="path">`. Loading state — `null` чтобы не было FOUC protected
  UI. 401 (dev без Caddy) — pass-through.
- `src/components/auth/NoAccessScreen.tsx` — fullscreen «доступа нет» с
  lucide `<Lock>` иконкой. Token-based styling.
- `src/components/landing/TopNav.tsx` — extracted nav из `page.tsx`.
  Фильтрует items через `isPathAllowed(scope, href)`. Pilot → нет «Админ».
- `src/app/{layout.tsx,page.tsx}` — обернуть в `<RouteGuard>`, inline nav
  → `<TopNav rightSlot=…>`.

Tradein mirror (`tradein-mvp/frontend/`):
- `src/lib/{useMe,isPathAllowed}.ts` — MIRROR. `useMe` использует
  `NEXT_PUBLIC_BASE_PATH`-aware URL (`/trade-in/api/v1/me`).
- `src/components/auth/{RouteGuard,NoAccessScreen}.tsx` — MIRROR.
  RouteGuard prepends `basePath` перед scope check (т.к. `usePathname()`
  strips basePath в Next.js 15 app router).
  NoAccessScreen — tokenized colors (var(--bg-app), --fg-primary, etc.)
  per `.claude/rules/ui-tokens.md`. Без lucide (отдельный bundle без
  shared deps).
- `src/components/trade-in/Topbar.tsx` — scraper tabs (Avito/Cian/Yandex/
  N1) скрыты для pilot (`scopePath` синтетически указывает на
  `/trade-in/api/v1/admin/*` для filter logic).
- `src/app/layout.tsx` — wrap children в `<RouteGuard>`.

Security verification (code-reviewer LGTM):
- pilot → /admin/* blocked: 
- ghost (403) → ничего не видно: 
- 401 fallback (dev) — safe (prod Caddy basic_auth гарантирует header).

UX nit: pilot direct URL `/trade-in/scrapers/avito` (UI page) не блочен
RouteGuard'ом — yaml allows `/trade-in/**`. Но data fetches к
`/trade-in/api/v1/admin/*` будут 403, страница покажет пустое состояние.
Follow-up: добавить `/trade-in/scrapers/**` в `pilot.deny` если нужен
hard-block UI (out-of-scope этого PR).

Tests: 14 files, +834/-91. `npm run type-check` нужно прогнать в CI
после merge (node_modules не доступны локально).
2026-05-26 11:42:03 +05:00
..
backend feat(rbac): role-based access control via X-Authenticated-User middleware (#585) 2026-05-26 06:18:40 +00:00
deploy fix(tradein): filter ДКП-only in rosreestr importer + re-enable deals (#549) 2026-05-24 19:42:28 +00:00
docs feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
frontend feat(rbac): frontend route-guard + nav filter for pilot/admin scopes 2026-05-26 11:42:03 +05:00
scripts feat(tradein/scripts): local Playwright runners для Cian backfill (bypass server-IP anti-bot) (#553) 2026-05-24 20:00:59 +00:00
.env.example feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
.gitignore feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
DEPLOY.md fix(tradein/devops): wire COOKIE_ENCRYPTION_KEY env to tradein-backend (Calculator was broken on prod) 2026-05-24 18:58:51 +03:00
docker-compose.prod.yml feat(rbac): role-based access control via X-Authenticated-User middleware (#585) 2026-05-26 06:18:40 +00:00
docker-compose.yml feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
Makefile feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
README.md feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00

Trade-In MVP

Локальный standalone-форк фичи Trade-In Estimator из проекта gendesign — оценка выкупной стоимости квартиры на вторичном рынке по аналогам и реальным сделкам. Layout повторяет PDF-отчёт «Брусника.Обмен» (см. docs/).

Что это и откуда взято

Источник Что Где
gendesign/main PR #316 TI-1 mock endpoint + Pydantic + SQL migration backend/
gendesign/main PR #317 TI-3 Next.js страница + 5 компонентов + hooks frontend/
gendesign/main PR #319 TI-2 PDF export 4 страницы (как у Брусники) backend/app/services/exporters/
gendesign/main PR #283 статичный tradein.html mockup (для Геныча) frontend/public/tradein.html
Встреча 19.05.2026 («Птица») требования к MVP оценки вторички docs/PTITSA_MEETING_2026-05-19.pdf
PDF Брусники EКБ-2485 референс layout-а отчёта docs/BRUSNIKA_REFERENCE_EKB-2485.pdf

Быстрый старт

make up              # build + up весь стек (caddy + frontend + backend + postgres)
open http://localhost:8080

Откроется / → автоматически редирект на /trade-in. Заполняешь форму (адрес/площадь/комнаты/этаж/...), нажимаешь «Оценить» — backend возвращает mock-оценку, фронт показывает median + диапазон цен + список аналогов.

Проверка backend напрямую:

make test-estimate
# или вручную:
curl -sS -X POST http://localhost:8080/api/v1/trade-in/estimate \
  -H 'Content-Type: application/json' \
  -d '{"address":"ул. Малышева, 1","area_m2":54,"rooms":2,"floor":5,"total_floors":17}' \
  | python3 -m json.tool

OpenAPI документация: http://localhost:8000/docs

Для сравнения макет vs реальная фича:

Структура

tradein-mvp/
├── docker-compose.yml         # caddy + frontend + backend + postgres
├── Makefile                   # удобные команды (up/down/logs/test-estimate)
├── deploy/
│   └── Caddyfile              # local reverse-proxy на http://localhost:8080
├── backend/                   # FastAPI + WeasyPrint
│   ├── Dockerfile
│   ├── pyproject.toml
│   ├── app/
│   │   ├── main.py            # FastAPI entry — только trade-in router
│   │   ├── core/
│   │   │   ├── config.py      # минимальный pydantic-settings
│   │   │   └── db.py          # SQLAlchemy engine + get_db
│   │   ├── api/v1/
│   │   │   └── trade_in.py    # 3 endpoint'а: POST /estimate, GET /estimate/{id}, GET /estimate/{id}/pdf
│   │   ├── schemas/
│   │   │   └── trade_in.py    # Pydantic: TradeInEstimateInput / AnalogLot / AggregatedEstimate
│   │   └── services/exporters/
│   │       └── trade_in_pdf.py  # WeasyPrint → 4-страничный PDF (cover/listings/deals/offer)
│   └── data/sql/
│       └── 001_trade_in_estimates.sql  # CREATE TABLE; применяется при первом старте postgres
├── frontend/                  # Next.js 15 + React 19 + TanStack Query
│   ├── Dockerfile
│   ├── package.json
│   ├── next.config.ts         # rewrites /api/* → backend
│   ├── tsconfig.json
│   ├── src/
│   │   ├── app/
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx       # redirect → /trade-in
│   │   │   ├── globals.css
│   │   │   ├── providers.tsx  # QueryClientProvider
│   │   │   └── trade-in/
│   │   │       └── page.tsx
│   │   ├── components/trade-in/
│   │   │   ├── EstimateForm.tsx       # форма ввода (sticky 360px)
│   │   │   ├── EstimateProgress.tsx   # индикатор «Парсим Циан → Авито → ...»
│   │   │   ├── EstimateResult.tsx     # карточка результата
│   │   │   ├── PriceRangeBar.tsx      # визуализация диапазона цен (как у Брусники)
│   │   │   └── AnalogsTable.tsx       # таблица аналогов
│   │   ├── lib/
│   │   │   ├── api.ts                 # apiFetch + HTTPError
│   │   │   ├── sessionId.ts           # X-Session-Id из localStorage
│   │   │   └── trade-in-api.ts        # useEstimateMutation + useEstimate hooks
│   │   └── types/
│   │       └── trade-in.ts            # TS типы зеркалят Pydantic schemas
│   └── public/
│       └── tradein.html              # статичный mockup от 17.05 (для side-by-side review)
└── docs/
    ├── BRUSNIKA_REFERENCE_EKB-2485.pdf  # эталон layout-а
    └── PTITSA_MEETING_2026-05-19.pdf    # AI-протокол встречи с требованиями

API

POST /api/v1/trade-in/estimate — оценить квартиру.

Запрос:

{
  "address": "ул. Малышева, 1, кв. 5, Екатеринбург",
  "area_m2": 54.0,
  "rooms": 2,
  "floor": 5,
  "total_floors": 17,
  "year_built": 1985,
  "house_type": "panel",
  "repair_state": "good",
  "has_balcony": true
}

Ответ:

{
  "estimate_id": "...uuid...",
  "median_price_rub": 13125000,
  "range_low_rub": 11550000,
  "range_high_rub": 14700000,
  "median_price_per_m2": 243056,
  "confidence": "high",
  "n_analogs": 8,
  "period_months": 24,
  "analogs": [ {"address": "...", "area_m2": 56, "price_rub": 12700000, ...} ],
  "actual_deals": [ ... ],
  "expires_at": "2026-05-20T22:48:00Z"
}

GET /api/v1/trade-in/estimate/{id} — получить сохранённую оценку (TTL 24ч) GET /api/v1/trade-in/estimate/{id}/pdf — скачать 4-страничный PDF (cover / listings / deals / offer)

Что внутри _mock_estimate() (текущая реализация)

Формула:

price = base_price_by_rooms × floor_factor × repair_factor
Поле Значения
Базовая цена ЕКБ 2026 студия 6.5M (260K/м²) · 1к 9.0M (225K/м²) · 2к 12.5M (208K/м²) · 3к 17.0M (213K/м²)
floor_factor 1-й этаж = ×0.95, последний = ×0.97, остальные = ×1.00
repair_factor needs_repair = ×0.90, standard = ×1.00, good = ×1.05, excellent = ×1.10
confidence 1-3 комнаты = high, остальные = medium
Улицы аналогов реальные центральные ЕКБ (Малышева, Куйбышева, 8 Марта, Белинского, пр. Ленина, Толмачёва, Радищева, Мамина-Сибиряка, Луначарского, Первомайская)

Каждая оценка сохраняется в trade_in_estimates с TTL 24 часа — UUID можно использовать для shareable links и PDF-экспорта.

Roadmap — что доделать

Phase 1 — заменить mock на реальные данные (TODO TI-1b из gendesign)

Сейчас _mock_estimate() возвращает хардкод. На встрече Птица 19.05 решили:

  • источники: Циан, Авито, Дом.Клик, Я.Недвижимость, Н1, Дом РФ
  • Объектив НЕ использовать на вторичке (он про первичку/ДДУ)
  • Росреестр для исторических сделок (квартал глубины)
  • картография ЕКБ для проверки этажности/года/планировок

Phase 2 — то что обсуждали на встрече

Задача Из протокола Птицы
Парольный вход + учёт пользователей + аналитика 0:23:44, 0:25:54
Доступ только Геныч / Загайнов / Паша (НЕ Рожкова) 0:25:50, 0:22:35
PDF-отчёт под паролем 0:08:39
Real-time парсинг ≥1/час чтобы ловить быстрые продажи 0:40:19
MVP к понедельнику 25.05.2026 0:26:12
Демо для девелопера в четверг 28.05.2026 0:18:08

Phase 3 — следующие продукты (упоминалось на встрече)

  1. Птица — анализ участков + расселение домов (≥20% квартир дома в продаже → подсветить можно расселять)
  2. Расселение как сервис — следствие #1 и Птицы

См. полный протокол: docs/PTITSA_MEETING_2026-05-19.pdf.

Как это связано с прод gendesign

Аспект Прод (gendsgn.ru) Этот MVP
URL https://gendsgn.ru/trade-in http://localhost:8080/trade-in
Backend shared FastAPI /api/v1/trade-in/* то же самое, standalone
Frontend Next.js 15 в большом monorepo тот же код, standalone
Postgres 84 таблицы, 6.83M ДДУ partitioned только trade_in_estimates (1 таблица)
Caddy TLS + 5 доменов + reverse-proxy local :8080 без TLS
Что отрезано site-finder, analytics, generative, scraper, OSM, NSPD, sentry, celery, redis, playwright всё это — кроме trade-in

Важно: эти два инстанса полностью изолированы. Локальный backend пишет в свой Postgres контейнер (порт 5433), не трогает прод. Можно сломать локально что угодно — прод не пострадает.

Лицензия

Internal use only. Forked from gendesign monorepo (private).