> ## Documentation Index
> Fetch the complete documentation index at: https://omniflow-vpn.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Диагностика неисправностей

> Реальные симптомы, их настоящие причины и места, куда смотреть в этой установке

Начните с `/readyz` на тех процессах, которые его отдают, и с датчика
`omniflow_dependency_up`. Большинство инцидентов — это одна зависимость, и остальная часть
страницы предполагает, что вы это уже исключили.

Про готовность стоит знать две вещи, прежде чем на неё полагаться:

* **Бот** вообще не обслуживает HTTP, пока не задан `APP_BOT_METRICS_ADDR`, и этот
  адрес игнорируется, когда он совпадает с `APP_BOT_HTTP_ADDR`, — публиковать пробы и метрики
  на смотрящем в интернет хосте вебхука запрещено. Из коробки у бота нет эндпоинта проб,
  поэтому «бот не отвечает на `/readyz`» обычно означает, что он никогда и не слушал.
* **API**, запущенный без `APP_DATABASE_URL`, не регистрирует ни одной пробы зависимостей и
  отвечает `200 {"status":"ready","checks":[]}`. Это зелёный свет для процесса, который умеет
  обслуживать только пробы и `/metrics`.

Воркер регистрирует только `postgres`. API регистрирует `postgres`, `valkey` и
`remnawave`, когда настроена коммерция. См.
[Наблюдаемость](https://omniflow-vpn.mintlify.site/ru/operations/observability.md).

## Клиент заплатил и не получил доступ

Это тот сбой, против которого Omniflow защищается упорнее всего, и почти всегда дело в выдаче
доступа, а не в оплате.

1. Найдите заказ. `GET /v1/admin/orders/{orderID}` показывает его состояние и оплаченную сумму.
2. `paid` означает, что деньги записаны и право на услугу существует. `fulfilled` означает,
   что Remnawave это подтвердил.
3. `GET /v1/admin/jobs?state=dead_letter` показывает запуски выдачи доступа, которые сами по
   себе повторяться не будут.
4. `GET /v1/admin/fulfillment/drifts` показывает права на услугу, чьё удалённое состояние не
   совпадает.

Чтобы перезапустить застрявший прогон, вызовите `POST /v1/admin/jobs/{jobID}/retry` или
воспользуйтесь панелью: `/admin/system` → Jobs. Лежащая под этим операция исполнения несёт
собственный ключ идемпотентности, поэтому повтор не может применить дважды то, что уже
удалось.

Платёж здесь ничем не рискует. Расчёт и запись долговременной задачи фиксируются в одной
транзакции, поэтому у оплаченного заказа всегда есть ожидающая его задача.

## Воркер падает при старте, жалуясь на отсутствующую таблицу

Сервис `migrate` применяет зафиксированные миграции Atlas из `database/migrations`, и
среди них есть внутренние таблицы очереди (`river_job`, `river_leader`, `river_queue`,
`river_notification`, `river_migration`). Если воркер падает на `client.Start`, сообщая
об отсутствии одного из этих отношений, значит миграции не доехали до конца на той базе,
на которую смотрит воркер: проверьте лог сервиса `migrate` и сравните `APP_DATABASE_URL`
у сервисов, прежде чем предполагать проблему со связью.

Пока этих таблиц нет, воркер не может запуститься, а оформление заказа, которое вставляет
задачу исполнения внутри своей расчётной транзакции, падает вместе с ним. См.
[Задачи и очереди](https://omniflow-vpn.mintlify.site/ru/operations/jobs-and-queues.md#схема-очереди-поставляется-вместе-с-миграциями).

## Платёж прошёл у провайдера, но заказ в ожидании

Вебхук не пришёл либо был отклонён.

1. `GET /v1/admin/webhooks` перечисляет недавние поступления с `signatureValid` и `status`.
2. `signatureValid: false` означает, что подпись не сошлась. Проверьте учётные данные
   провайдера и убедитесь, что обратный прокси не изменяет тело запроса — перекодированное
   тело делает недействительной любую подпись, потому что подпись считается по точным байтам,
   которые прислал провайдер.
3. `status: failed` с `payment_not_found` означает, что вебхук ссылался на платёжное
   намерение, которого в этой установке нет. Обычно это вебхук из другого окружения,
   направленный на эту.
4. Если вебхук не пришёл вовсе, `POST /v1/admin/payments/{paymentID}/reconcile` опрашивает
   провайдера напрямую. Проход `payment_reconcile` на воркере также сам переопрашивает до
   100 незавершённых намерений каждую минуту.

Оба пути идемпотентны. Вебхук, пришедший после ручной сверки, ничего не делает.

> [!NOTE]
>
> Собственные кнопки **Reconcile** и **Refund** на `/admin/finance` записывают событие аудита
> о том, кто попросил и почему; сами по себе они не переопрашивают провайдера и не двигают
> деньги. Когда нужно, чтобы к провайдеру обратились, используйте маршруты `/v1/admin` выше.

## Учётные данные провайдера в панели выглядят правильными, а платежи всё равно не проходят

Платёжные адаптеры, которые вызывают API, бот и воркер, строятся из переменных окружения при
старте — `APP_TELEGRAM_TOKEN`, `APP_CRYPTOBOT_TOKEN`, `APP_YOOKASSA_SHOP_ID`,
`APP_YOOKASSA_SECRET`. Учётные данные, сохранённые на карточках провайдеров в панели, — не то,
чем пользуется работающий процесс. Измените переменную и перезапустите. См.
[Платежи](https://omniflow-vpn.mintlify.site/ru/integrations/payments.md) и
[Настройки установки](https://omniflow-vpn.mintlify.site/ru/operations/installation-settings.md).

## Пополнение зачислило неверную сумму

Смотрите топик `topup` или события платежа по заказу. Пополнение зачисляет ровно то, что
провёл провайдер:

* `overpayment` — пришло больше, чем запрашивалось, и всё это зачислено.
* `underpayment` — пришло меньше, и зачислено именно это.

В обоих случаях заказ остаётся `paid`. Так и задумано: кошелёк не должен ничего сверх самих
денег. Если расхождение нужно скорректировать, используйте корректировку кошелька с причиной,
которая записывается компенсирующей проводкой, а не переписыванием.

## Клиенту отказано в пополнении, которое он считает укладывающимся в лимиты

Лимиты пополнения берутся из `APP_WALLET_TOPUP_*` на API, боте и воркере, а не из карточки пополнения
в панели. Сохранение новых цифр на этой карточке меняет то, что показывает карточка, а не то,
что применяет магазин. Проверьте значения окружения и сумму в скользящем окне: отказ обычно
даёт `APP_WALLET_TOPUP_WINDOW_LIMIT_MINOR` за `APP_WALLET_TOPUP_WINDOW_HOURS`, а не потолок
одной операции.

## Сохранённая корзина не выкупает себя

Рассчитанное пополнение пробует купить корзину сразу; любое другое начисление —
реферальное вознаграждение, корректировка оператора — освобождает её через проход
`commerce_maintenance` на воркере раз в минуту. Без работающего воркера такие корзины ждут,
а неудачный проход логируется как `periodic task failed` с именем задачи. В остальных
случаях проверьте `carts.last_failure`:

| Значение | Смысл |
| - | - |
| `cart_insufficient_balance` | Работает как задумано; баланса не хватает на пересчитанную сумму |
| `cart_plan_unavailable` | Тариф или дополнение архивированы, сняты с продажи или лишились цены |
| `cart_price_changed` | Заказ был отклонён по текущей цене |
| `cart_auto_purchase_off` | Клиент выключил автоматическую покупку |
| `cart_maintenance` | Установка находится в режиме обслуживания |

Помните, что корзина переоценивается при каждой попытке. Корзина, сохранённая до повышения
цены, по старой цене не купится — так и задумано.

## Двойные списания

Они должны быть невозможны, и инварианты объясняют почему:

* `orders (user_id, idempotency_key)` уникален, поэтому одно оформление заказа даёт один заказ.
* `ledger_transactions.idempotency_key` уникален, и `(reference_type, reference_id, type)`
  уникален, поэтому один расчёт зачисляется один раз.
* `entitlement_addons (order_id, addon_version_id)` уникален, поэтому одно дополнение
  применяется один раз.
* `fulfillment_operations.idempotency_key` уникален, поэтому одна операция выполняется один раз.

Если вам кажется, что вы видите дубль, сравните `ledger_entries` по клиенту. Две проводки в
рамках одной транзакции — это две стороны одного движения, а не два списания.

## Операторская группа молчит

1. Убедитесь, что `APP_OPERATOR_CHAT_ID` задан и бот был перезапущен. Если он не задан, бот
   пишет в лог `operator notifications are disabled because no group is configured` и никогда
   не разбирает очередь — уведомления при этом продолжают накапливаться строками `pending` и
   доставляются, как только группа будет настроена.
2. Проверьте `operator_topics` на строку со `status = 'failed'` и прочитайте `last_error_code`.
   См. [Операторские уведомления](https://omniflow-vpn.mintlify.site/ru/operations/operator-notifications.md).
3. Проверьте `operator_notifications` на строки со `status = 'suppressed'` и
   `error_code = 'volume_cap'` — это означает, что ограничитель делает свою работу.
4. Если молчат только *некоторые* топики, это может быть правильно. У покупок, продлений,
   пополнений, возвратов, сбоев исполнения и админской безопасности есть источники событий.
   Топики 🚨 Инциденты и 💾 Резервные копии создаются, но сейчас в них ничто не ставится
   в очередь.

## При входе появляется «Не удалось войти. Попробуйте ещё раз.» с верными данными

Это сообщение панель показывает, когда запрос вернулся без сессии, и есть две
причины, не имеющие отношения к паролю. На экране они выглядят одинаково, и обе
относятся к конфигурации, а не к учётным данным. Откройте вкладку сети в
браузере: она покажет, какая из них у вас.

**Запрос вернул 404.** Он дошёл до веб-приложения, а не до API. Обе панели
обращаются к API на своём собственном origin — у браузерного клиента нет
базового URL, и CORS нигде нет, — поэтому кто-то должен обслуживать `/v1` на
веб-хосте. За обратным прокси это его работа, и примеры в `deploy/proxies` её
выполняют; конфигурация, написанная вручную и направляющая только приложение,
даст ровно такую картину. Без прокси веб-сервису нужна переменная
`API_INTERNAL_URL`; если она не задана, он отвечает `502` с проблемным
документом, который её называет. См. [Обратный прокси](https://omniflow-vpn.mintlify.site/ru/operations/reverse-proxy.md).

**Запрос вернул 200, а следующий уже анонимный.** Браузер отбросил cookie. По
обычному HTTP это значит, что `Secure` всё ещё включён: браузер не возвращает
cookie с флагом `Secure` по незашифрованному соединению. Поставьте
`APP_ADMIN_COOKIE_SECURE=false` и `APP_CUSTOMER_COOKIE_SECURE=false` для
локального стека и оставьте их включёнными везде, где есть TLS. Проверьте заодно
имя cookie в ответе: с `Secure` это `__Host-omniflow_admin`, без него —
`omniflow_admin`, а имя с префиксом `__Host-`, пришедшее без `Secure`,
отвергается целиком, а не просто считается более слабым.

## Оператор не может войти, и страница пишет «rate limited»

Ограничение частоты запросов при входе опирается на Valkey и **отказывает по умолчанию**: когда
Valkey недоступен, ограничитель отвечает «не разрешено», а не пропускает запросы, поэтому сбой
Valkey полностью блокирует вход операторов. Сначала проверьте запись `valkey` в выводе
`/readyz` у API.

Настоящее ограничение отвечает `429 rate_limited`; заблокированная учётная запись отвечает
`429 account_locked` после пяти неудач подряд, причём ожидание удваивается вплоть до часа.

Учтите, что сбой Valkey **не** открывает режим обслуживания. Автоматическое обнаружение следит
только за `postgres` и `remnawave`, поэтому `valkey` появляется в `/readyz` и сам по себе
никогда не запускает окно.

## Бот не получает обновлений

В режиме длинных опросов проверьте логи бота на ошибки Telegram API и убедитесь, что исходящий
HTTPS работает.

В режиме вебхука:

1. Убедитесь, что прокси направляет `POST /telegram/webhook` на слушатель бота
   (`APP_BOT_HTTP_ADDR`, по умолчанию `:8082`) и не переписывает тело. Эндпоинт ограничивает
   тело 1 MiB.
2. Убедитесь, что `APP_TELEGRAM_WEBHOOK_SECRET` совпадает с тем, что было зарегистрировано.
   Обновление без заголовка с секретным токеном отбрасывается до разбора, и выглядит это
   ровно как отсутствие трафика. Секрет должен соответствовать `^[A-Za-z0-9_-]{32,256}$`, иначе
   бот откажется стартовать.
3. `APP_TELEGRAM_WEBHOOK_URL` должен быть HTTPS, иначе бот откажется стартовать.
4. Спросите у Telegram, что думает он: `getWebhookInfo` сообщает URL, на который он доставляет,
   и последнюю увиденную ошибку.

Возврат к длинным опросам — поддерживаемый запасной вариант: очистите
`APP_TELEGRAM_WEBHOOK_URL` и перезапустите.

## `/ops` ничего не делает в Telegram

Команда зарегистрирована всегда и отвечает **вообще ничем** аккаунту, которому ею пользоваться
нельзя, поэтому факт существования команды клиенту не подтверждается. Прежде чем вы получите
хоть какой-то ответ, должны выполняться оба условия:

1. `APP_OPERATOR_TELEGRAM_IDS` содержит числовой ID аккаунта Telegram, и бот с тех пор был
   перезапущен. Пустой список полностью отключает эту поверхность, и это значение по
   умолчанию.
2. Сообщение отправлено в **приватном** чате с ботом. В группе оно игнорируется.

Если ответ всё же приходит и в нём написано *«Backups are disabled»*, поверхность работает, а
отсутствует `APP_BACKUP_ENABLED` или `APP_BACKUP_ENCRYPTION_KEY`. Некорректная запись в
`APP_OPERATOR_TELEGRAM_IDS` — фатальная ошибка конфигурации, поэтому, если процесс вместо
этого сразу завершился, проверьте стартовый лог бота.

## Покупки отклоняются с уведомлением об обслуживании

Проверьте `GET /v1/admin/maintenance` или вкладку Health на `/admin/system` в панели. `source`
говорит, было ли окно ручным (`manual`) или автоматическим (`database`, `remnawave`).
Автоматическое окно закрывается само, как только зависимость была здорова
`APP_MAINTENANCE_RECOVERY_STREAK` замеров подряд; ручное само не закрывается никогда.

Задача исполнения, попавшая в активное окно, откладывается на минуту, а не проваливается, и её
счётчик попыток возвращается, поэтому пауза на обслуживание не тратит бюджет повторов. См.
[Режим обслуживания](https://omniflow-vpn.mintlify.site/ru/operations/maintenance-mode.md).

## Число ожидающих событий в outbox только растёт

Так будет всегда. Строки `outbox_events` пишутся на `order.paid`, `wallet.credited` и
`addon.purchased`, и ничто в продукте не отмечает их опубликованными. Поэтому
`GET /v1/admin/outbox` и вкладка outbox в панели показывают число, которое растёт всю жизнь
установки, а `APP_RETENTION_OUTBOX_DAYS` — который удаляет только опубликованные строки —
не удаляет ничего.

Это не отставание, это не застрявший диспетчер, и никакое действие это не очищает. Не
настраивайте на это алерты. Объявленный датчик `omniflow_outbox_pending_events` тоже никогда
не пишется, поэтому собирать нечего.

## Резервные копии не создаются

Причина сбоя записана в строке резервной копии и в логе процесса, который её делал, в виде
классифицированного кода:

| Код | Причина |
| - | - |
| `command_exit_N` | `pg_dump` или `pg_restore` завершился с ненулевым кодом, обычно из-за учётных данных или несовпадения версий |
| `verification_failed` | Записанный файл не удалось прочитать обратно, либо его дайджест не совпал; проверьте здоровье диска и свободное место |
| `file_missing` | Каталог резервных копий недоступен для записи или не смонтирован |
| `cancelled` | Процесс завершился посреди копирования |
| `backup_failed` | Всё остальное, что сервис не смог классифицировать |

Плановые резервные копии выполняются в **воркере**; копии и восстановления из `/ops` — в
**боте**, поэтому смотрите лог именно этого процесса, даже если сообщение на экране упоминает
воркер.

`pg_dump` должен соответствовать мажорной версии сервера. Образы бота и воркера поставляются с
клиентскими утилитами PostgreSQL 18 именно по этой причине; если вы запускаете их где-то ещё,
направьте `APP_PG_DUMP_PATH` и `APP_PG_RESTORE_PATH` на подходящие бинарники. См.
[Резервные копии и восстановление](https://omniflow-vpn.mintlify.site/ru/operations/backup-restore.md).

## Вложение к обращению, загруженное из веба, пропало

`APP_SUPPORT_ATTACHMENT_DIR` (по умолчанию `/var/lib/omniflow/support-attachments`) читают и
API, который пишет вложения, и воркер, который освобождает их файлы при истечении срока
хранения. Отсюда два вида сбоев:

* Compose-стек не монтирует том по этому пути ни на одном из сервисов, поэтому вложение,
  записанное API, живёт в записываемом слое контейнера и теряется при его пересборке.
  Смонтируйте том на API, если вложения, загруженные из браузера, должны переживать
  повторное развёртывание.
* Если значение у воркера отличается от значения у API, срок хранения удаляет строки и
  оставляет байты на диске навсегда. Задайте одинаковый путь в обоих местах.

Вложения в установках, работающих только через Telegram, — это ссылки на файлы, которые хранит
Telegram, и их это не затрагивает.

## Запущено больше одной реплики

Поддерживается сколько угодно реплик `api` и `worker`: периодические проходы — обслуживание
заказов и корзин, сверку платежей, продления, обходы, рассылки, очистку по срокам хранения,
резервные копии — ставит в очередь выбранный River лидер, и они выполняются один раз за
интервал, сколько бы воркеров ни работало. Исключение — `bot`: Telegram доставляет
обновления одному потребителю, и long polling отказывает второму `getUpdates`. Запускайте
ровно один бот.

Больше реплик — это и больше подключений к базе. Каждая открывает пул размером
`APP_DATABASE_MAX_CONNS` (по умолчанию api 20, bot 10, worker 40), и сумма должна оставаться
ниже `max_connections` PostgreSQL. Воркер, которому дали меньше 32, отказывается
запускаться и сообщает, сколько ему нужно. См.
[Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md#подключения-к-базе-данных).

## Процесс упал, и его никто не поднял

Поставляемый `compose.yaml` не объявляет политику `restart:` ни для `postgres`, `valkey`,
`api`, `bot`, `worker`, ни для `web` — её задаёт только оверлей Traefik. Упавший процесс
остаётся лежать, пока кто-нибудь не заметит. Для развёртывания без присмотра добавьте политику
перезапуска. См. [Развёртывание](https://omniflow-vpn.mintlify.site/ru/operations/deployment.md).

## У клиента две подписки, а ожидается одна

Одновременные подписки по умолчанию отключены. Если у клиента их больше одной, значит либо
переключатель включён, либо они были приняты от разных пользователей Remnawave при импорте.
См. [Подписки](https://omniflow-vpn.mintlify.site/ru/commerce/subscriptions.md) — там описано, как свести клиента к одной, не
осиротив доступ.

## Трассировки пустые

Omniflow открывает спаны на входящем HTTP, обновлениях Telegram, задачах исполнения, запросах
к PostgreSQL и исходящем HTTP, но никогда не устанавливает провайдер трассировки и не читает
ни одной переменной `OTEL_*`. Без установленного провайдера глобальный является пустышкой, и
ни один спан не экспортируется, поэтому одна лишь настройка переменных коллектора ничего не
меняет. Метрики и логи — та инструментальная база, на которую сегодня можно полагаться.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.