Audit P1: 5 of 7 layout-property animations were transition-[width]
or transition-[left] — these trigger reflow + repaint every frame.
Convert to transform-based equivalents that run on the compositor.
Subscription.tsx — 3 progress bars + 1 toggle:
• Device usage bar (track w-16) — width % → scaleX with a 0.0625
floor (= 4px on 64px track) preserving the minWidth: 4px guard
when connectedDevices > 0.
• Traffic-purchase progress — absolute inset-0 + width % →
transform: scaleX with origin-left, gradient stretches with the
fill same as before.
• Subscription period progress — same conversion.
• Autopay toggle thumb — left: 3↔26px → translateX(0↔23px) with
fixed left: 3px. role='switch' + aria-checked unchanged.
globals.css — .progress-fill utility class:
• transition-property: width, background-color → transform,
background-color. Width set to w-full + origin-left so consumers
just set transform: scaleX(0..1). Currently has no .tsx consumers
(grep clean) but the API stays valid for future use.
Skipped (bounded one-shot, transform alternative has tradeoffs):
• Info.tsx + InfoPageView.tsx accordion height. Animating from 0
to content-driven scrollHeight needs layout one way or another —
grid-template-rows 0fr/1fr and max-height also layout-class
properties; transform: scaleY squishes text contents. Cost is
bounded: one animation per user click, not continuous, not on
critical path. Separate work if priority changes.
Visual parity: scaleX renders identical geometry to width % on the
inner fill. box-shadow on the device bar (0 0 8px 40)
scales proportionally — tiny visual difference at full width, none
visible at partial widths.
Bedolaga Cabinet - Web Interface
Веб-интерфейс личного кабинета для VPN бота на базе Remnawave Bedolaga Telegram Bot V3.0.0+.
React + Vite + TypeScript | Авторизация через Telegram | Мультиязычность (EN/RU) | Адаптивный дизайн
Требования
- Docker и Docker Compose
- Запущенный backend бота с включенным Cabinet API
- Обратный прокси (Caddy / Nginx / Traefik)
Архитектура
Браузер → Caddy/Nginx → /api/* → remnawave_bot:8080 (backend API)
→ /* → /srv/cabinet (статика frontend)
Frontend — это статические файлы (HTML, JS, CSS). Обратный прокси выполняет две задачи:
- Раздает статику frontend
- Проксирует
/api/*запросы на backend бота (с удалением префикса/api)
Установка
Шаг 1. Настройка backend бота
В .env файле бота добавьте:
# Включить Cabinet API
CABINET_ENABLED=true
# JWT секрет (сгенерируйте случайную строку: openssl rand -hex 32)
CABINET_JWT_SECRET=your_random_secret_key_here
# Разрешенные origins для CORS (домен, на котором будет кабинет)
CABINET_ALLOWED_ORIGINS=https://cabinet.example.com
Перезапустите бота после изменений.
Шаг 2. Получение frontend файлов
Вариант A: Готовый Docker образ (рекомендуется)
docker pull ghcr.io/bedolaga-dev/bedolaga-cabinet:latest
Извлеките собранные файлы из образа:
# Создать временный контейнер и скопировать статику
docker create --name tmp_cabinet ghcr.io/bedolaga-dev/bedolaga-cabinet:latest
mkdir -p ./cabinet-dist
docker cp tmp_cabinet:/usr/share/nginx/html/. ./cabinet-dist/
docker rm tmp_cabinet
Вариант B: Сборка из исходников
git clone https://github.com/BEDOLAGA-DEV/bedolaga-cabinet.git
cd bedolaga-cabinet
cp .env.example .env
Отредактируйте .env:
VITE_API_URL=/api
VITE_TELEGRAM_BOT_USERNAME=your_bot_username
VITE_APP_NAME=My VPN
VITE_APP_LOGO=V
Соберите и извлеките:
docker compose build cabinet-frontend
docker compose create cabinet-frontend
mkdir -p ./cabinet-dist
docker cp cabinet_frontend:/usr/share/nginx/html/. ./cabinet-dist/
docker compose rm -sf cabinet-frontend
Шаг 3. Размещение файлов на сервере
Скопируйте содержимое cabinet-dist в директорию, которую будет раздавать ваш прокси:
# Создайте директорию на сервере
sudo mkdir -p /srv/cabinet
# Скопируйте файлы (с локальной машины или напрямую на сервере)
sudo cp -r ./cabinet-dist/* /srv/cabinet/
Шаг 4. Настройка обратного прокси
Caddy (рекомендуется)
Caddy автоматически получает и обновляет SSL сертификаты.
https://cabinet.example.com {
encode gzip zstd
# API запросы → backend бота (удаляет /api префикс)
handle /api/* {
uri strip_prefix /api
reverse_proxy remnawave_bot:8080
}
# Frontend статика
handle {
root * /srv/cabinet
try_files {path} /index.html
file_server
# Кэширование статических ассетов (JS, CSS, шрифты, изображения)
@static path *.js *.css *.woff *.woff2 *.ttf *.ico *.png *.jpg *.jpeg *.svg *.webp *.gif
header @static Cache-Control "public, max-age=31536000, immutable"
# HTML без кэша (для обновлений SPA)
@html path *.html /
header @html Cache-Control "no-cache, must-revalidate"
}
}
Примечание:
remnawave_bot:8080— имя контейнера бота в Docker сети. Если Caddy запущен на хосте, а не в Docker, используйтеlocalhost:8080или IP сервера.
Nginx
server {
listen 443 ssl http2;
server_name cabinet.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
root /srv/cabinet;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
# API запросы → backend бота
location /api/ {
rewrite ^/api/(.*) /$1 break;
proxy_pass http://remnawave_bot:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Кэширование статических ассетов
location ~* \.(?:js|css|woff2?|ttf|ico|png|jpe?g|svg|webp|gif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# SPA fallback
location / {
try_files $uri /index.html;
add_header Cache-Control "no-cache, must-revalidate";
}
}
Шаг 5. Запуск и проверка
# Перезагрузите Caddy
docker exec <caddy_container> caddy reload --config /etc/caddy/Caddyfile
# Или Nginx
docker exec <nginx_container> nginx -s reload
Откройте https://cabinet.example.com в браузере.
Альтернативная установка: Docker контейнер с nginx внутри
Если вы не хотите раздавать статику напрямую, можно проксировать запросы на Docker контейнер cabinet.
docker-compose.yml
services:
cabinet-frontend:
image: ghcr.io/bedolaga-dev/bedolaga-cabinet:latest
container_name: cabinet_frontend
restart: unless-stopped
# Порты НЕ пробрасываем — доступ только через Docker сеть
networks:
- bot_network
networks:
bot_network:
external: true
name: remnawave-bedolaga-telegram-bot_bot_network # Сеть вашего бота
Caddyfile
https://cabinet.example.com {
encode gzip zstd
# API запросы → backend бота
handle /api/* {
uri strip_prefix /api
reverse_proxy remnawave_bot:8080
}
# Frontend → nginx контейнер (порт 80 внутри Docker сети)
handle {
reverse_proxy cabinet_frontend:80
}
}
В этом варианте кэшированием статики занимается nginx внутри контейнера.
Переменные окружения
Build-time (вшиваются в JS при сборке)
| Переменная | Описание | По умолчанию |
|---|---|---|
VITE_API_URL |
Путь к API (/api или полный URL) |
/api |
VITE_TELEGRAM_BOT_USERNAME |
Username Telegram бота (без @) | — |
VITE_APP_NAME |
Название в шапке и вкладке браузера | Cabinet |
VITE_APP_LOGO |
Текст логотипа (1-2 символа) | V |
Runtime (только для Docker контейнера)
| Переменная | Описание | По умолчанию |
|---|---|---|
CABINET_PORT |
Порт контейнера на хосте | 3020 |
Backend бота (.env бота)
| Переменная | Описание | По умолчанию |
|---|---|---|
CABINET_ENABLED |
Включить Cabinet API | false |
CABINET_JWT_SECRET |
Секретный ключ для JWT | BOT_TOKEN |
CABINET_ALLOWED_ORIGINS |
CORS origins (через запятую) | — |
CABINET_ACCESS_TOKEN_EXPIRE_MINUTES |
Время жизни access token | 15 |
CABINET_REFRESH_TOKEN_EXPIRE_DAYS |
Время жизни refresh token | 7 |
Устранение проблем
CORS ошибка в консоли браузера
Домен кабинета не добавлен в CABINET_ALLOWED_ORIGINS в .env бота. Добавьте и перезапустите бота.
API возвращает HTML вместо JSON
Прокси настроен неправильно — запросы /api/* попадают на frontend вместо backend. Проверьте порядок блоков handle в Caddyfile (API должен быть первым).
502 Bad Gateway
- Backend бота не запущен — проверьте
docker ps - Контейнеры в разных Docker сетях — проверьте и подключите:
# Проверить сети контейнера docker inspect <container> --format='{{json .NetworkSettings.Networks}}' | python3 -m json.tool # Подключить к нужной сети docker network connect <network_name> <container_name> - Неправильное имя сервиса в прокси — проверьте через:
docker exec <caddy_container> wget -qO- http://remnawave_bot:8080/health
Telegram авторизация не работает
VITE_TELEGRAM_BOT_USERNAMEдолжен быть без@- Домен кабинета добавлен в BotFather → Bot Settings → Domain
Белый экран / SPA не работает
Прокси не настроен на fallback к index.html. Убедитесь что try_files {path} /index.html (Caddy) или try_files $uri /index.html (Nginx) присутствует в конфигурации.
Разработка
git clone https://github.com/BEDOLAGA-DEV/bedolaga-cabinet.git
cd bedolaga-cabinet
npm install
cp .env.example .env
# Отредактируйте .env
npm run dev
Dev-сервер запустится на http://localhost:5173 с автоматическим проксированием /api на localhost:8080.
Структура проекта
bedolaga-cabinet/
├── src/
│ ├── api/ # API клиенты (axios)
│ ├── components/ # React компоненты (UI kit)
│ ├── contexts/ # React контексты (auth, theme)
│ ├── hooks/ # Custom hooks
│ ├── locales/ # Переводы (i18n)
│ ├── pages/ # Страницы приложения
│ ├── types/ # TypeScript типы
│ └── utils/ # Утилиты
├── public/ # Статические файлы
├── Dockerfile # Multi-stage сборка (node → nginx)
├── docker-compose.yml # Docker Compose для сборки
├── nginx.conf # Nginx конфиг внутри контейнера
└── .env.example # Пример переменных окружения
Связанные проекты
- Remnawave Bedolaga Telegram Bot — Backend бота
- Bedolaga Chat — Чат поддержки
Контакты
- Telegram: @fringg
- Telegram: @pedzeo
- Чат: Bedolaga Chat