# Trade-In MVP — деплой в gendesign репо

## Архитектура

```
lekss361/-gendesign/
├── backend/           ← gendesign Python backend (НЕ ТРОГАЕМ)
├── frontend/          ← gendesign Next.js (НЕ ТРОГАЕМ)
├── docker-compose.prod.yml  ← gendesign-стек
├── Caddyfile          ← основной Caddy → ВКЛЮЧАЕТ tradein-mvp/deploy/Caddyfile.tradein-fragment
├── .github/workflows/
│   ├── deploy.yml             ← существующий gendesign deploy
│   └── deploy-tradein.yml     ← НАШ новый, триггерится только на tradein-mvp/**
└── tradein-mvp/       ← НАША подпапка (изолированный subproject)
    ├── backend/
    ├── frontend/
    ├── deploy/
    │   ├── Caddyfile.tradein-fragment  ← импортится в основной Caddyfile
    │   └── cron-scrape.sh
    ├── docker-compose.yml         ← локальный dev
    ├── docker-compose.prod.yml    ← production overlay (использует GHCR images)
    └── .github/workflows/deploy-tradein.yml
```

## Прод URL routes

| URL                              | Сервис                  |
|----------------------------------|-------------------------|
| `gendsgn.ru/trade-in/`           | tradein-frontend:3000   |
| `gendsgn.ru/trade-in/api/v1/...` | tradein-backend:8000    |

Caddy в gendesign-стеке проксирует через `handle_path /trade-in/api/*` (вырезает префикс) и `handle /trade-in/*` (Next.js basePath).

## Прод docker stacks

- `docker compose -p gendesign` — основной gendesign (postgres + backend + frontend + caddy + couchdb)
- `docker compose -p gendesign-tradein` — наш (tradein-postgres + tradein-backend + tradein-frontend)

Оба подключены к `gendesign_shared` external network — туда смотрит Caddy.

## Что нужно положить в основной Caddyfile gendesign

В блок `gendsgn.ru { ... }` **ПЕРЕД** универсальным `handle { reverse_proxy frontend:3000 }`:

```caddyfile
import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment
```

Или вставить содержимое фрагмента inline.

## Что нужно положить в `.env.runtime` (на сервере, НЕ в git)

Два файла:

1. `/opt/gendesign/tradein-mvp/.env.runtime` — переменные для docker compose
   substitution (`${TRADEIN_POSTGRES_PASSWORD}` и т.п. в compose-файле). Читается
   shell-скриптом deploy через `source .env.runtime` перед `compose up`.
2. `/opt/gendesign/tradein-mvp/backend/.env.runtime` — переменные внутри
   контейнера `tradein-backend` (читаются через `env_file:` в compose). Сюда
   попадают `YANDEX_GEOCODER_API_KEY`, `COOKIE_ENCRYPTION_KEY` —
   всё, что нужно scripts/backfill_house_coords.py и application code внутри
   контейнера.

```bash
# /opt/gendesign/tradein-mvp/.env.runtime
TRADEIN_POSTGRES_USER=tradein
TRADEIN_POSTGRES_PASSWORD=<сгенерировать openssl rand -hex 32>
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
YANDEX_GEOCODER_API_KEY=  # пусто пока, Nominatim fallback работает

# Encryption key for Cian session cookies (pgp_sym_encrypt / Stage 9 Calculator).
# Empty = Valuation Calculator scraper disabled + /api/v1/cookies/upload returns 503.
# Generate once on VPS:
#   openssl rand -hex 32
COOKIE_ENCRYPTION_KEY=<64-char hex>
```

```bash
# /opt/gendesign/tradein-mvp/backend/.env.runtime — те же ключи которые
# читаются ВНУТРИ container'а (scripts/backfill_house_coords.py, app/*).
# Может быть симлинком на ../.env.runtime если переменные совпадают:
#   ln -s ../.env.runtime /opt/gendesign/tradein-mvp/backend/.env.runtime
YANDEX_GEOCODER_API_KEY=<key или пусто>
COOKIE_ENCRYPTION_KEY=<64-char hex>
GENDESIGN_FDW_PASSWORD=<password или пусто>
GLITCHTIP_DSN=<dsn или пусто>
# DaData on-demand enrichment в /estimate flow (PR Q1).
# Demo tier: 100 req/день. Пусто = enrichment disabled (graceful).
# Регистрация ключей: https://dadata.ru/api/clean/
DADATA_API_TOKEN=<token или пусто>
DADATA_API_SECRET=<secret или пусто>

# Telegram support-bot bridge (сервис tgbot, docker-compose.prod.yml).
# Long-polling воркер (app/tgbot_main.py), тот же образ что backend/scraper,
# отдельный контейнер tradein-tgbot. Пусто TELEGRAM_BOT_TOKEN = бот НЕ падает
# и НЕ рестарт-лупится — процесс стартует, уходит в idle-блокировку и просто
# висит (это норма для окружений без токена, не сбой; см. tgbot_main.py).
#
# 1. TELEGRAM_BOT_TOKEN — токен от @BotFather (/newbot). Пусто = бот выключен
#    (idle, не polling).
TELEGRAM_BOT_TOKEN=<token или пусто>
# 2. TELEGRAM_SUPPORT_CHAT_ID — id супергруппы-форума (Topics включены в
#    настройках группы), вида -100XXXXXXXXXX. Получить: добавить бота в группу,
#    отправить любое сообщение в любой топик, дернуть
#    https://api.telegram.org/bot<token>/getUpdates — в ответе
#    message.chat.id (для супергруппы всегда отрицательный, начинается с -100).
TELEGRAM_SUPPORT_CHAT_ID=<-100... или пусто>
# 3. TELEGRAM_SUPPORT_TOPIC_ID — id конкретного топика (thread) внутри группы,
#    куда падают support-обращения. Открыть нужный топик в Telegram Desktop/Web →
#    в URL топика (t.me/c/<chat>/<topic_id>) последнее число — это topic_id.
#    Либо взять message_thread_id из того же getUpdates-ответа (п.2), отправив
#    тестовое сообщение именно в целевой топик.
TELEGRAM_SUPPORT_TOPIC_ID=<topic_id или пусто>
```

