# Production deployment

Перед выпуском прочитайте `AGENTS.md` и `TECHNICAL_CONTEXT.md`. Команды ниже
предназначены для production-каталога `/var/www/html/kiln`; секреты находятся
в `.env.production`, имеют права `600` и не выводятся в журнал/чат.

## Текущая топология

- Ветка: `feat/menu-postgres-yandex-feed`.
- Compose-файл: `docker-compose.production.yml`, project `kiln`.
- `kiln-app-1`: Next.js, host `127.0.0.1:3100` -> container `3000`.
- `kiln-postgres-1`: PostgreSQL 17 во внутренней сети `kiln_internal`, volume
  `kiln_kiln_postgres`.
- `kiln-bot-bot-1`: бот из `/srv/kiln-bot`, доступный приложению как
  `http://bot:8383` по сети `kiln_bot_api`.
- Apache принимает HTTP/HTTPS на `80/443`, перенаправляет bot-owned маршруты
  на `127.0.0.1:8383`, остальные — на `127.0.0.1:3100`.
- Сертификат `kiln.spb.ru` покрывает `kiln.spb.ru`, `kiln.beer`, `kiln.bar` и
  `xn--h1adei.xn--90a1af.xn--p1ai`.

Не направляйте `BOT_API_BASE_URL` на публичный `https://kiln.spb.ru`: это
создаёт лишний круг через Apache и смешивает приватный API с публичной
маршрутизацией.

## Первичная установка

```bash
git clone https://github.com/qwantru/kiln.git /var/www/html/kiln
cd /var/www/html/kiln
git switch feat/menu-postgres-yandex-feed
cp .env.production.example .env.production
chmod 600 .env.production
```

Заполните уникальные `POSTGRES_PASSWORD`, `DATABASE_URL`, `ADMIN_LOGIN`,
`ADMIN_PASSWORD_HASH`, `AUTH_SECRET` и совпадающий с ботом `BOT_API_KEY`.
Пароль администратора хешируется командой
`npm run admin:hash -- 'длинный-случайный-пароль'`; исходный пароль не хранится
в репозитории. В штатной Docker-схеме задайте `BOT_API_BASE_URL=http://bot:8383`.

Бот должен уже создать внешнюю сеть `kiln_bot_api`. Проверьте её перед
запуском, затем соберите приложение:

```bash
docker network inspect kiln_bot_api >/dev/null
docker compose --env-file .env.production -f docker-compose.production.yml config --quiet
docker compose --env-file .env.production -f docker-compose.production.yml up -d --build
docker compose --env-file .env.production -f docker-compose.production.yml ps
```

Контейнер приложения применяет checked-in миграции перед запуском Next.js.
Первый production-запуск допустим только после резервной копии или на пустой
БД.

## Apache и TLS

Включите необходимые модули и подготовьте webroot Certbot:

```bash
sudo a2enmod proxy proxy_http headers ssl rewrite
sudo mkdir -p /var/www/kiln-acme/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/kiln-acme
sudo cp deploy/apache/kiln.conf /etc/apache2/sites-available/kiln.conf
sudo a2ensite kiln.conf
sudo apachectl configtest
sudo systemctl reload apache2
```

Выпустите сертификат:

```bash
sudo certbot certonly --webroot -w /var/www/kiln-acme \
  --cert-name kiln.spb.ru \
  -d kiln.spb.ru \
  -d kiln.beer \
  -d kiln.bar \
  -d xn--h1adei.xn--90a1af.xn--p1ai
sudo certbot renew --dry-run
```

После выпуска сертификата:

```bash
sudo cp deploy/apache/kiln-ssl.conf /etc/apache2/sites-available/kiln-ssl.conf
sudo a2ensite kiln-ssl.conf
sudo apachectl configtest
sudo systemctl reload apache2
```

Перед копированием всегда сравнивайте tracked vhost с действующим файлом:
маршруты `/api/order` и `/api/booking` должны оставаться на сайте, а только
`/api/ma/`, `/o/`, `/z/`, `/pay/`, `/cal/`, `/dv/`, `/media/` и `/health`
направляются в бот.

## Обновление

