docs: refactor docs structure
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# Админ-панель Web App
|
||||
|
||||
Админ-панель доступна пользователям, чей Telegram ID указан в `ADMIN_IDS`. В Web App такие пользователи видят раздел администрирования; все API `/api/admin/*` дополнительно проверяют сессию и `ADMIN_IDS` на сервере. Доступ проверяется по **Telegram ID**, сохранённому у записи пользователя в базе: аккаунт только с email без привязки Telegram не получит права администратора, даже если его идентификатор ошибочно указан в `ADMIN_IDS`.
|
||||
|
||||
## Возможности
|
||||
|
||||
- дашборд со статистикой пользователей, платежей и синхронизации с Remnawave;
|
||||
- список пользователей с поиском, фильтрами и колонкой premium-трафика; карточка пользователя с активной подпиской, обычным и premium-трафиком, платежами и действиями (подробнее в разделе «Пользователи» ниже);
|
||||
- блокировка пользователей, входящий список тикетов поддержки, рассылки, промокоды и просмотр логов;
|
||||
- ручная синхронизация с Remnawave;
|
||||
- редактор разрешенных настроек приложения из manifest-файла;
|
||||
- раздел **Внешний вид** для логотипа, emoji-логотипа, выбора темы, accent-цвета, масштаба логотипа и предпросмотра тем;
|
||||
- раздел **Инструкции подключения** для встроенной страницы установки, поведения кнопок бота и Remnawave Subscription Page config;
|
||||
- редактор JSON-каталога тарифов;
|
||||
- загрузка Internal Squads из Remnawave для выбора в тарифах.
|
||||
|
||||
## Пользователи
|
||||
|
||||
Раздел **Пользователи** — таблица с **пагинацией по 25 записей**. Строка поиска ищет по внутреннему числовому ID, Telegram ID, фрагменту `@username`, имени или email; применение — кнопка «Найти» или клавиша Enter в поле поиска.
|
||||
|
||||
**Фильтры:**
|
||||
|
||||
- состояние аккаунта: все / не забанены / забанены;
|
||||
- наличие Telegram или email;
|
||||
- связь с панелью Remnawave (`panel_user_uuid`);
|
||||
- **статус подписки в панели** для активной подписки в базе бота: active, expired, limited;
|
||||
- **premium-трафик**: все; без лимита premium в тарифе; безлимитный оверрайд; норма (ниже 85% квоты); предупреждение (от 85% до 100%); исчерпана квота.
|
||||
|
||||
**Сортировка:** по дате регистрации, имени, ID, а также по **доле использования premium-квоты** (если у активной подписки есть конечный premium-лимит).
|
||||
|
||||
В колонке premium для конечной квоты показывается израсходовано относительно лимита; лимит складывается из базовой premium-квоты тарифа, докупок и бонусов. Отображение **не опирается** на служебный флаг «limited» в базе как на признак «есть premium-трафик» — он отражает другую семантику на стороне панели.
|
||||
|
||||
Карточка пользователя по переходу из списка объединяет просмотр подписки, трафика, платежей и действий (блокировка, сообщения, продление, оверрайды и т.д.) в соответствии с доступными эндпоинтами `/api/admin/users/*`.
|
||||
|
||||
## Настройки
|
||||
|
||||
Раздел **Система -> Настройки** позволяет менять приложение без редактирования `.env`, но только в пределах allowlist из `backend/bot/app/web/admin_settings_manifest.py`. Даже администратор не может менять через UI произвольные атрибуты `Settings`.
|
||||
|
||||
Изменения сохраняются как overrides в базе данных и применяются поверх `.env` без перезапуска. Если значение сброшено, снова используется значение из `.env`. После сохранения публичный кеш настроек Web App сбрасывается, поэтому пользователи видят изменения при следующих запросах.
|
||||
|
||||
В manifest сейчас входят:
|
||||
|
||||
- общие параметры: язык, валюта, ссылки поддержки, документы, обязательный канал, Remnawave-доступы и поведение `/start`;
|
||||
- внешний вид и доступность Web App: название, цвет, логотип, emoji-логотип и `WEBAPP_ENABLED`;
|
||||
- инструкции подключения: `SUBSCRIPTION_GUIDES_ENABLED`, `SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED`, чтение конфига из Remnawave Panel, JSON-override и fallback-путь к файлу;
|
||||
- legacy-цены без JSON-каталога: периоды подписки, RUB/Stars цены и пакеты трафика;
|
||||
- платежные провайдеры: включение методов, порядок кнопок, публичные параметры и секреты YooKassa, FreeKassa, Platega, SeverPay, Wata, CryptoPay, Heleket и Stars, а также текст и иконки кнопок оплаты;
|
||||
- пробный период, реферальные бонусы, уведомления, логирование, поддержка, раздел устройств, лимит устройств и legacy-лимиты трафика.
|
||||
|
||||
Секретные поля помечены как secret и не должны использоваться для произвольного просмотра старых значений. Настройки, которых нет в manifest, остаются только в `.env` или коде.
|
||||
|
||||
Для каждого платежного метода в разделе провайдера доступны presentation-настройки `PAYMENT_<METHOD>_WEBAPP_LABEL_RU`, `PAYMENT_<METHOD>_WEBAPP_LABEL_EN`, `PAYMENT_<METHOD>_WEBAPP_ICON`, `PAYMENT_<METHOD>_TELEGRAM_LABEL_RU`, `PAYMENT_<METHOD>_TELEGRAM_LABEL_EN` и `PAYMENT_<METHOD>_TELEGRAM_EMOJI`. Пустое значение возвращает мультиязычный дефолт из модуля платежного провайдера. Иконка Web App выбирается из уже подключённых lucide-иконок (`frontend/src/lib/components/ui/icons.js`) через модалку в админке.
|
||||
|
||||
## Переводы
|
||||
|
||||
Раздел **Система -> Переводы** позволяет переопределять отдельные строки из `locales/ru.json` и `locales/en.json` без монтирования полного файла локализации. Строки сгруппированы по месту применения: админка, Mini App, Telegram-бот, платежи, подписки, поддержка и другие группы.
|
||||
|
||||
В разделе строки разделены по аудитории: пользовательские тексты Mini App/бота/платежей и внутренние тексты админки, логов и синхронизации. Дополнительные языки можно добавлять прямо в интерфейсе по коду локали, например `uk`, `de` или `pt-BR`; для таких языков оверрайды хранятся без отдельного базового файла локали, а отсутствующие строки берутся из языка по умолчанию.
|
||||
|
||||
Файл `data/locales-overrides.json` считается источником правды, а таблица `locale_overrides` хранит его DB-зеркало. Если валидный JSON-файл есть, при старте backend и worker полностью синхронизируют БД с файлом, включая удаления строк. Если файл отсутствует, но каталог доступен для записи, он автоматически создается из текущих DB-оверрайдов или как пустой `{}`. Если файл не примонтирован, недоступен или временно сломан по JSON, используется fallback из БД. Сохранение из админки записывает полный итоговый снапшот и в JSON-файл, и в БД; если активный JSON-файл есть, но его нельзя перезаписать, сохранение отклоняется, чтобы БД не разъехалась с главным файлом.
|
||||
|
||||
Формат файла:
|
||||
|
||||
```json
|
||||
{
|
||||
"ru": {
|
||||
"menu_personal_account_button": "Личный кабинет"
|
||||
},
|
||||
"en": {
|
||||
"menu_personal_account_button": "Account"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Инструкции подключения
|
||||
|
||||
Секция **Система -> Настройки -> Инструкции подключения** управляет встроенным экраном установки. `SUBSCRIPTION_GUIDES_ENABLED` включает `/install` в личном кабинете, а `SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED` заставляет кнопки подключения в Telegram-боте открывать Mini App вместо финальной Remnawave Subscription Page. Оба переключателя включены по умолчанию.
|
||||
|
||||
По умолчанию Minishop читает Remnawave Subscription Page config из панели (`SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED=True`). Это основной режим, потому что один и тот же конфиг используется и в панели, и во встроенной инструкции. JSON-поле `SUBSCRIPTION_PAGE_CONFIG_JSON` применяется только когда явно включен `SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED`; иначе оно может храниться в админке, но не влияет на пользователей. `SUBSCRIPTION_PAGE_CONFIG_PATH` остается fallback-путем к локальному v1 JSON-файлу, если конфиг панели отключен или недоступен.
|
||||
|
||||
При сохранении backend валидирует JSON-override как Remnawave Subscription Page v1 config. Ошибки показываются как обычные validation errors настроек, а если рабочий конфиг недоступен, пользовательская кнопка подключения откатывается к старой финальной ссылке подписки.
|
||||
|
||||
## Поддержка
|
||||
|
||||
Раздел **Коммуникации -> Поддержка** показывает входящий список тикетов из Mini App. В списке доступны фильтры по статусу, приоритету, категории и назначенному администратору, поиск по теме и пользователю, сортировка по обновлению, созданию или важности.
|
||||
|
||||
В карточке тикета администратор видит диалог, пользовательский контекст и действия: ответить пользователю, оставить внутреннюю заметку, изменить статус, приоритет, категорию или исполнителя, закрыть тикет и перейти в карточку пользователя. Внутренние заметки не показываются пользователю.
|
||||
|
||||
Счетчик непрочитанных обращений отображается в навигации админки. Уведомления о новых тикетах и ответах пользователя настраиваются через `LOG_SUPPORT`, `LOG_SUPPORT_THREAD_ID` и параметры `SUPPORT_*`. Подробности: [support.md](support.md).
|
||||
|
||||
## Внешний вид
|
||||
|
||||
Раздел **Внешний вид** объединяет настройки бренда и темы Web App. Логотип можно загрузить файлом или по HTTPS-ссылке; backend сохраняет файл в `data/webapp-logo/uploads` и подставляет локальный URL. Если включен emoji-логотип, картинка скрывается, а для emoji можно выбрать системный, Twemoji, Noto Color, animated Noto и другие варианты отрисовки.
|
||||
|
||||
В блоке тем админка читает каталог из `WEBAPP_THEMES_DIR`, показывает встроенные и кастомные темы, позволяет выбрать текущую тему, изменить accent, включить или выключить тему для админки и настроить масштаб логотипа на главной и экране входа. Кнопка предпросмотра открывает `/home?theme_preview=<key>` и не меняет глобальную тему до сохранения.
|
||||
|
||||
Подробный формат `theme.json`, CSS/asset-роуты и пошаговый пайплайн создания новой темы описаны в [webapp-themes.md](webapp-themes.md).
|
||||
|
||||
## Тарифы
|
||||
|
||||
Раздел **Система -> Тарифы** работает с файлом `TARIFFS_CONFIG_PATH` (по умолчанию `data/tariffs.json`). При сохранении backend валидирует payload через `TariffsConfig`, пишет JSON в UTF-8 и сбрасывает кеш публичных данных Web App.
|
||||
|
||||
Если файла еще нет, админка открывает пустой каталог. Перед сохранением должен быть хотя бы один включенный тариф, а `default_tariff` должен ссылаться на включенный тариф.
|
||||
|
||||
Редактор тарифа разделен на вкладки:
|
||||
|
||||
- **Основное**: ключ, модель `period`/`traffic`, видимость, названия и описания RU/EN, базовые Internal Squads, HWID-лимит, месячный лимит или курс конвертации;
|
||||
- **Цены**: периоды и цены для `period`, пакеты GB и цены для `traffic`;
|
||||
- **Докупки**: обычные пакеты докупки трафика для `period`; для `traffic` отдельные докупки не нужны, пользователь повторно покупает пакеты из `traffic_packages`;
|
||||
- **Premium**: названия premium-раздела RU/EN, premium Internal Squads, месячный premium-лимит и RUB/Stars пакеты premium-докупки;
|
||||
- **Устройства**: RUB/Stars пакеты докупки HWID-устройств.
|
||||
|
||||
Базовые и premium Internal Squads выбираются из Remnawave через `/api/admin/panel/internal-squads`. Если панель недоступна, можно сохранить уже существующие UUID в JSON, но выпадающий список не загрузится.
|
||||
|
||||
## Практические замечания
|
||||
|
||||
Не меняйте `key` опубликованного тарифа без миграции активных подписок: подписки, платежи и смена тарифа ссылаются на этот ключ.
|
||||
|
||||
Отключенный тариф исчезает с витрины, но активные подписки с его `tariff_key` продолжают существовать. Удаление тарифа безопасно только если не осталось активных подписок, которым нужна докупка, продление или смена с этого тарифа.
|
||||
|
||||
Для Docker важно, чтобы путь `TARIFFS_CONFIG_PATH` был доступен на запись контейнеру. Если каталог `./data` смонтирован в `/app/data`, админка может сохранять `data/tariffs.json`.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Основные возможности
|
||||
|
||||
Minishop закрывает путь от регистрации пользователя до оплаты, продления, поддержки и сопровождения подписки.
|
||||
|
||||
## Для пользователей
|
||||
|
||||
- Регистрация через Telegram Mini App или email-код.
|
||||
- Просмотр подписки, срока действия, трафика и ссылки подключения.
|
||||
- Покупка подписки, пакетов трафика и дополнительных устройств.
|
||||
- Пробный период, промокоды и реферальные сценарии.
|
||||
- Тикеты поддержки внутри Mini App.
|
||||
- Встроенные инструкции установки и публичные ссылки `/s/<token>`.
|
||||
|
||||
## Для администраторов
|
||||
|
||||
- Поиск и управление пользователями.
|
||||
- Настройка платежей, тарифов, внешнего вида и поддержки.
|
||||
- Рассылки, промокоды и логи действий.
|
||||
- Ручная синхронизация с Remnawave Panel.
|
||||
- Редактор JSON-каталога тарифов.
|
||||
|
||||
Подробности: [админ-панель](admin-panel.md), [Mini App](web-app.md) и [поддержка](support.md).
|
||||
@@ -0,0 +1,28 @@
|
||||
# Платежи
|
||||
|
||||
Платежные методы включаются настройками и отображаются пользователю как кнопки оплаты в Mini App и Telegram-сценариях.
|
||||
|
||||
## Поддерживаемые провайдеры
|
||||
|
||||
- [YooKassa](../payments/yookassa.md)
|
||||
- [FreeKassa](../payments/freekassa.md)
|
||||
- [Platega](../payments/platega.md)
|
||||
- [SeverPay](../payments/severpay.md)
|
||||
- [Wata](../payments/wata.md)
|
||||
- [CryptoPay](../payments/cryptopay.md)
|
||||
- [Heleket](../payments/heleket.md)
|
||||
- [Telegram Stars](../payments/telegram-stars.md)
|
||||
|
||||
## Типовой порядок настройки
|
||||
|
||||
1. Включите нужный провайдер в админке или через `.env`.
|
||||
2. Заполните публичные параметры и секреты.
|
||||
3. Настройте webhook URL у провайдера, если это требуется.
|
||||
4. Проверьте порядок и подписи кнопок оплаты.
|
||||
5. Выполните тестовый платеж и проверьте логи backend.
|
||||
|
||||
## Где смотреть параметры
|
||||
|
||||
- [Справочник `.env`](../configuration/env-vars.md) содержит все ключи провайдеров.
|
||||
- [Админ-панель](admin-panel.md) описывает UI-настройки платежей.
|
||||
- [Тарифы](tariffs.md) описывают цены, Stars и сценарии покупки.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Подписки
|
||||
|
||||
Подписки управляются через каталог тарифов и синхронизируются с Remnawave Panel.
|
||||
|
||||
## Модели тарифов
|
||||
|
||||
- **Period** - подписка на срок с месячным лимитом трафика.
|
||||
- **Traffic** - покупка пакетов трафика без привязки к периоду.
|
||||
- **Premium** - отдельные premium-сквады и premium-лимит.
|
||||
- **HWID-устройства** - докупка дополнительных устройств при включенном разделе устройств.
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
- создание пользователя в панели;
|
||||
- применение пробного периода или покупки;
|
||||
- продление и докупки;
|
||||
- предупреждения по трафику;
|
||||
- синхронизация подписки и статусов;
|
||||
- обработка смены тарифа.
|
||||
|
||||
Подробности: [тарифы](tariffs.md) и [Mini App](web-app.md).
|
||||
@@ -0,0 +1,82 @@
|
||||
# Поддержка
|
||||
|
||||
В проекте есть два канала поддержки:
|
||||
|
||||
- внешняя ссылка `SUPPORT_LINK`, которая ведет пользователя в Telegram-чат, канал, форму или любой другой публичный URL;
|
||||
- встроенные тикеты Web App / Mini App, если включен `SUPPORT_TICKETS_ENABLED`.
|
||||
|
||||
Внешняя ссылка остается простым резервным каналом. Тикеты дают полноценный диалог внутри личного кабинета: пользователь создает обращение, видит историю ответов, получает счетчик непрочитанных сообщений, а администратор отвечает из админ-панели.
|
||||
|
||||
## Пользовательский сценарий
|
||||
|
||||
Раздел **Поддержка** появляется в Web App, когда `SUPPORT_TICKETS_ENABLED=True`. Пользователь может:
|
||||
|
||||
- создать тикет с темой, категорией, приоритетом и первым сообщением;
|
||||
- выбрать категорию `billing`, `technical`, `account` или `other`;
|
||||
- выбрать приоритет `normal` или `high`;
|
||||
- открыть список своих тикетов с фильтром по активным и всем обращениям;
|
||||
- отвечать в открытом тикете и видеть ответы поддержки;
|
||||
- перейти по `SUPPORT_LINK`, если нужна внешняя поддержка.
|
||||
|
||||
Заблокированные пользователи не могут создавать тикеты и отвечать в них. Для пользователей показываются только обычные сообщения: внутренние заметки администраторов скрыты.
|
||||
|
||||
## Админский сценарий
|
||||
|
||||
В админ-панели тикеты доступны в разделе **Коммуникации -> Поддержка**. Доступ проверяется так же, как и для остальных `/api/admin/*`: нужна Web App-сессия пользователя, чей Telegram ID указан в `ADMIN_IDS`.
|
||||
|
||||
Администратор может:
|
||||
|
||||
- видеть сводку по открытым, ожидающим ответа, закрытым и непрочитанным тикетам;
|
||||
- фильтровать обращения по статусу, приоритету, категории и назначенному администратору;
|
||||
- искать по теме, username, имени и email пользователя;
|
||||
- сортировать по обновлению, созданию или важности;
|
||||
- отвечать пользователю, менять статус, категорию, приоритет и исполнителя;
|
||||
- оставлять внутренние заметки, которые видны только администраторам;
|
||||
- открыть карточку пользователя и видеть контекст подписки: тариф, статус, остаток времени, обычный и premium-трафик.
|
||||
|
||||
Статусы тикета: `open`, `awaiting_user`, `awaiting_admin`, `resolved`, `closed`. При создании тикет сразу получает статус `awaiting_admin`; ответ пользователя переводит незакрытый тикет в `awaiting_admin`, ответ администратора - в `awaiting_user`. Закрытые статусы считаются `resolved` и `closed`.
|
||||
|
||||
## Уведомления
|
||||
|
||||
Новые тикеты и ответы пользователя могут отправляться в Telegram-уведомления администраторам и в лог-чат. Для отдельного топика поддержки используйте `LOG_SUPPORT_THREAD_ID`; если он пустой, сообщения идут в общий `LOG_THREAD_ID`/чат по настройкам логирования.
|
||||
|
||||
Повторные уведомления по одному непрочитанному тикету ограничиваются cooldown-настройками, чтобы не заспамить админов:
|
||||
|
||||
- `SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS` - пауза для Telegram/log уведомлений;
|
||||
- `SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS` - пауза для email-уведомлений.
|
||||
|
||||
Email-уведомления администраторам включаются через `SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED=True`. Письма отправляются только администраторам из `ADMIN_IDS`, у которых в базе есть email. Для отправки нужен рабочий SMTP-конфиг, как и для входа по email.
|
||||
|
||||
Ответ администратора и закрытие тикета дополнительно отправляются пользователю в Telegram, если у него есть Telegram-аккаунт, и на email, если он привязан.
|
||||
|
||||
## Настройки
|
||||
|
||||
| Переменная | Назначение |
|
||||
| --- | --- |
|
||||
| `SUPPORT_LINK` | Внешняя ссылка поддержки. Показывается в боте и Web App как быстрый способ связаться с командой. |
|
||||
| `SUPPORT_TICKETS_ENABLED` | Включает раздел тикетов в Mini App и разрешает создание обращений. |
|
||||
| `SUPPORT_TICKET_MAX_BODY_LENGTH` | Максимальная длина сообщения тикета. |
|
||||
| `SUPPORT_TICKET_MAX_SUBJECT_LENGTH` | Максимальная длина темы тикета. |
|
||||
| `SUPPORT_TICKET_RATE_LIMIT_PER_HOUR` | Сколько новых тикетов пользователь может создать за час; `0` отключает лимит. |
|
||||
| `LOG_SUPPORT` | Включает Telegram/log уведомления по тикетам поддержки. |
|
||||
| `LOG_SUPPORT_THREAD_ID` | Необязательный ID топика в лог-чате для сообщений поддержки. |
|
||||
| `SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED` | Включает email-уведомления администраторам о новых тикетах и ответах пользователей. |
|
||||
| `SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS` | Минимальная пауза между повторными Telegram/log уведомлениями по одному непрочитанному тикету. |
|
||||
| `SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS` | Минимальная пауза между повторными email-уведомлениями по одному непрочитанному тикету. |
|
||||
|
||||
Все эти параметры описаны в [env-vars.md](../configuration/env-vars.md). Основной рекомендуемый способ менять их - админка **Система -> Настройки -> Поддержка**; значения применяются как override поверх `.env`.
|
||||
|
||||
## API и хранение
|
||||
|
||||
Пользовательские маршруты:
|
||||
|
||||
- `GET /api/support/tickets` - список тикетов пользователя;
|
||||
- `POST /api/support/tickets` - создать тикет;
|
||||
- `GET /api/support/tickets/{id}` - открыть тикет;
|
||||
- `POST /api/support/tickets/{id}/messages` - отправить ответ;
|
||||
- `POST /api/support/tickets/{id}/read` - отметить сообщения прочитанными;
|
||||
- `GET /api/support/unread` - счетчик непрочитанных ответов поддержки.
|
||||
|
||||
Админские маршруты находятся под `/api/admin/support/*`: список, карточка тикета, ответ, изменение статуса/приоритета/категории/исполнителя, отметка прочитанного и статистика.
|
||||
|
||||
Данные хранятся в таблицах `support_tickets` и `support_ticket_messages`; миграция применяется автоматически сервисом `migrate` при `docker compose up -d --build`.
|
||||
@@ -0,0 +1,329 @@
|
||||
# Тарифы
|
||||
|
||||
Бот поддерживает два способа описания продаж:
|
||||
|
||||
- JSON-каталог тарифов из `TARIFFS_CONFIG_PATH` (по умолчанию `data/tariffs.json`);
|
||||
- конфигурация через переменные `.env`, если JSON-файл отсутствует.
|
||||
|
||||
JSON-каталог может содержать несколько тарифов разных моделей: подписки на срок, пакеты трафика без срока действия, разные наборы Internal Squads, лимиты устройств и пакеты докупки. Пример формата: [data/tariffs.example.json](https://gitlab.com/3252a8/remnawave-minshop/-/blob/main/data/tariffs.example.json).
|
||||
|
||||
Коротко по моделям:
|
||||
|
||||
- `period` - подписка на срок с месячным лимитом трафика и опциональной докупкой GB поверх месячного лимита;
|
||||
- `traffic` - покупка пакетов GB без пользовательского срока действия, где повторная покупка добавляет трафик к текущему остатку;
|
||||
- premium-сквады - дополнительный набор Internal Squads внутри любого тарифа, с отдельным названием, счетчиком, месячным лимитом и отдельными пакетами докупки.
|
||||
|
||||
## Управление через админку
|
||||
|
||||
Каталог тарифов можно настраивать из Web App админки: раздел **Система → Тарифы**. Админка читает и сохраняет файл из `TARIFFS_CONFIG_PATH`, валидирует данные той же моделью `TariffsConfig`, что и бот, и атомарно перезаписывает JSON только после успешной проверки.
|
||||
|
||||
В интерфейсе доступны:
|
||||
|
||||
- добавление, редактирование и удаление тарифов;
|
||||
- включение и выключение тарифа на витрине;
|
||||
- выбор тарифа по умолчанию;
|
||||
- настройка `period`-тарифов: месячный лимит, периоды, RUB/Stars цены, пакеты докупки трафика;
|
||||
- настройка `traffic`-тарифов: пакеты GB, RUB/Stars цены, курс конвертации;
|
||||
- настройка базовых Internal Squads из списка Remnawave;
|
||||
- настройка premium-раздела: названия RU/EN, premium Internal Squads, месячный premium-лимит и RUB/Stars пакеты докупки premium-трафика;
|
||||
- настройка базового HWID-лимита и пакетов докупки устройств.
|
||||
|
||||
После сохранения изменения применяются к новым запросам Web App сразу, потому что конфиг тарифов загружается из JSON при обращении. Уже созданные подписки сохраняют свой `tariff_key`; при удалении или отключении тарифа проверьте, что активные подписки с этим ключом не требуют дальнейшего продления или смены.
|
||||
|
||||
Подробности по админ-панели, правам доступа, сохранению настроек и списку разделов есть в [админ-панели](admin-panel.md).
|
||||
|
||||
## Как выбирается режим
|
||||
|
||||
Если файл из `TARIFFS_CONFIG_PATH` существует и проходит валидацию, используется каталог тарифов. В этом режиме `TRAFFIC_PACKAGES` и цены подписок из `.env` не формируют витрину продаж, потому что цены и пакеты берутся из JSON.
|
||||
|
||||
Если JSON-файл отсутствует, бот использует значения `.env`:
|
||||
|
||||
- `RUB_PRICE_*`, `STARS_PRICE_*` и `*_MONTHS_ENABLED` для подписок на срок;
|
||||
- `TRAFFIC_PACKAGES` и `STARS_TRAFFIC_PACKAGES` для продажи пакетов трафика;
|
||||
- `USER_TRAFFIC_LIMIT_GB`, `USER_TRAFFIC_STRATEGY`, `USER_SQUAD_UUIDS`, `USER_HWID_DEVICE_LIMIT` для пользователей Remnawave.
|
||||
|
||||
В режиме без JSON-каталога наличие `TRAFFIC_PACKAGES` или `STARS_TRAFFIC_PACKAGES` переключает витрину на продажу трафика вместо подписок на срок.
|
||||
|
||||
## Структура JSON-каталога
|
||||
|
||||
Минимальная структура:
|
||||
|
||||
```json
|
||||
{
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": { "ru": "Стандарт", "en": "Standard" },
|
||||
"descriptions": { "ru": "Базовый набор серверов" },
|
||||
"premium_names": { "ru": "Premium-серверы", "en": "Premium servers" },
|
||||
"squad_uuids": ["uuid-1"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 500,
|
||||
"prices_rub": { "1": 150, "3": 400 },
|
||||
"enabled_periods": [1, 3],
|
||||
"topup_packages": {
|
||||
"rub": [{ "gb": 10, "price": 99 }],
|
||||
"stars": [{ "gb": 10, "price": 2500 }]
|
||||
},
|
||||
"hwid_device_packages": {
|
||||
"rub": [
|
||||
{
|
||||
"count": 1,
|
||||
"price": 99,
|
||||
"prices": { "1": 99, "3": 249 },
|
||||
"min_price": 20
|
||||
}
|
||||
],
|
||||
"stars": [{ "count": 1, "price": 50, "prices": { "1": 50, "3": 130 } }]
|
||||
},
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Основные поля:
|
||||
|
||||
| Поле | Назначение |
|
||||
| --- | --- |
|
||||
| `default_tariff` | Тариф по умолчанию для первичного выбора и привязки активных подписок без `tariff_key`. |
|
||||
| `tariffs[].key` | Стабильный ключ тарифа. Используется в платежах, подписках и смене тарифа. |
|
||||
| `tariffs[].names` | Названия тарифа по языкам. |
|
||||
| `tariffs[].descriptions` | Описания тарифа по языкам. |
|
||||
| `tariffs[].enabled` | Доступность тарифа на витрине. |
|
||||
| `tariffs[].squad_uuids` | Internal Squads Remnawave для пользователей тарифа. |
|
||||
| `tariffs[].premium_names` | Название premium-раздела по языкам. Используется в карточке лимита, модалке докупки premium-трафика и предупреждениях. Если поле не задано, используется `Premium-серверы` / `Premium servers`. |
|
||||
| `tariffs[].premium_squad_uuids` | Internal Squads с отдельным premium-лимитом. Ноды для учета берутся автоматически из accessible nodes этих сквадов через API панели. |
|
||||
| `tariffs[].premium_monthly_gb` | Отдельный месячный лимит трафика по premium-сквадам. `0` или отсутствие поля отключает отдельное ограничение. |
|
||||
| `tariffs[].premium_topup_packages` | Пакеты докупки premium-трафика в формате `{ "rub": [{ "gb": 10, "price": 99 }], "stars": [...] }`. Требуют `premium_squad_uuids`. |
|
||||
| `tariffs[].billing_model` | Модель тарифа: `period` или `traffic`. |
|
||||
| `tariffs[].hwid_device_limit` | Базовый лимит HWID-устройств. `0` означает безлимит, отсутствие поля использует `USER_HWID_DEVICE_LIMIT`. |
|
||||
| `tariffs[].hwid_device_packages` | Пакеты докупки устройств. `price` — legacy/monthly fallback, `prices` задаёт полную цену пакета для периодов тарифа (`"1"`, `"3"`, `"6"`, `"12"`), `min_price` задаёт минимальную цену prorate-докупки. |
|
||||
|
||||
Для `period`-тарифа также используются:
|
||||
|
||||
| Поле | Назначение |
|
||||
| --- | --- |
|
||||
| `monthly_gb` | Базовый месячный лимит трафика тарифа. `0` означает безлимит. |
|
||||
| `prices_rub` | Цены периодов в рублях, ключ - количество месяцев. |
|
||||
| `prices_stars` | Цены периодов в Telegram Stars. |
|
||||
| `enabled_periods` | Периоды, доступные для покупки. |
|
||||
| `topup_packages` | Пакеты докупки трафика именно для этого тарифа. Если поле не задано или списки пустые, докупка для тарифа не показывается в Web App и Telegram-боте. |
|
||||
|
||||
Для `traffic`-тарифа используются:
|
||||
|
||||
| Поле | Назначение |
|
||||
| --- | --- |
|
||||
| `traffic_packages` | Пакеты трафика в GB для рублей и Telegram Stars. |
|
||||
| `conversion_rate_rub_per_gb` | Курс для конвертации оставшихся дней period-тарифа в GB при смене на traffic-тариф. |
|
||||
|
||||
Если у traffic-тарифа нет RUB-пакетов, `conversion_rate_rub_per_gb` обязателен.
|
||||
|
||||
## Period-тарифы
|
||||
|
||||
`period` продает доступ на срок с месячным лимитом трафика.
|
||||
|
||||
При покупке или продлении:
|
||||
|
||||
- дата начала берется от текущей активной подписки, если она еще действует, иначе от текущего времени;
|
||||
- срок считается календарными месяцами через `add_months`;
|
||||
- промокод может добавить бонусные дни к рассчитанному сроку;
|
||||
- `tier_baseline_bytes` получает значение `monthly_gb`;
|
||||
- `topup_balance_bytes` сохраняется из текущей активной подписки;
|
||||
- `traffic_limit_bytes` становится `tier_baseline_bytes + topup_balance_bytes`;
|
||||
- в Remnawave отправляется `trafficLimitStrategy = MONTH`;
|
||||
- в Remnawave отправляются Internal Squads из тарифа;
|
||||
- в Remnawave отправляется эффективный HWID-лимит тарифа.
|
||||
|
||||
`MONTH` означает, что сброс использованного трафика выполняет Remnawave. Бот не рассчитывает дату сброса самостоятельно и не хранит отдельный период сброса для period-тарифов.
|
||||
|
||||
Докупка трафика для period-тарифа увеличивает `topup_balance_bytes` и общий `traffic_limit_bytes`. Этот баланс сохраняется в подписке и учитывается при продлении period-тарифа. В панель отправляется актуальный лимит, а доступ переводится в `ACTIVE`.
|
||||
|
||||
## Premium-сквады и отдельный лимит
|
||||
|
||||
Тариф может включать дополнительный набор Internal Squads с отдельным лимитом трафика. Это удобно для сценария “обычные серверы без изменений, premium-серверы ограничены отдельно”.
|
||||
|
||||
```json
|
||||
{
|
||||
"squad_uuids": ["standard-squad-uuid"],
|
||||
"premium_names": { "ru": "Premium-серверы", "en": "Premium servers" },
|
||||
"premium_squad_uuids": ["premium-squad-uuid"],
|
||||
"premium_monthly_gb": 50,
|
||||
"premium_topup_packages": {
|
||||
"rub": [{ "gb": 10, "price": 99 }],
|
||||
"stars": [{ "gb": 10, "price": 2500 }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- обычный лимит тарифа продолжает работать через `trafficLimitBytes` Remnawave;
|
||||
- premium-трафик считается отдельно по нодам, доступным из `premium_squad_uuids`;
|
||||
- список UUID нод не хранится в тарифе: бот запрашивает accessible nodes каждого premium-сквада у Remnawave и кеширует результат;
|
||||
- пока premium-лимит не исчерпан, пользователь получает `squad_uuids + premium_squad_uuids`;
|
||||
- при исчерпании premium-лимита бот убирает только premium-сквады, обычный доступ остается;
|
||||
- после докупки premium-трафика бот возвращает premium-сквады, если новый лимит снова больше использованного premium-трафика.
|
||||
- докупленный premium-трафик не сгорает в конце месяца: каждый месяц сначала расходуется `premium_monthly_gb`, а докупленный остаток уменьшается только на трафик сверх месячного лимита;
|
||||
- при новом календарном месяце счетчик premium-трафика и `premium_topup_used_bytes` сбрасываются, но `premium_topup_balance_bytes` переносится дальше.
|
||||
|
||||
Если `premium_squad_uuids` заданы, но `premium_monthly_gb` пустой или `0` и нет `premium_topup_packages`, premium-сквады работают как дополнительный доступ без отдельного ограничения. Если заданы `premium_topup_packages` или положительный `premium_monthly_gb`, `premium_squad_uuids` обязательны.
|
||||
|
||||
Состояние хранится в подписке:
|
||||
|
||||
- `premium_baseline_bytes` - базовый premium-лимит тарифа;
|
||||
- `premium_topup_balance_bytes` - оставшийся докупленный premium-трафик;
|
||||
- `premium_topup_used_bytes` - часть докупленного premium-трафика, уже потраченная в текущем месяце;
|
||||
- `premium_used_bytes` - использованный premium-трафик за текущий календарный месяц;
|
||||
- `premium_period_start_at` - месяц, к которому относится `premium_used_bytes`;
|
||||
- `premium_is_limited` - признак, что premium-сквад временно снят.
|
||||
|
||||
В пользовательском Web App premium-лимит показывается отдельной карточкой: использовано, лимит, остаток, докупленный переносимый остаток и список серверов/сквадов, на которые действует отдельное ограничение. В Telegram-разделе “Моя подписка” выводится тот же блок.
|
||||
|
||||
Обычная докупка и premium-докупка показываются отдельно. Обычная докупка использует `topup_packages` у `period`-тарифа или `traffic_packages` у `traffic`-тарифа. Premium-докупка использует только `premium_topup_packages`, получает `sale_mode=premium_topup` и в заголовке показывает `premium_names`, а не название обычной докупки.
|
||||
|
||||
Предупреждения по premium-лимиту отправляются отдельно от обычного трафика на тех же процентах `TARIFF_TRAFFIC_WARNING_LEVELS`. Сообщение использует название из `premium_names`, перечисляет серверы/сквады и ведет пользователя в докупку premium-трафика.
|
||||
|
||||
В Web App админке premium-сквады можно выбрать из выпадающего списка на вкладке **Premium** в редакторе тарифа. Список берется из API Remnawave (`/api/admin/panel/internal-squads`), поэтому UUID обычно не нужно копировать вручную.
|
||||
|
||||
## Traffic-тарифы
|
||||
|
||||
`traffic` продает объем трафика без пользовательского срока действия.
|
||||
|
||||
При покупке:
|
||||
|
||||
- `end_date` ставится в дальнюю дату `2099-01-01 UTC`, если у активной подписки нет более поздней даты;
|
||||
- `duration_months = 0`;
|
||||
- `period_start_at = NULL`;
|
||||
- `tier_baseline_bytes = 0`;
|
||||
- `topup_balance_bytes` хранит доступный остаток трафика;
|
||||
- в Remnawave отправляется `trafficLimitStrategy = NO_RESET`;
|
||||
- автопродление и уведомления о скором окончании срока отключаются для такой подписки.
|
||||
|
||||
Очередная покупка добавляет GB к фактическому остатку:
|
||||
|
||||
```text
|
||||
remaining = max(0, current_limit - current_used)
|
||||
balance_after = remaining + purchased
|
||||
limit_after = current_used + balance_after
|
||||
```
|
||||
|
||||
Так пользователь не теряет уже оплаченный остаток, а Remnawave продолжает считать общий лимит от текущего использованного трафика.
|
||||
|
||||
Если докупка трафика вызывается для traffic-тарифа, она обрабатывается как покупка очередного пакета этого же traffic-тарифа.
|
||||
|
||||
## HWID-устройства
|
||||
|
||||
Тариф может задавать базовый лимит устройств и пакеты докупки:
|
||||
|
||||
```json
|
||||
{
|
||||
"hwid_device_limit": 5,
|
||||
"hwid_device_packages": {
|
||||
"rub": [
|
||||
{
|
||||
"count": 1,
|
||||
"price": 99,
|
||||
"prices": { "1": 99, "3": 249, "6": 449, "12": 799 },
|
||||
"min_price": 20
|
||||
}
|
||||
],
|
||||
"stars": [{ "count": 1, "price": 50, "prices": { "1": 50, "3": 130 } }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- `hwid_device_limit` хранит базовый лимит тарифа;
|
||||
- `extra_hwid_devices` хранит только текущую активную сумму докупленных устройств;
|
||||
- срок действия каждой докупки хранится в `hwid_device_purchases.valid_from` / `valid_until`;
|
||||
- эффективный лимит равен `hwid_device_limit + active extra_hwid_devices`;
|
||||
- базовый лимит `0` означает безлимит, в Remnawave отправляется `hwidDeviceLimit = 0`;
|
||||
- при безлимитном базовом лимите докупка устройств не применяется;
|
||||
- полная цена HWID-пакета берется из `prices[duration_months]`; если периода нет, используется fallback `price * duration_months`;
|
||||
- фактическая цена докупки считается пропорционально оплачиваемому окну `valid_from -> valid_until` относительно периода подписки и фиксируется в платежe;
|
||||
- для Telegram Stars цена округляется вверх до целого Stars, для RUB — вверх до копеек; `min_price` защищает от микроплатежей в конце периода;
|
||||
- при продлении подписки докупленные устройства не продлеваются автоматически: старая докупка действует до прежнего `end_date`, а для нового срока создается отдельная `hwid_devices_renewal`-покупка;
|
||||
- `traffic`-тарифы не показывают и не принимают докупку HWID-устройств, потому что у них нет срока подписки;
|
||||
- при смене тарифа базовый лимит берется из целевого тарифа, а неиспользованная RUB-стоимость HWID-докупок конвертируется в дни нового period-тарифа или GB traffic-тарифа; XTR/Stars-докупки не конвертируются без явного курса и продолжают жить по своему `valid_until`;
|
||||
- история докупок пишется в `hwid_device_purchases`;
|
||||
- платеж хранит количество устройств в `payments.purchased_hwid_devices`.
|
||||
|
||||
Докупка устройств доступна в Web App через `/api/devices/topup-options` и `/api/payments`, а также в Telegram-боте из раздела устройств.
|
||||
|
||||
## Смена тарифа
|
||||
|
||||
Смена тарифа доступна для активных подписок с `tariff_key` и записывается в таблицу `tariff_changes`.
|
||||
|
||||
Варианты расчета:
|
||||
|
||||
| Переход | Поведение |
|
||||
| --- | --- |
|
||||
| `period -> period` | Остаток оплаченных дней оценивается по `effective_monthly_price_rub`, затем пересчитывается в дни целевого тарифа через месячную цену целевого тарифа. Неиспользованная RUB-стоимость HWID-докупок добавляется к этому расчету как дополнительные дни. Количество дней округляется вниз. |
|
||||
| `period -> period` с доплатой | Если целевой тариф дороже, может быть создан платеж `tariff_upgrade`; неиспользованная RUB-стоимость HWID-докупок уменьшает сумму доплаты. После оплаты применяется целевой тариф, а конвертированные HWID-окна закрываются. |
|
||||
| `period -> traffic` | Остаток оплаченных дней и неиспользованная RUB-стоимость HWID-докупок конвертируются в GB по `conversion_rate_rub_per_gb` или минимальной RUB-цене GB из пакетов целевого тарифа. |
|
||||
| `traffic -> period` | Пользователь выбирает и оплачивает период целевого тарифа; остаток GB сохраняется как `topup_balance_bytes` поверх лимита period-тарифа. |
|
||||
|
||||
При смене тарифа бот меняет:
|
||||
|
||||
- `tariff_key`;
|
||||
- Internal Squads в Remnawave;
|
||||
- `trafficLimitBytes`;
|
||||
- `trafficLimitStrategy`;
|
||||
- базовый HWID-лимит;
|
||||
- `effective_monthly_price_rub` для period-тарифов;
|
||||
- `auto_renew_enabled` и уведомления для traffic-тарифов.
|
||||
|
||||
## Платежи
|
||||
|
||||
В платежах используются поля:
|
||||
|
||||
| Поле | Назначение |
|
||||
| --- | --- |
|
||||
| `sale_mode` | Тип продажи: `subscription`, `traffic_package`, `topup`, `premium_topup`, `tariff_upgrade`, `hwid_devices`. |
|
||||
| `tariff_key` | Ключ тарифа, к которому относится платеж. |
|
||||
| `purchased_gb` | Купленный объем GB для traffic-пакетов и докупки трафика. |
|
||||
| `purchased_hwid_devices` | Количество устройств при докупке HWID. |
|
||||
| `hwid_valid_from`, `hwid_valid_until` | Зафиксированное окно действия HWID-докупки на момент создания платежа. |
|
||||
| `hwid_pricing_period_months`, `hwid_proration_ratio`, `hwid_full_price` | Метаданные расчета цены HWID-докупки: период тарифа, коэффициент prorate и полная цена пакета для периода. |
|
||||
| `subscription_duration_months` | Количество месяцев для подписки на срок; также используется платежными обработчиками как числовое поле покупки. |
|
||||
|
||||
В callback и metadata платежных провайдеров `sale_mode` может передаваться с суффиксом тарифа, например `subscription@standard` или `topup@standard`. При активации платежа тариф сохраняется отдельно в `tariff_key`.
|
||||
|
||||
## Предупреждения и исчерпание трафика
|
||||
|
||||
Remnawave ограничивает доступ при достижении `trafficLimitBytes`, переводя пользователя в статус `LIMITED`. Бот не удаляет пользователя из Internal Squads при 100% использования трафика.
|
||||
|
||||
`TariffTrafficWorker` запускается, когда активен JSON-каталог тарифов. Раз в 300 секунд он:
|
||||
|
||||
- синхронизирует из панели `status`, `trafficLimitBytes`, `usedTrafficBytes` и `trafficLimitStrategy`;
|
||||
- для period-тарифов выставляет `trafficLimitStrategy = MONTH`, если панель еще показывает другую стратегию;
|
||||
- отправляет предупреждения на уровнях из `TARIFF_TRAFFIC_WARNING_LEVELS` (по умолчанию `85,90,95`);
|
||||
- не отправляет `status=ACTIVE` при простой синхронизации стратегии, чтобы не снять статус `LIMITED`, выставленный Remnawave;
|
||||
- дедуплицирует предупреждения через `traffic_warnings`.
|
||||
|
||||
Для period-тарифов дедупликация предупреждений привязана к началу текущего месяца. Для traffic-тарифов она учитывает текущий `trafficLimitBytes`, чтобы после покупки очередного пакета пользователь мог получить следующий набор предупреждений.
|
||||
|
||||
Подписки, которые были ограничены логикой предыдущих запусков бота (`is_throttled=True`), восстанавливаются воркером только когда лимит снова больше использованного трафика.
|
||||
|
||||
## Автопродление, пробный период и бонусы
|
||||
|
||||
Автопродление через YooKassa применяется к подпискам на срок. Для режима продажи трафика без JSON-каталога автопродление пропускается. Для traffic-тарифов JSON-каталога покупка является пакетом трафика, а не периодической подпиской.
|
||||
|
||||
Пробный период использует настройки `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`, `TRIAL_TRAFFIC_STRATEGY` и `TRIAL_SQUAD_UUIDS`. Он не выбирает тариф из JSON-каталога, но его можно настроить на странице **Система → Тарифы** рядом с каталогом продаж. Если `TRIAL_SQUAD_UUIDS` пустой, для trial применяются squads из `USER_SQUAD_UUIDS`.
|
||||
|
||||
Промокоды с бонусными днями применяются к покупке period-подписки. Реферальные бонусы по периодам также относятся к подпискам на срок; в режиме продажи трафика без JSON-каталога Web App не показывает детализацию бонусов по месяцам.
|
||||
|
||||
## Привязка существующих подписок
|
||||
|
||||
При запуске с активным JSON-каталогом бот заполняет активные подписки без `tariff_key`:
|
||||
|
||||
- `tariff_key` получает `default_tariff`;
|
||||
- `tier_baseline_bytes` берется из текущего лимита подписки или из `monthly_gb` тарифа по умолчанию;
|
||||
- `topup_balance_bytes` становится `0`, если значение отсутствовало;
|
||||
- `period_start_at` очищается;
|
||||
- `effective_monthly_price_rub` берется из последнего успешного платежа или из цены тарифа по умолчанию.
|
||||
|
||||
Это позволяет существующим активным подпискам отображаться и управляться в интерфейсах тарифов.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Web App / Mini App
|
||||
|
||||
Web App собирается в отдельный `frontend` image и отдается через nginx. Static/Mini App запросы идут в `frontend:80`; frontend nginx проксирует `/api/*`, `/auth/*` и theme/logo assets в backend WebApp server на `backend:8081`. Telegram, payment и panel webhook routes остаются на backend webhook server `backend:8080`.
|
||||
|
||||
## Что показывает Web App
|
||||
|
||||
- текущую ссылку подключения;
|
||||
- статус и дату окончания подписки;
|
||||
- использованный и доступный трафик;
|
||||
- отдельную карточку premium-трафика, если у активного тарифа настроены premium-сквады и premium-лимит;
|
||||
- доступные тарифы, способы оплаты и платежный статус;
|
||||
- смену тарифа, обычную докупку трафика и докупку premium-трафика при настроенном каталоге тарифов;
|
||||
- встроенную инструкцию установки: подбор платформы, список приложений, deeplink-кнопки, QR и действия со ссылкой подписки;
|
||||
- раздел "Мои устройства" при `MY_DEVICES_SECTION_ENABLED=True`;
|
||||
- раздел "Поддержка" с тикетами и внешней ссылкой `SUPPORT_LINK` при включенном `SUPPORT_TICKETS_ENABLED`;
|
||||
- реферальную ссылку и статистику приглашений;
|
||||
- привязку email и Telegram к одному аккаунту.
|
||||
|
||||
Для администраторов из `ADMIN_IDS` Web App также показывает админ-панель: статистику, **пользователей** (поиск, фильтры, premium-трафик), поддержку, рассылки, промокоды, логи, настройки и редактор тарифов. Подробности: [админ-панель](admin-panel.md).
|
||||
|
||||
## Настройки `.env`
|
||||
|
||||
```env
|
||||
WEBAPP_ENABLED=True
|
||||
WEBAPP_SERVER_HOST=0.0.0.0
|
||||
WEBAPP_SERVER_PORT=8081
|
||||
SUBSCRIPTION_MINI_APP_URL=https://app.domain.com/
|
||||
SUBSCRIPTION_GUIDES_ENABLED=True
|
||||
SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED=True
|
||||
SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED=True
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=False
|
||||
WEBAPP_TITLE="Моя подписка"
|
||||
WEBAPP_THEMES_DIR=data/themes
|
||||
WEBAPP_DEFAULT_THEME=
|
||||
WEBAPP_SESSION_SECRET=<stable-random-secret>
|
||||
WEBHOOK_SECRET_TOKEN=<stable-random-secret>
|
||||
WEBAPP_SESSION_TTL_SECONDS=86400
|
||||
WEBAPP_AUTH_MAX_AGE_SECONDS=86400
|
||||
WEBAPP_LOGIN_TOKEN_TTL_SECONDS=600
|
||||
|
||||
TELEGRAM_OAUTH_CLIENT_ID=<client-id-from-botfather>
|
||||
TELEGRAM_OAUTH_CLIENT_SECRET=<client-secret-from-botfather>
|
||||
TELEGRAM_OAUTH_REQUEST_ACCESS=write
|
||||
|
||||
SMTP_HOST=smtp-relay.brevo.com
|
||||
SMTP_PORT=587
|
||||
SMTP_FALLBACK_PORTS=2525,465
|
||||
SMTP_STARTTLS=True
|
||||
SMTP_USE_SSL=False
|
||||
SMTP_USERNAME=<smtp-login>
|
||||
SMTP_PASSWORD=<smtp-password-or-key>
|
||||
SMTP_FROM_EMAIL=no-reply@domain.com
|
||||
SMTP_FROM_NAME=Remnawave Minishop
|
||||
|
||||
SUPPORT_LINK=https://t.me/your_support_link
|
||||
SUPPORT_TICKETS_ENABLED=True
|
||||
SUPPORT_TICKET_RATE_LIMIT_PER_HOUR=5
|
||||
```
|
||||
|
||||
`SUBSCRIPTION_MINI_APP_URL` - это публичный HTTPS URL именно frontend/Mini App, обычно отдельный домен вроде `https://app.domain.com/`. Его указывают в BotFather в Mini Apps, а бот использует его для кнопок личного кабинета, referral-ссылок и email-входа. Не добавляйте в него `/api`, `/webhook` или путь конкретной страницы.
|
||||
|
||||
## Инструкции установки
|
||||
|
||||
Если `SUBSCRIPTION_GUIDES_ENABLED=True`, кнопка **Установить и настроить** в личном кабинете открывает внутренний экран `/install`. Если инструкции выключены, конфиг не загрузился или не прошел валидацию, сохраняется старое поведение: кнопка открывает финальную ссылку подключения из панели.
|
||||
|
||||
Экран `/install` доступен только авторизованному пользователю Web App. Он получает данные из `/api/subscription-guides`, определяет платформу по Telegram Mini Apps platform, `navigator.userAgentData.platform` и `navigator.userAgent`, а затем показывает приложения и шаги из Remnawave Subscription Page v1 config. Ссылки типа `happ://...` и другие deeplink-кнопки открываются прямо из Mini App; в шаблонах заменяются `{{SUBSCRIPTION_LINK}}`, `{{USERNAME}}`, `{{HAPP_CRYPT3_LINK}}` и `{{HAPP_CRYPT4_LINK}}`.
|
||||
|
||||
Конфиг инструкций загружается в таком порядке:
|
||||
|
||||
1. JSON из админки, только если включен `SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED`.
|
||||
2. Subscription Page config из Remnawave Panel, если включен `SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED`.
|
||||
3. Локальный файл `SUBSCRIPTION_PAGE_CONFIG_PATH` как fallback.
|
||||
|
||||
По умолчанию используются инструкции из Remnawave Panel, чтобы не дублировать настройку страницы подписки в Minishop. Локальный файл в `data/subpage-config/multiapp.json` не создается автоматически. Конфиг кешируется на backend и обновляется при изменении связанных настроек; ошибки загрузки кешируются кратко, чтобы не дергать панель на каждый пользовательский запрос.
|
||||
|
||||
Личный экран показывает QR-код финальной ссылки подписки, кнопку копирования и кнопку **Поделиться**. Для передачи инструкции генерируется публичная ссылка `/s/<token>`: она открывает тот же интерфейс инструкций без авторизации и нижней навигации, но без QR-блока. Публичный payload отдается через `/api/subscription-guides/public/{share_token}` только для активной локальной подписки с валидным share token.
|
||||
|
||||
`SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED=True` включает такое же поведение в Telegram-боте: кнопки подключения открывают Mini App `/install`, а после успешной оплаты, trial или промокода пользователь получает публичную ссылку `/s/<token>`. Если настройку выключить, бот снова отправляет пользователя на финальную Remnawave Subscription Page.
|
||||
|
||||
Конфиг совместим с Remnawave Subscription Page v1 (`version`, `locales`, `brandingSettings`, `uiConfig`, `baseSettings`, `baseTranslations`, `svgLibrary`, `platforms`). Backend проверяет обязательные locale-строки, допустимые платформы и типы кнопок, ссылки на `svgIconKey`, а SVG из `svgLibrary` санитизирует перед отдачей в UI.
|
||||
|
||||
Если `WEBAPP_ENABLED=False`, пользовательский Web App и админ-панель не регистрируются. Чтобы снова попасть в админку, включите `WEBAPP_ENABLED=True` в `.env` и перезапустите backend/frontend контейнеры.
|
||||
|
||||
Внешний вид настраивается в админке: раздел **Внешний вид** управляет логотипом, emoji-логотипом, accent-цветом, выбранной темой и масштабом логотипа. Кастомные темы читаются из `WEBAPP_THEMES_DIR`, а `WEBAPP_DEFAULT_THEME` может принудительно выбрать тему по ключу. Подробный контракт `theme.json`, CSS/asset-роуты и пайплайн создания темы описаны в [webapp-themes.md](webapp-themes.md).
|
||||
|
||||
Если SMTP-настройки не заполнены, вход по email скрывается.
|
||||
|
||||
Тикеты поддержки включаются через `SUPPORT_TICKETS_ENABLED`; внешний резервный контакт задается `SUPPORT_LINK`. Полный сценарий пользователя, админа и уведомлений описан в [support.md](support.md).
|
||||
|
||||
## Telegram-авторизация
|
||||
|
||||
Внутри Telegram Mini App пользователь авторизуется через Telegram Mini Apps `initData`. При открытии страницы вне Telegram используется Telegram OAuth / OpenID Connect Authorization Code Flow с PKCE, callback `/auth/telegram/callback`, `nonce` и серверной проверкой `id_token` по JWKS Telegram.
|
||||
|
||||
Настройка в BotFather:
|
||||
|
||||
1. Откройте `@BotFather` -> `/mybots` -> выберите бота.
|
||||
2. В `Bot Settings` -> `Domain` укажите домен Web App без протокола и пути, например `app.domain.com`.
|
||||
3. В `Bot Settings` -> `Mini Apps` укажите URL, например `https://app.domain.com/`.
|
||||
4. В `Bot Settings` -> `Web Login` включите OpenID Connect Login, если BotFather предлагает переключение.
|
||||
5. Скопируйте Client ID и Client Secret в `TELEGRAM_OAUTH_CLIENT_ID` и `TELEGRAM_OAUTH_CLIENT_SECRET`.
|
||||
6. В `Web Login` -> `Allowed URLs` добавьте:
|
||||
|
||||
```text
|
||||
https://app.domain.com/
|
||||
https://app.domain.com/auth/telegram/callback
|
||||
```
|
||||
|
||||
`TELEGRAM_OAUTH_REQUEST_ACCESS=write` разрешает боту написать пользователю после логина. Если дополнительные разрешения не нужны, оставьте переменную пустой.
|
||||
|
||||
## Email-вход
|
||||
|
||||
Email-вход работает через одноразовый код:
|
||||
|
||||
1. Пользователь вводит email.
|
||||
2. Бот отправляет код через SMTP.
|
||||
3. Код вводится в модальном окне Web App.
|
||||
4. После подтверждения создается или находится пользователь, а email можно связать с Telegram-аккаунтом.
|
||||
|
||||
Для Brevo обычно подходит порт `587` с STARTTLS. Если основной порт недоступен, приложение пробует порты из `SMTP_FALLBACK_PORTS`; порт `465` используется через SSL.
|
||||
|
||||
Полный список переменных, обязательные поля для включения email-входа и типичные ошибки подключения описаны в разделе **SMTP и вход по email** в [configuration.md](../configuration.md).
|
||||
|
||||
## Проксирование
|
||||
|
||||
Рекомендуемая production-схема - два публичных домена:
|
||||
|
||||
- `WEBHOOK_BASE_URL`, например `https://webhooks.domain.com`, целиком проксируется в `backend:8080`;
|
||||
- `SUBSCRIPTION_MINI_APP_URL`, например `https://app.domain.com/`, целиком проксируется в `frontend:80`.
|
||||
|
||||
`frontend` уже сам проксирует `/api/*`, `/auth/*`, `/webapp-logo` и ассеты тем/логотипов во внутренний
|
||||
WebApp API на `backend:8081`, поэтому внешний reverse proxy обычно не должен отправлять эти пути в
|
||||
`backend:8081` напрямую.
|
||||
|
||||
Готовые варианты описаны в [Deploy examples](../deploy-examples/index.md):
|
||||
|
||||
- [Caddy](../deploy-examples/caddy.md) - автоматический HTTPS;
|
||||
- [Nginx](../deploy-examples/nginx.md) - сертификаты в соседней папке `ssl/`;
|
||||
- [Pangolin/Newt](../deploy-examples/newt.md) - публикация без входящих портов на сервере приложения;
|
||||
- [No proxy](../deploy-examples/no-proxy.md) - прямая публикация портов для проверки или внешней TLS-платформы.
|
||||
|
||||
В default `docker-compose.yml` наружу публикуются `frontend` и webhook/backend port, а внутри Docker
|
||||
network сервисы доступны друг другу по service DNS names:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frontend:
|
||||
expose:
|
||||
- "80"
|
||||
backend:
|
||||
expose:
|
||||
- "8080"
|
||||
```
|
||||
|
||||
## Реферальные ссылки
|
||||
|
||||
Реферальные ссылки доступны в двух форматах:
|
||||
|
||||
- Telegram deep-link: `https://t.me/<bot>?start=ref_u<code>`;
|
||||
- Web App ссылка: `https://app.domain.com/?ref=u<code>`.
|
||||
|
||||
Web App учитывает `ref`, `start`, `start_param` и Telegram Mini Apps `start_param`, сохраняет найденный параметр до авторизации и передает его в Telegram OAuth или email-вход.
|
||||
|
||||
Для email-регистраций пользователь в Remnawave создается с username вида `em_<referral_code>`. Email добавляется в описание пользователя панели и, если API панели принимает поле `email`, передается отдельным полем. Для Telegram-регистраций используется username `tg_<telegram_id>`.
|
||||
@@ -0,0 +1,369 @@
|
||||

|
||||
|
||||
# Темы и внешний вид Web App
|
||||
|
||||
Web App поддерживает файловые темы, предпросмотр и базовую настройку внешнего вида из админ-панели. Тема может быть простой цветовой схемой на JSON-токенах или полноценным скином с собственным CSS, шрифтами, иконками и графикой.
|
||||
|
||||
## Что можно поменять
|
||||
|
||||
Через раздел **Админка -> Внешний вид** можно:
|
||||
|
||||
- выбрать глобальную тему Web App;
|
||||
- изменить accent-цвет конкретной темы;
|
||||
- включить или выключить применение темы в админ-панели;
|
||||
- настроить масштаб логотипа на главной и экране входа;
|
||||
- загрузить логотип файлом или по HTTPS-ссылке;
|
||||
- включить emoji-логотип и выбрать способ его отрисовки;
|
||||
- открыть предпросмотр темы через `/home?theme_preview=<key>`.
|
||||
|
||||
Через файлы темы можно менять намного больше:
|
||||
|
||||
- базовые цвета Mini App и админки;
|
||||
- радиусы, семейства шрифтов и размер главного логотипа;
|
||||
- любые компоненты через CSS: карточки, навигацию, таблицы, модалки, кнопки, скелетоны, прогресс-бары, состояния hover/active и мобильную/desktop-верстку;
|
||||
- экран инструкций установки (`/install` и `/s/<token>`): topbar, выбор платформы, карточки приложений, шаги инструкции и QR-блок личной страницы;
|
||||
- иконки и изображения, если CSS ссылается на ассеты темы;
|
||||
- стили только пользовательской части, только админки или обеих частей сразу.
|
||||
|
||||
Готовые темы лежат в `backend/bot/app/web/themes`: `dark`, `light`, `windows95`, `ascii`. При первом запуске они копируются в `WEBAPP_THEMES_DIR`, по умолчанию `data/themes`.
|
||||
|
||||
## Где живут темы
|
||||
|
||||
Каждая тема - отдельная папка:
|
||||
|
||||
```text
|
||||
data/themes/
|
||||
neon/
|
||||
theme.json
|
||||
style.css
|
||||
icons/
|
||||
save.png
|
||||
```
|
||||
|
||||
Путь настраивается переменной:
|
||||
|
||||
```env
|
||||
WEBAPP_THEMES_DIR=data/themes
|
||||
WEBAPP_DEFAULT_THEME=
|
||||
```
|
||||
|
||||
`WEBAPP_DEFAULT_THEME` опционален. Если он задан и совпадает с ключом темы, он переопределяет `default: true` в `theme.json`. Если переменная пустая, дефолт выбирается из дескрипторов тем.
|
||||
|
||||
Важно: `WEBAPP_PRIMARY_COLOR`, `WEBAPP_LOGO_URL`, `WEBAPP_LOGO_USE_EMOJI`, `WEBAPP_LOGO_EMOJI` и `WEBAPP_LOGO_EMOJI_FONT` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent.
|
||||
|
||||
## Контракт `theme.json`
|
||||
|
||||
Минимальная тема:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "neon",
|
||||
"names": {
|
||||
"ru": "Неон",
|
||||
"en": "Neon"
|
||||
},
|
||||
"enabled": true,
|
||||
"default": true,
|
||||
"use_primary_accent": true,
|
||||
"use_in_admin": true,
|
||||
"tokens": {
|
||||
"color_scheme": "dark",
|
||||
"bg": "#05040a",
|
||||
"panel": "#11101c",
|
||||
"text": "#f8f7ff",
|
||||
"muted": "#b8b2d8",
|
||||
"accent": "#a855f7",
|
||||
"radius": "14px"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Тема с CSS:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "neon",
|
||||
"names": {
|
||||
"ru": "Неон",
|
||||
"en": "Neon"
|
||||
},
|
||||
"enabled": true,
|
||||
"default": false,
|
||||
"use_primary_accent": false,
|
||||
"use_in_admin": true,
|
||||
"css_file": "style.css",
|
||||
"assets_version": 1,
|
||||
"tokens": {
|
||||
"color_scheme": "dark",
|
||||
"style_preset": "none",
|
||||
"accent": "#a855f7",
|
||||
"bg": "#05040a",
|
||||
"panel": "#11101c",
|
||||
"panel_2": "#090815",
|
||||
"panel_3": "#1a1830",
|
||||
"border": "rgba(168, 85, 247, 0.28)",
|
||||
"border_strong": "rgba(168, 85, 247, 0.48)",
|
||||
"text": "#f8f7ff",
|
||||
"muted": "#b8b2d8",
|
||||
"dim": "#756f9b",
|
||||
"danger": "#ff6b8a",
|
||||
"blue": "#38bdf8",
|
||||
"radius": "14px",
|
||||
"font_sans": "Inter, system-ui, sans-serif",
|
||||
"font_logo": "Inter, system-ui, sans-serif",
|
||||
"font_mono": "\"JetBrains Mono\", \"Fira Code\", monospace",
|
||||
"home_logo_scale": 120,
|
||||
"admin_bg": "#05040a",
|
||||
"admin_surface": "#11101c",
|
||||
"admin_surface_2": "#090815",
|
||||
"admin_elev": "#1a1830",
|
||||
"admin_border": "rgba(168, 85, 247, 0.28)",
|
||||
"admin_border_strong": "rgba(168, 85, 247, 0.48)",
|
||||
"admin_text": "#f8f7ff",
|
||||
"admin_muted": "#b8b2d8",
|
||||
"admin_dim": "#756f9b"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля верхнего уровня:
|
||||
|
||||
| Поле | Назначение |
|
||||
| --- | --- |
|
||||
| `key` | Уникальный ключ темы, 1-64 символа: латиница, цифры, `_` и `-`. Если ключ не указан, берется имя папки. |
|
||||
| `names` | Локализованные названия, например `ru` и `en`. |
|
||||
| `enabled` | Показывать тему пользователям. Отключенная тема не попадает в публичный каталог. |
|
||||
| `default` | Делает тему выбранной по умолчанию, если `WEBAPP_DEFAULT_THEME` не задан. |
|
||||
| `use_primary_accent` | Если `true`, тема может получить accent из настройки внешнего вида, когда в `tokens.accent` ничего нет. |
|
||||
| `use_in_admin` | Если `false`, пользовательская часть использует тему, но админка откатывается на `dark`. |
|
||||
| `css_file` | CSS-файл внутри папки темы. Может быть `style.css` или вложенный путь вроде `css/theme.css`. |
|
||||
| `assets_version` | Версия ассетов. Для встроенных тем используется для обновления старых файлов в `data/themes`. |
|
||||
| `tokens` | Дизайн-токены, которые превращаются в CSS-переменные на `.app-shell`. |
|
||||
|
||||
## Токены
|
||||
|
||||
Поддерживаемые токены:
|
||||
|
||||
| Токен | CSS-переменная | Что меняет |
|
||||
| --- | --- | --- |
|
||||
| `color_scheme` | `color-scheme` | Нативная светлая/темная схема браузера: `dark` или `light`. |
|
||||
| `style_preset` | CSS-класс пресета | Сейчас `win95`/`windows95` добавляет `theme-preset-win95`; остальные значения не дают специального класса. |
|
||||
| `accent` | `--accent` | Главный акцент: активные элементы, кнопки, прогресс, фокус. Только hex `#RGB` или `#RRGGBB`. |
|
||||
| `bg` | `--bg` | Основной фон приложения. |
|
||||
| `panel` | `--panel` | Основные карточки и поверхности. |
|
||||
| `panel_2` | `--panel-2` | Вторичные поверхности. |
|
||||
| `panel_3` | `--panel-3` | Поверхности повышенной вложенности, dropdown/popover. |
|
||||
| `border` | `--border` | Обычные границы. |
|
||||
| `border_strong` | `--border-strong` | Усиленные границы и hover-состояния. |
|
||||
| `text` | `--text` | Основной текст. |
|
||||
| `muted` | `--muted` | Вторичный текст. |
|
||||
| `dim` | `--dim` | Еще более тихий текст и служебные подписи. |
|
||||
| `danger` | `--danger` | Ошибки и опасные действия. |
|
||||
| `blue` | `--blue` | Синий вспомогательный цвет. |
|
||||
| `radius` | `--radius` | Базовый радиус карточек, кнопок и контролов. |
|
||||
| `font_sans` | `--font-sans` | Основной шрифт интерфейса. |
|
||||
| `font_logo` | `--font-logo` | Шрифт бренда и заголовка. |
|
||||
| `font_mono` | `--font-mono` | Моноширинный шрифт. |
|
||||
| `home_logo_scale` | `--home-logo-scale` | Масштаб логотипа на главной и входе, от `50` до `300` процентов. |
|
||||
| `admin_bg` | `--admin-bg` | Фон админ-панели. |
|
||||
| `admin_surface` | `--admin-surface` | Основные карточки админки. |
|
||||
| `admin_surface_2` | `--admin-surface-2` | Вторичные поверхности админки. |
|
||||
| `admin_elev` | `--admin-elev` | Elevated-поверхности админки. |
|
||||
| `admin_border` | `--admin-border` | Границы админки. |
|
||||
| `admin_border_strong` | `--admin-border-strong` | Усиленные границы админки. |
|
||||
| `admin_text` | `--admin-text` | Основной текст админки. |
|
||||
| `admin_muted` | `--admin-muted` | Вторичный текст админки. |
|
||||
| `admin_dim` | `--admin-dim` | Тихие подписи админки. |
|
||||
|
||||
Если `css_file` не задан, интерфейс полностью строится на токенах и общих стилях. Если `css_file` задан, токены все равно применяются первыми, а CSS темы может уточнить или полностью переопределить внешний вид.
|
||||
|
||||
## CSS-слой темы
|
||||
|
||||
CSS темы подключается как:
|
||||
|
||||
```text
|
||||
/webapp-theme-css/<key>/<css_file>
|
||||
```
|
||||
|
||||
Например `data/themes/neon/style.css` будет доступен как `/webapp-theme-css/neon/style.css`.
|
||||
|
||||
Корневой контейнер получает классы:
|
||||
|
||||
```text
|
||||
app-shell theme-dark theme-key-neon theme-css-style
|
||||
```
|
||||
|
||||
Для светлой схемы будет `theme-light`. Класс `theme-key-<key>` - основной якорь для CSS темы. Всегда начинайте селекторы с него, чтобы тема не задевала другие режимы:
|
||||
|
||||
```css
|
||||
.theme-key-neon.app-shell {
|
||||
--surface-sheen: rgba(168, 85, 247, 0.12);
|
||||
--shadow-soft: 0 18px 48px rgba(12, 5, 30, 0.44);
|
||||
}
|
||||
|
||||
.theme-key-neon .card {
|
||||
border-color: color-mix(in srgb, var(--accent) 34%, var(--border));
|
||||
background:
|
||||
linear-gradient(135deg, rgba(168, 85, 247, 0.14), rgba(56, 189, 248, 0.05)),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.theme-key-neon .bottom-nav button.active,
|
||||
.theme-key-neon .admin-nav-item.active {
|
||||
box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||
}
|
||||
```
|
||||
|
||||
CSS можно писать для пользовательской части и админки одновременно:
|
||||
|
||||
```css
|
||||
.theme-key-neon .period-card,
|
||||
.theme-key-neon .method-card,
|
||||
.theme-key-neon .option-row {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.theme-key-neon .admin-card,
|
||||
.theme-key-neon .admin-stat-card,
|
||||
.theme-key-neon .admin-revenue-panel {
|
||||
border-radius: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
Ограничения:
|
||||
|
||||
- CSS-файл должен быть внутри папки темы;
|
||||
- размер CSS - до 512 KiB;
|
||||
- путь не может содержать `..`;
|
||||
- удаленные CSS, `data:` и protocol-relative URL в `css_file` не подключаются.
|
||||
|
||||
## Ассеты темы
|
||||
|
||||
Картинки темы кладутся рядом с `theme.json` и отдаются через:
|
||||
|
||||
```text
|
||||
/webapp-theme-assets/<key>/<path>
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```css
|
||||
.theme-key-neon .btn-primary::before {
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url("/webapp-theme-assets/neon/icons/spark.png") center / contain no-repeat;
|
||||
}
|
||||
```
|
||||
|
||||
Разрешены `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `ico`. Один asset - до 1 MiB. Для шрифтов лучше использовать внешние источники, уже разрешенные CSP (`fonts.googleapis.com`, `fonts.gstatic.com`, `cdn.jsdelivr.net`) или системные fallback-цепочки в `font_*` токенах.
|
||||
|
||||
## Пайплайн создания новой темы
|
||||
|
||||
1. Выберите ключ темы.
|
||||
|
||||
Ключ должен быть стабильным: по нему сохраняется выбранная тема и строятся URL ассетов. Используйте короткий slug: `neon`, `brand_dark`, `terminal-blue`. Не переименовывайте ключ после публикации без миграции файлов и сохраненных настроек.
|
||||
|
||||
2. Создайте папку в `WEBAPP_THEMES_DIR`.
|
||||
|
||||
В Docker по умолчанию это `data/themes`. Если включен bind mount `./data:/app/data`, убедитесь, что контейнер может писать в `data`.
|
||||
|
||||
```bash
|
||||
mkdir -p data/themes/neon
|
||||
```
|
||||
|
||||
3. Скопируйте ближайшую базовую тему.
|
||||
|
||||
Для обычной брендовой темы чаще всего удобнее начать с `dark` или `light`. Для глубокого CSS-скина можно взять `ascii` или `windows95` как пример того, насколько далеко можно уйти от стандартного вида.
|
||||
|
||||
```bash
|
||||
cp backend/bot/app/web/themes/dark/theme.json data/themes/neon/theme.json
|
||||
```
|
||||
|
||||
4. Отредактируйте `theme.json`.
|
||||
|
||||
Сначала поменяйте `key`, `names`, `default`, `use_primary_accent` и базовые токены. На этом этапе можно вообще не создавать CSS: приложение уже увидит тему как новый набор токенов.
|
||||
|
||||
5. Запустите приложение и откройте админку.
|
||||
|
||||
Раздел **Внешний вид** загружает `/api/admin/themes`, backend читает `WEBAPP_THEMES_DIR`, добавляет обязательные базовые темы и возвращает каталог. Нажмите **Обновить**, если папка была создана во время работы приложения.
|
||||
|
||||
6. Проверьте тему через предпросмотр.
|
||||
|
||||
В карточке темы нажмите **Предпросмотр** или откройте:
|
||||
|
||||
```text
|
||||
https://app.domain.com/home?theme_preview=neon
|
||||
```
|
||||
|
||||
Предпросмотр не меняет глобальную тему и удобен для проверки CSS до публикации.
|
||||
|
||||
7. Подберите accent и масштаб логотипа.
|
||||
|
||||
В админке можно менять accent и `home_logo_scale` без ручного редактирования JSON. При сохранении backend перепишет `theme.json` в `WEBAPP_THEMES_DIR`, выставит ровно один `default` и сбросит кеш публичных настроек.
|
||||
|
||||
8. Добавьте `style.css`, если токенов мало.
|
||||
|
||||
Создайте файл, укажите его в `theme.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"css_file": "style.css"
|
||||
}
|
||||
```
|
||||
|
||||
Начинайте с переопределения CSS-переменных на `.theme-key-neon.app-shell`, затем переходите к конкретным компонентам. Проверяйте минимум: главная, `/install`, публичная `/s/<token>`, оплата, настройки, модалки, админский дашборд, таблица пользователей, редактор тарифов.
|
||||
|
||||
9. Добавьте ассеты при необходимости.
|
||||
|
||||
Положите картинки в подпапку темы и ссылайтесь на них через `/webapp-theme-assets/<key>/...`. Не используйте относительные пути вроде `url("icons/x.png")`, если CSS может быть подключен с другого URL-уровня; явный `/webapp-theme-assets/neon/icons/x.png` надежнее.
|
||||
|
||||
10. Настройте поведение админки.
|
||||
|
||||
Если тема сильно декоративная и мешает рабочей админке, выставьте `use_in_admin: false`. Пользователи увидят тему, а администраторы в разделе админки получат `dark` как fallback.
|
||||
|
||||
11. Сделайте тему дефолтной.
|
||||
|
||||
Есть два способа:
|
||||
|
||||
- в админке выбрать тему и сохранить;
|
||||
- указать `WEBAPP_DEFAULT_THEME=neon` в `.env`, если нужен жесткий override на уровне окружения.
|
||||
|
||||
12. Зафиксируйте тему.
|
||||
|
||||
Для темы, которая должна ехать вместе с проектом, добавьте ее в репозиторий в `backend/bot/app/web/themes` и при необходимости расширьте `DEFAULT_THEME_KEYS` в `backend/config/webapp_themes_config.py`. Для приватной инсталляции достаточно хранить ее в `data/themes`.
|
||||
|
||||
## Насколько глубоко можно менять вид
|
||||
|
||||
Уровни кастомизации:
|
||||
|
||||
1. **Быстрый бренд** - токены `accent`, `bg`, `panel`, `text`, `radius`, логотип в админке. Код не нужен.
|
||||
2. **Полная палитра** - все пользовательские и admin-токены, отдельные шрифты, масштаб логотипа.
|
||||
3. **CSS-скин** - переопределение карточек, навигации, таблиц, модалок, progress/skeleton/toast, desktop/mobile раскладок.
|
||||
4. **Почти новый UI** - тема вроде `windows95` или `ascii`: можно менять форму контролов, иконки, эффекты, таблицы и визуальный язык целиком, пока сохраняется DOM и интерактивные состояния.
|
||||
|
||||
Не стоит менять через CSS смысловые состояния: скрывать ошибки, отключать фокус, перекрывать кнопки невидимыми слоями или делать `display: none` для обязательных действий оплаты и авторизации. Тема должна менять внешний вид, а не бизнес-логику.
|
||||
|
||||
## Диагностика
|
||||
|
||||
Если тема не появилась:
|
||||
|
||||
- проверьте, что `theme.json` лежит ровно в `WEBAPP_THEMES_DIR/<key>/theme.json`;
|
||||
- ключ состоит только из латиницы, цифр, `_` и `-`;
|
||||
- JSON валиден;
|
||||
- тема не отключена через `enabled: false`;
|
||||
- в логах нет предупреждения `Ignoring theme descriptor`.
|
||||
|
||||
Если CSS не применился:
|
||||
|
||||
- проверьте `css_file` и URL `/webapp-theme-css/<key>/<css_file>`;
|
||||
- убедитесь, что файл меньше 512 KiB;
|
||||
- начинайте селекторы с `.theme-key-<key>`;
|
||||
- откройте `/home?theme_preview=<key>` в новом окне, чтобы исключить сохраненный старый выбор.
|
||||
|
||||
Если ассеты не грузятся:
|
||||
|
||||
- используйте путь `/webapp-theme-assets/<key>/<path>`;
|
||||
- проверьте расширение: `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `ico`;
|
||||
- размер каждого файла должен быть до 1 MiB;
|
||||
- путь не должен содержать пробелы, кириллицу или `..`.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Reference in New Issue
Block a user