All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
Клиент пишет боту в личку → воркер зеркалит сообщение через copyMessage в топик супергруппы-форума → оператор отвечает реплаем на зеркало → бот доставляет ответ клиенту. Полный лог переписки в Postgres. Отдельный контейнер на long-polling, а не webhook в tradein-backend: не нужно пробивать дырку в auth-middleware (_PUBLIC_PATHS, #2213) и маршрут в Caddy, нулевая внешняя поверхность, падение бота не задевает API. Без aiogram — httpx уже в зависимостях, нужны только getUpdates/copyMessage. Маршрутизация ответа — по topic_message_id: message_id в Telegram уникален в пределах чата сквозь все топики, а все зеркала лежат в одном support-чате, поэтому спутать адресата нельзя. Реплай на шапку/на ответ другого оператора не резолвится (у direction='out' topic_message_id IS NULL) → тихий игнор. Безопасность (найдено ревью, воспроизведено эмпирически): - токен Telegram живёт в PATH URL, поэтому sanitize_url его не режет; утекал в GlitchTip через locals стек-фреймов (include_local_variables по умолчанию True) и через span data HttpxIntegration. Закрыто include_local_variables=False + regex-редактор в before_send (обе формы: /bot<id>:<secret> и голая <id>:<secret>), поверх существующего PII-scrub. - httpx-логгер печатает полный URL на INFO → боевой токен уходил бы в docker logs каждые 30с. Приглушён до WARNING. Надёжность: - kill-switch при пустом токене — idle-блокировка, не exit(0): при restart: unless-stopped выход с любым кодом даёт рестарт-луп. unless-stopped выбран сознательно — только он гарантирует автозапуск после ребута VPS. - stop_grace_period: 120s — дефолтные 10с убивали бы контейнер раньше, чем докрутится long-poll (30с) и отработает drain (100с). - сбой SQL теперь ловится отдельно и делает rollback перед сдвигом offset: иначе сессия в failed-transaction не давала сохранить offset, апдейт переигрывался и зеркалился в топик по кругу. 152-ФЗ: переписка — ПДн, ON DELETE CASCADE по chat_id, удаление клиента одним DELETE. Ретенция — follow-up. Бот не включается автоматически: TELEGRAM_* задаются в runtime-env на VPS, без них воркер штатно висит в idle. Порядок — в DEPLOY.md. Тесты: 51 passed (маршрутизация обоих направлений, дедуп, 403→is_blocked, throttle-окно шапки, redaction токена во всех формах event).
243 lines
12 KiB
Markdown
243 lines
12 KiB
Markdown
# 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
|
||
```
|