При выпуске изменений истории заказов сначала обновите совместимый бот и его
контракт, проверьте `GET /api/history/{phone}` и только затем пересобирайте сайт.
Проверьте, что история содержит `reference`, видимый `order_id`, `items` и
`details_pending`, а операторская карточка принимает `attach_existing_order`.
Для pending-заявки дополнительно проверьте `total_source=submitted_items`,
кнопку изменения состава и замену с прежним `client_ref`; после подтверждения
изменение должно завершаться 409 без второго заказа.
Для клиентских действий дополнительно проверьте флаги `can_cancel`,
`can_change_due`, `can_edit_items`: неоплаченный текущий заказ отменяется с
сохранением записи в истории, оплаченный и завершённый возвращают запрет, а
старая `pay_url` не показывается при `can_pay=false`. Расчёт доставки проверяйте
после обновления бота: production endpoint Достависты и токен должны относиться
к одной среде. Сначала выпустите совместимый бот с `quote_mode=checkout` и
`POST /api/catalog/availability`, затем сайт. Проверьте адреса в пешей и
автомобильной зонах на subtotal ниже/equal/выше minimum и freeFrom: UI не
показывает имя зоны, старый quote исчезает при смене адреса/корзины, а
самовывоз остаётся доступен на любой ненулевой subtotal и показывает адрес.
После post-payment обновления дополнительно проверьте, что свежий ответ бота с
`paid=true` даёт сайту `can_pay=false` и `pay_url=null`; браузер не должен
создавать повторную оплату или самостоятельно запускать внешние side effects.
Проверьте клиентскую ленту recovery-заказа: устаревшее «не оплачен» и ложное
«заказ изменён» не показываются, paid стоит по времени документа СБИС, а все
даты отображаются в `Europe/Moscow` независимо от timezone браузера/контейнера.
После выпуска переключателя Достависты проверьте в admin settings, что текущий
mode остаётся PROD, TEST/PROD показывают только boolean настроенности, а
operator session не имеет доступа к API переключения. Payload браузера не
должен содержать endpoint или credentials. Без боевой доставки переключите TEST,
выполните согласованный smoke в тестовом API, верните PROD и убедитесь, что
TEST-доставка продолжает читаться/отменяться через TEST badge и credentials.
Проверьте также недостаточное количество: бот возвращает `stock_changed` без
создания документа, а checkout сохраняет проблемную строку для исправления.
При выпуске исправления идентичности каталога сначала обновите бот, затем сайт.
После первой синхронизации убедитесь, что активные привязки используют
`sbis_id` номенклатуры, старые привязки по `position_id` закрыты с причиной
`catalog_position_id_migrated`, а `sbis_sync_state.last_error` пуст.
Преобразование collation MariaDB и восстановление осиротевшего заказа выполняются
по `/srv/kiln-bot/OPERATIONS.md`, после отдельного backup и проверки на копии.

1. Проверьте чистоту рабочей копии и remote, не скрывая локальные изменения:

```bash
git status --short
git branch --show-current
git fetch origin feat/menu-postgres-yandex-feed
git log --oneline --decorate -5
git diff --stat HEAD..origin/feat/menu-postgres-yandex-feed
```

2. До миграций создайте дамп в защищённый каталог вне репозитория. Имя файла
   должно содержать дату и текущий commit. Пример запуска `pg_dump` без вывода
   пароля:

```bash
install -d -m 700 /var/backups/kiln
set -o noclobber
docker compose --env-file .env.production -f docker-compose.production.yml \
  exec -T postgres sh -lc 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  > /var/backups/kiln/site-before-update.dump
test -s /var/backups/kiln/site-before-update.dump
```

Для выпуска адресной книги до запуска приложения проверьте миграцию
`0014_customer_addresses.sql` на копии БД: повторный запуск должен быть
идемпотентным, существующие пользователи и заказы не изменяются. Rollback до
начала пользовательской записи — удаление только новой таблицы и её индексов;
после появления адресов rollback выполняется восстановлением проверенного дампа
или совместимым откатом приложения без удаления таблицы.

3. Выполняйте только fast-forward и пересборку:

```bash
git merge --ff-only origin/feat/menu-postgres-yandex-feed
docker compose --env-file .env.production -f docker-compose.production.yml \
  up -d --build
```

4. Проверьте контейнеры, health и маршрутизацию, явно обходя системный proxy
   для localhost:

```bash
docker compose --env-file .env.production -f docker-compose.production.yml ps
docker compose --env-file .env.production -f docker-compose.production.yml logs --tail 200 app
curl --noproxy '*' -fsS http://127.0.0.1:3100/api/info >/dev/null
curl --noproxy '*' -fsS http://127.0.0.1:8383/health >/dev/null
curl -fsS https://kiln.spb.ru/ >/dev/null
curl -fsS https://kiln.spb.ru/favicon.svg?v=2 | grep -q '<svg'
curl -fsS https://kiln.spb.ru/favicon-v2.png >/dev/null
```

После выпуска графики дополнительно проверьте шапку на ширине 320 px и favicon
в новой вкладке: SVG должен быть первым icon в HTML, а Apple/PWA-иконки должны
показывать тот же знак с вилкой и ножом.

При ошибке не правьте production-БД вручную. Остановите выпуск, сохраните логи
и верните предыдущий commit/image по заранее проверенному rollback-плану. Если
миграция изменила данные, восстановление выполняется из проверенного дампа с
учётом возникших после него пользовательских операций.

## Retention ПДн

```bash
sudo cp deploy/systemd/kiln-pdn-maintenance.* /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now kiln-pdn-maintenance.timer
sudo systemctl start kiln-pdn-maintenance.service
systemctl status kiln-pdn-maintenance.timer kiln-pdn-maintenance.service
```

Worker идемпотентен, использует PostgreSQL advisory lock и пропускает аккаунты
под legal hold. Предупреждения о lock проверяйте по логам и соединениям; не
подавляйте их без установления причины.