Оба файла создаются вручную при первом деплое.

### Деплой / рестарт `tgbot`

```bash
# .env.runtime читается на старте container — `compose restart` НЕ перечитывает.
docker compose -p gendesign-tradein -f docker-compose.prod.yml \
  up -d --force-recreate --no-deps tgbot
```

`restart: unless-stopped` + `stop_grace_period: 120s` в compose (см. `docker-compose.prod.yml`)
— автозапуск после ребута VPS гарантирован (`on-failure` сюда не годится: код выхода
контейнера при ребуте — гонка с long-poll таймаутом 30с, `unless-stopped`/`always`
не зависят от exit-кода). 120s grace даёт time докрутить long-poll + отработать
кооперативный drain (`_DRAIN_TIMEOUT_S=100s` в `tgbot_main.py`) до docker SIGKILL —
паттерн скопирован с `scraper` (см. комментарий там же).

### После изменения `backend/.env.runtime`

```bash
# .env.runtime читается на старте container — `compose restart` НЕ перечитывает.
docker compose -p gendesign-tradein -f docker-compose.prod.yml \
  up -d --force-recreate --no-deps backend
```

См. `.claude/rules/deploy.md` — main backend pattern идентичный.

### Обновление `COOKIE_ENCRYPTION_KEY` на существующем VPS

```bash
# На VPS, если COOKIE_ENCRYPTION_KEY ещё не задан:
echo "COOKIE_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> /opt/gendesign/tradein-mvp/.env.runtime

# Применить без полного рестарта стека:
cd /opt/gendesign/tradein-mvp
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --force-recreate --no-deps backend
```

## GitHub Secrets (уже должны быть от gendesign deploy)

- `DEPLOY_HOST` — IP сервера (94.228.121.73)
- `DEPLOY_USER` — root
- `DEPLOY_SSH_KEY` — приватный SSH ключ
- `DEPLOY_PORT` — 22 (или другой если меняли)

## Первый деплой — пошагово

### 1. На анто́новой машине: подготовить tradein-mvp/ к слиянию

```bash
# clone gendesign репо (с PAT)
git clone https://<PAT>@github.com/lekss361/-gendesign.git
cd -gendesign

# скопировать tradein-mvp/ как подпапку
cp -r /Users/anton/Птица/tradein-mvp ./tradein-mvp
rm -rf ./tradein-mvp/postgres-data  # на всякий
rm -rf ./tradein-mvp/.git           # не нужен внутри monorepo

# скопировать workflow в правильное место
mkdir -p .github/workflows
cp ./tradein-mvp/.github/workflows/deploy-tradein.yml .github/workflows/

git add tradein-mvp/ .github/workflows/deploy-tradein.yml
git commit -m "feat: add tradein-mvp subproject + deploy workflow"
git push origin main
```

### 2. На сервере: первый раз вручную добавить Caddy include

```bash
ssh -i ~/.ssh/id_bot_server root@94.228.121.73

cd /opt/gendesign
# git reset --hard origin/main подтянет tradein-mvp/ автоматически

# Добавить в Caddyfile (ровно одну строку в блок gendsgn.ru { ... })
nano Caddyfile
# вставить ПЕРЕД последним handle { ... }:
#   import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment

# Создать .env.runtime — два файла:
# 1. tradein-mvp/.env.runtime — для compose substitution на host'е
cat > tradein-mvp/.env.runtime <<EOF
TRADEIN_POSTGRES_USER=tradein
TRADEIN_POSTGRES_PASSWORD=$(openssl rand -hex 32)
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
YANDEX_GEOCODER_API_KEY=
EOF
chmod 600 tradein-mvp/.env.runtime

# 2. tradein-mvp/backend/.env.runtime — для env_file внутри container'а.
# Симлинк работает если переменные одни и те же; иначе создать отдельный файл.
ln -sf ../.env.runtime tradein-mvp/backend/.env.runtime

# Запустить stack первый раз вручную (после этого GitHub Actions сам)
docker network inspect gendesign_shared >/dev/null 2>&1 || docker network create gendesign_shared
cd tradein-mvp
set -a; source .env.runtime; set +a
docker compose -p gendesign-tradein -f docker-compose.prod.yml pull
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d

# Reload Caddy
cd /opt/gendesign
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy caddy reload --config /etc/caddy/Caddyfile

# Проверить
curl -sS https://gendsgn.ru/trade-in/api/health
curl -sI https://gendsgn.ru/trade-in/
```

### 3. Дальше — автодеплой

После первого ручного запуска все следующие `git push origin main` с изменениями в `tradein-mvp/**` будут:
1. Триггерить `.github/workflows/deploy-tradein.yml`
2. Билдить и пушить новые образы в `ghcr.io/lekss361/gendesign-tradein-*`
3. SSH в сервер → `docker compose pull` → `up -d` → Caddy reload
4. Health check

## Откат

```bash
ssh root@94.228.121.73
cd /opt/gendesign/tradein-mvp
# Откатиться на предыдущий SHA-тег
docker compose -p gendesign-tradein -f docker-compose.prod.yml down
IMAGE_TAG=<previous-sha> docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d
```
