/v1/panel. Добавляют они возможность вести бизнес без
доступа к SQL и к оболочке.
Каждый маршрут закрыт дважды. Серверная граница Next.js отказывается
отрисовывать страницу, которой оператор не может пользоваться, поэтому
запрещённая поверхность вообще не порождает разметку. Go API проверяет то же
право доступа на каждом запросе, и именно эта проверка — а не скрытый маршрут —
на самом деле защищает данные.
Права доступа, добавленные в v0.7
Риски отделены от администрирования клиентов, потому что эти две вещи отвечают
на разные вопросы: чтение записи о клиенте — рутинная работа поддержки, а
решение о том, что мнение внешнего списка о нём верно, — суждение с
последствиями. Цифровые товары отделены от
catalog, потому что провайдер
товаров держит собственные деньги оператора: тот, кому позволено назначать цену
VPN-тарифа, не должен автоматически получать возможность ротировать учётные
данные, которые тратят средства с пополненного баланса.
Поддержка и финансы получают доступ на чтение ко всем трём. Финансы вдобавок
получают gifts.write, потому что отзыв непогашенного подарка — это решение о
возврате.
Заголовок с причиной
Большинство изменений требуют объяснения, передаваемого в заголовке запросаX-Operator-Reason. API отклоняет изменение, которому нужна причина и которое
её не получило, поэтому панель блокирует элемент управления, пока причина не
введена, вместо того чтобы дать оператору узнать о требовании из ошибки.
Причина обязательна при блокировке или разблокировке клиента, при решении по
совпадению со списком блокировок или его обжаловании, при добавлении клиента в
список разрешённых, при отзыве подарка, при отмене доставки из магазина, при
оформлении возврата и при предпросмотре массового действия.
Дашборд
Каждая плитка публикует определение того, что именно она посчитала. Это не украшение: типичный сбой операционного дашборда — не неверное число, а верное число, которое кто-то прочитал как означающее нечто иное. «Активные клиенты» — это записи о клиентах, которые не заблокированы и не удалены; это не количество людей с действующей подпиской, и плитка так и говорит. Стоит знать о трёх свойствах. Время в UTC. Полезная нагрузка указывает часовой пояс, а панель конвертирует его для отображения, поэтому два оператора в разных местах никогда не сравнивают цифры, которые незаметно охватывали разные дни. Выручка — это три числа, а не одно. Деньги, принятые через провайдера, потраченный кредит кошелька и возвращённые деньги отчитываются отдельно. Кредит кошелька уже был засчитан как выручка при пополнении баланса, поэтому его прибавление к деньгам провайдера считает его дважды. Они рисуются тремя столбиками рядом, по одному графику на валюту, и оба решения несут это же правило. Столбики сгруппированы, а не сложены в стопку, потому что стопка — это картинка суммы, а у этих трёх суммы нет. Валютам дан отдельный график каждой, потому что общая ось утверждает, что сотня одной сравнима с сотней другой. Точные цифры лежат в таблице под каждым графиком: график не заменяет число, по которому нужно действовать, а один из трёх цветов серий на светлой поверхности ниже порога контраста — поэтому ничто никогда не читается одним лишь цветом. Трафик наблюдаемый, а не авторитетный. Трафиком владеет Remnawave. Omniflow хранит счётчик, который наблюдал последним, и плитка помечена соответственно. Под числами лежит их форма. Под каждой группой те же данные картинкой: записи клиентов, права доступа и платёжные намерения — кольцами, очереди операций — одним баром, отсортированным по величине. Кольцами рисуется только состав, и показатель, входящий в другой, намеренно не рисуется: просроченные тикеты входят в открытые, а зависшие платежи — в те, что в процессе, поэтому доля для любого из них утверждала бы целое, которого нет. Очереди — бар, а не кольцо, по обратной причине: это несвязанные кучи, измеренные в один момент, и их сумма ничему не соответствует. Под каждым графиком лежит таблица с цифрами. Список «требует действия» выводится из тех же цифр, что показывают плитки, поэтому оповещение никогда не может противоречить соседней плитке. Каждая запись ведёт вглубь на обычную поверхность, которая её разрешает, а не предлагает короткий путь в обход привычного подтверждения и аудита. Каждая плитка, за которой стоит список, ведёт на этот список: число, которое выглядит неверным, можно открыть, а не искать.Клиенты
Поиск принимает идентификатор клиента Omniflow, идентификатор Telegram или имя пользователя Remnawave и определяет по форме, что именно ему передали. Полнотекстового поиска сознательно нет. Контактные значения хранятся как шифротекст плюс отпечаток именно затем, чтобы их нельзя было вычерпать, а поиск по подстроке в записях о клиентах — это тот самый запрос, который превращает аккаунт поддержки в аккаунт для массовой выгрузки. Сегменты фильтруют по отношению, а не по статусу: есть действующая подписка, истёкшая, никогда не покупал или помечен для проверки. Выгрузка клиентов в CSV (GET /v1/panel/customers/export) требует
customers.export в дополнение к customers.read, и это право есть только у
ролей owner и administrator. Найти одного клиента, чтобы ответить на тикет, и
скачать всех клиентов вместе с их Telegram ID — разные действия, поэтому
поддержка, маркетинг и аудитор могут первое, но не второе. Каждая выгрузка пишет
в журнал аудита panel.customers.exported (категория customer) до отправки
первой строки, поэтому брошенная на полпути выгрузка тоже остаётся в журнале.
Профиль клиента перечисляет все одновременные подписки с правом на услугу,
которое ими сейчас управляет, а также заказы, движения по кошельку, историю
обращений, состояние согласий и любые совпадения по рискам. Вкладка кошелька
закрыта правом finance.read, а не доступом к клиентам, поэтому оператор
поддержки может видеть, что у клиента есть баланс, не читая каждое движение,
которое его сформировало.
Блокировка клиента не отключает пользователя в Remnawave напрямую. Конвейер
выдачи доступа наблюдает за состоянием клиента; клик в панели в обход него
оставил бы Omniflow и Remnawave в разногласии без записи о причине.
Каталог
Тарифы, их версии и промоакции. Каталог версионируется, поэтому здесь ничто не переписывает историю: скрытие или архивирование тарифа останавливает новые покупки и оставляет каждый уже купивший его заказ оценённым по неизменяемой версии, по которой он был продан. Список тарифов показывает, сколько строк заказов ссылается на тариф, чтобы архивирование используемого тарифа было видимым решением, а не сюрпризом. В v0.7 награды промоакций расширены за пределы скидок:
Две промоакции комбинируются, только если обе помечены как складываемые. Поле
precedence задаёт порядок вычисления, от большего к меньшему, поэтому
применение нескольких промоакций детерминировано, а не зависит от порядка, в
котором клиент их ввёл.
Вид промоакции не редактируется. Превращение действующей промоакции из
процентной скидки в кредит кошелька незаметно изменило бы то, сколько стоит
каждый непогашенный код; другая награда — это другая промоакция.
Финансы
Поиск заказов, очередь сверки и разбор неудавшихся списаний. Очередь сверки перечисляет платёжные намерения, которые находятся в пути дольше, чем должен занимать любой поддерживаемый провайдер. Сверка заново опрашивает провайдера через существующий идемпотентный платёжный сервис — панель записывает, кто попросил и почему, и никогда не решает, чем закончится платёж. У экспорта в CSV стабильная схема: более поздняя версия добавляет столбец в конец и никогда не вставляет его в середину, поэтому импортёр, написанный под более старый экспорт, продолжает работать. Суммы — целые числа в минорных единицах валюты, а все отметки времени в UTC, и то и другое указано в названиях столбцов, а не только здесь.Задания и диагностика
Неудавшиеся операции выдачи доступа, обратные вызовы провайдеров, расхождение с Remnawave и накопившийся outbox. Повтор ставит ту же операцию в очередь под тем же ключом идемпотентности, поэтому воркер по-прежнему выполняет её ровно один раз. Отмена предлагается только для операции, которая ещё не завершилась успехом: завершённая выдача доступа не может быть отозвана кликом в панели. Вебхук можно проиграть заново, только если он завершился неудачей или был проигнорирован, — повторная обработка ключуется по идентификатору события провайдера, поэтому второй проход приходит в то же терминальное состояние, а не применяется дважды. Ни одно из этих представлений не показывает полезную нагрузку провайдера или тело ответа Remnawave. Они несут статус, классификацию и корреляционный идентификатор — то, что нужно оператору, чтобы решить, стоит ли повторять, и ничто из этого не может вывести ссылку на подписку на экран оператора.Настройки
Лимиты пополнения кошелька и одновременность подписок были переменными окружения до v0.7. Теперь это строки вcommerce_settings.
Путь миграции продуман. При первом запуске после обновления API заполняет строку
из того окружения, которое у установки уже было, поэтому оператор, пришедший с
v0.5, сохраняет настроенные им лимиты, а не получает значения по умолчанию из
схемы. С этого момента панель — единственное, что их пишет, а переменные
окружения остаются читаемыми, поэтому восстановление в свежую базу данных
воспроизводит ту же исходную точку.
Список провайдеров показывает то, что настроил оператор, рядом с тем, что
фактически объявляет каждый скомпилированный адаптер. Недоступный переключатель
регулярных платежей не просто отклоняется, а объясняется: либо у интеграции нет
способа привязать способ оплаты, либо собственный торговый аккаунт оператора не
прошёл проверку возможностей.
Массовые действия
Массовое действие сначала предпросматривается, затем применяется. Начаться может только операция в состоянииready, поэтому «предпросмотр последствий перед
массовым изменением» — свойство базы данных, а не привычка панели: пути от
запроса к применённому изменению в обход предпросмотра просто нет.
Одно действие ограничено 1000 целями. Это именно предел, а не страница:
изменение, предпросмотр которого оператор не в состоянии просмотреть, — это
изменение, которое на самом деле никто не одобрял. У каждого запуска своя
обязательная причина, своё событие аудита и исход по каждому элементу, а
счётчики пересчитываются по элементам, а не инкрементируются, поэтому
перезапущенный воркер не может посчитать дважды.
Выполняющаяся операция не может быть отменена. Часть её целей уже изменена, и
наполовину применённое действие с отчётом «отменено» исказило бы то, что
произошло; операция доходит до конца, а результаты по элементам говорят точно,
какие цели были затронуты.
Предпросмотр требует заголовка Idempotency-Key, поэтому дважды отправленная
форма возвращает уже созданный предпросмотр, а не второй.