diff --git a/.amnezia-panel-edition b/.amnezia-panel-edition deleted file mode 100644 index 1cfafac..0000000 --- a/.amnezia-panel-edition +++ /dev/null @@ -1 +0,0 @@ -community diff --git a/README.md b/README.md index 7b77566..fb363d6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Веб‑панель **AmneziaWG** на своём VPS (**Docker**): таблица клиентов, статусы «в туннеле», AllowedIPs, несколько инстансов через **`AWG_PROFILES`**, часы сервера и браузера, смена пароля панели. -**Редакция по умолчанию — FREE (`community`):** просмотр клиентов, **удаление** peer с сервера, раздел установки **Telegram MTProto‑прокси** (Docker). Полное управление (вкл/выкл, даты, переименование, экспорт `.conf`, каскад, Cloudflare WARP, синхронизация времени и т.д.) — в **[amnezia_web-PRO](https://github.com/andrey271192/amnezia_web-PRO)**; из FREE можно запустить переход на PRO по GitHub‑токену (см. жёлтый баннер после входа). +**Базовая открытая панель:** просмотр клиентов, **удаление** peer с сервера, раздел установки **Telegram MTProto‑прокси** (Docker), несколько инстансов через **`AWG_PROFILES`**, смена пароля. Вкл/выкл клиентов, даты, переименование, экспорт `.conf`, каскад, Cloudflare WARP и синхронизация времени хоста в этой версии **не поддерживаются**. --- @@ -24,15 +24,13 @@ ### Что умеет Amnezia Web -* **Управление через браузер** — таблица клиентов AmneziaWG, время сервера и браузера, быстрые действия там, где редакция это позволяет (FREE vs PRO). +* **Управление через браузер** — таблица клиентов AmneziaWG, время сервера и браузера, удаление peer, установка MTProto‑прокси. * **Быстрое развёртывание панели** — образ админки и опционально страницы для пользователей через **`scripts/install.sh`**. * **Приватность** — всё крутится **на вашем VPS**; доступ к панели ограничиваете паролем (и при желании сетью/TLS). * **Удобство** — одно место для обзора клиентов и (в FREE) установки **MTProto‑прокси** из интерфейса. * **Несколько инстансов AWG на одном хосте** — переключатель профилей через **`AWG_PROFILES`**. * **Современный стек** — Docker, Node.js, веб‑интерфейс. -Полный список возможностей FREE и PRO см. в тексте выше и в репозитории **[amnezia_web-PRO](https://github.com/andrey271192/amnezia_web-PRO)**. - --- ### Для кого этот проект @@ -59,7 +57,7 @@ ## Скриншоты -В редакции FREE сверху панели отображается жёлтый блок «базовая версия» и меньше кнопок, чем в PRO (см. список ограничений выше); общая разметка и таблица клиентов сохраняются. +Интерфейс ориентирован на просмотр клиентов, удаление peer и установку MTProto‑прокси; расширенные действия с клиентами в UI недоступны.
@@ -72,100 +70,20 @@
---
-## Установка для клиента (маркер в томе + явный ALLOW в контейнере)
-Раньше многие полагались на этот режим ради поля PAT — сейчас оно включено по умолчанию. **`INSTALL_FREE_COMMUNITY_ACTIVATION=1`** по-прежнему полезен: создаёт **`/opt/amnezia-admin-data/.install-free-github-pro-opt-in`** и при следующих **`curl … | sudo bash`** снова прокидывает **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=1`** в контейнер (если когда-то задали отключение вручную).
-
-```bash
-export INSTALL_FREE_COMMUNITY_ACTIVATION=1
-curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scripts/install.sh | sudo -E bash
-```
-если нужно сменить порт например на 8884
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scripts/install.sh \
- | sudo -E env HOST_PORT=8884 LANDING_PORT=8081 bash
-```
-
-Переменная **`INSTALL_FREE_COMMUNITY_ACTIVATION`** сохранится в томе как файл **`.install-free-github-pro-opt-in`**: следующие апдейты через **`sudo bash`** подхватят маркер **без** повторного `export` (или с **`sudo -E`**, если передаёте другие переменные в той же сессии).
-
----
-
-## Отключить поле GitHub и установку из панели (жёсткий FREE)
-
-Чтобы под баннером **не было** ввода токена и **запретить** **`POST …/run-private-install`**, задайте в контейнере **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=0`** (или **`false`** / **`no`** / **`off`**) и перезапустите панель. Обычная установка **`curl … | sudo bash`** без этой переменной **оставляет поле включённым** в FREE.
-
-Стандартная установка того же образа без отключения:
+## Установка
```bash
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scripts/install.sh | sudo bash
```
-Форк или ветка (**с тем же режимом маркера по желанию**):
+Смена порта (пример):
```bash
-export GITHUB_REPO="вы/репо"
-export BRANCH="main"
-export INSTALL_FREE_COMMUNITY_ACTIVATION=1
-curl -fsSL "https://raw.githubusercontent.com/${GITHUB_REPO}/${BRANCH}/scripts/install.sh" | sudo -E bash
+curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scripts/install.sh \
+ | sudo -E env HOST_PORT=8884 LANDING_PORT=8081 bash
```
-Уже скачали проект:
-
-```bash
-cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bash scripts/install.sh
-```
-
-### Переменные окружения и `sudo`
-
-Если передаёте **`AWG_PROFILES`** или **`ADMIN_PASSWORD`** в одной строке с `curl`, используйте **`sudo -E bash`**, иначе `sudo` не увидит переменные. Альтернатива — записать JSON профилей в **`/root/amnezia-admin.awg-profiles.json`** и запустить обычный `curl … | sudo bash`. Подробнее см. историю коммитов и зеркальный README в репозитории PRO.
-
-### Важные переменные
-
-| Переменная | По умолчанию | Назначение |
-|------------|--------------|------------|
-| `INSTALL_FREE_COMMUNITY_ACTIVATION` | _(не задано)_ | `1`: маркер **`.install-free-github-pro-opt-in`** в томе + явно **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=1`** при сборке **`RUN_ENV`** (удобно после ручного `ALLOW=0` или если нужно навсегда фиксировать в томе).
-| `GITHUB_REPO` | `andrey271192/amnezia_web` | Архив для установки |
-| `HOST_PORT` | `8080` | Порт панели |
-| `AWG_CONTAINER` | `amnezia-awg2` | Контейнер WG по умолчанию |
-| `AWG_PROFILES` | _(нет)_ | Несколько инстансов; см. примеры в репозитории PRO |
-| `COMMUNITY_UPGRADE_URL` | Страница подписки PRO на Boosty (зашита в код по умолчанию) | Переопределите, если смените уровень подписки |
-| `COMMUNITY_UPGRADE_PITCH` | _(текст по умолчанию в коде)_ | Текст под заголовком базовой версии |
-| `PANEL_FOOTER_DONATE_URL` | Как **`COMMUNITY_UPGRADE_URL`** | Ссылка «донат» в **шапке и подвале** веб‑панели (**не** добавляется на nginx‑лендинг пользователей). |
-| `PANEL_FOOTER_TELEGRAM_URL` | `https://t.me/lot_andrey` | Ссылка Telegram‑спонсора / канала (там же: только панель). |
-| `PANEL_FOOTER_OZON_URL` | Ссылка на **Ozon СБП** автора (дефолт в `server.js`) | Донат через Ozon — **шапка и подвал** панели с остальными ссылками. |
-| `PANEL_FOOTER_DONATE_LABEL` | `Поддержать проект` | Текст якоря доната |
-| `PANEL_FOOTER_OZON_LABEL` | `Донат · Ozon СБП` | Текст якоря Ozon |
-| `PANEL_FOOTER_TELEGRAM_LABEL` | `Telegram‑канал спонсора` | Текст якоря Telegram |
-| `PANEL_FOOTER_PROMO_SUBTITLE` | _(строка по умолчанию в коде)_ | Поясняющий текст в верхней полосе панели |
-| `SKIP_LANDING` | `0` | `1` — без лендинга на порту 80 |
-| `ALLOW_COMMUNITY_GITHUB_ACTIVATION` | _в редакции **`community` поле вкл по умолчанию_ | Явное **`0`** / **`false`** / **`no`** / **`off`** — **скрыть** ввод PAT и заблокировать **`POST /api/community/run-private-install`** (после перезапуска контейнера). **`1`** / **`true`** не обязательны. |
-| `COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL` | `https://raw.githubusercontent.com/andrey271192/amnezia_web-pro/main/scripts/install.sh` | Сырой URL `scripts/install.sh` в **вашем** приватном репозитории PRO |
-| `COMMUNITY_INSTALL_FETCH_MS` | `60000` | Таймаут HTTP при скачивании установщика из GitHub |
-| `PRIVATE_INSTALL_SCRIPT_MAX_BYTES` | `2097152` | Максимальный размер скачанного скрипта (байты) |
-| `COMMUNITY_INSTALL_LOG_TAIL_BYTES` | `98304` | Первый ответ **`GET /api/community/install-log`** без `since` отдаёт **хвост** последних N байт (для живого блока в UI при установке PRO). Диапазон 1 KiB … 512 KiB через env. |
-| `COMMUNITY_INSTALL_LOG_API_CHUNK_MAX` | `524288` | Максимум байт **за один** запрос «догонять» журнал (~512 KiB; верхний предел через env ограничен). |
-| `COMMUNITY_DISABLE_DOCKER_CLI_HELPER` | _(не задано)_ | Если `1`, установка из UI снова только «bash внутри панели» (часто конфликт **:8080** с PRO). По умолчанию используется одноразовый контейнер **docker:cli**, который **снимает FREE** и зависший PRO перед `install.sh`. |
-| `COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO` | `0` | `1` — не выполнять `docker rm` FREE/лендинга перед install (если знаете, что делаете). |
-| `COMMUNITY_PRO_INSTALL_HELPER_IMAGE` | `docker:26-cli` | Образ с бинарём `docker` для фонового контейнера (нужен доступ к демону по сокету). |
-| `COMMUNITY_HELPER_SKIP_PREPARE_TOOLS` | _(не задано)_ | `1` — helper не ставит **`bash`** и **`curl`** в образе Alpine (обычно **не нужно**; без них `#!/usr/bin/env bash` или `curl` в PRO `install.sh` падают). |
-| `MTPRO_PROXY_CONTAINER` | `mtproto-proxy` | Имя контейнера MTProto‑прокси Telegram (официальный образ **telegrammessenger/proxy**, см. Dockerfile на Docker Hub). |
-| `MTPRO_PROXY_IMAGE` | `telegrammessenger/proxy:latest` | Образ **`docker pull` + `docker run`** при установке из панели (FREE/community). |
-| `MTPRO_INTERNAL_PORT` | `443` | Порт процесса **внутри** контейнера (официальный прокси слушает 443/tcp). Мапится на **`MTPRO_PUBLISH_*`**. |
-| `MTPRO_PUBLISH_PORT` | `8443` | Порт хоста VPS (**внешний**), проброшенный в контейнер. |
-| `MTPRO_PUBLISH_BIND` | `0.0.0.0` | Адрес биндинга на хосте (`-p` в Docker); при нескольких сетевых интерфейсах при необходимости уточняют. |
-| `MTPRO_PUBLIC_HOST` | _(нет)_ | Публичный IP или DNS для ссылки **`tg://proxy`** в UI; можно вместо него задать **`CLIENT_CONFIG_ENDPOINT`**. |
-| `UI_HIDE_MTPROTO` | _(не задано)_ | `1` или **`UI_HIDE_SECTIONS=...,mtproto`** — скрыть раздел установки MTProto в веб‑интерфейсе (по умолчанию виден даже FREE). |
-| `FREE_PANEL_CONTAINER_FOR_PRO_INSTALL` и др. | см. `server.js` | Имена контейнеров для `docker rm` перед install (по умолчанию `amnezia-admin`, `amnezia-web-landing`, `amnezia-admin-pro`). |
-
-**MTProto‑прокси и API панели:** **`GET /api/mtproto/status`** отдаёт состояние контейнера и ссылку **`tg://`** без тяжёлых операций при открытии блока. Опционально **`GET /api/mtproto/status?withLogs=1`** добавляет в ответ хвост **`docker logs`** (поле **`logsFetched`**). Отдельные эндпоинты: **`GET /api/mtproto/logs`** и **`GET /api/mtproto/tail`** (тот же смысл, если обратный прокси режет URI с **`logs`**). За nginx/Caddy перед панелью нужно пробрасывать **весь** префикс **`/api/`** одним правилом — см. блок **«Not found»** в **«Частые проблемы»** ниже.
-
-Поле токена **по умолчанию уже есть**. Чтобы **убрать** его — см. раздел **«Отключить поле GitHub…»** выше. URL приватного `install.sh` задаёт **`COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL`**.
-
-**Важно:** при одноразовом запуске с переменными в одной строке с `curl … | sudo bash` они **пропадают** — нужен **`export`** перед `curl` и **`sudo -E bash`**, см. блок **«Установка для клиента»** выше.
-
-Остальные переменные совместимы с образом панели (см. Dockerfile / `server.js` в этом репозитории).
-
---
## Обновление
@@ -176,8 +94,6 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas
curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scripts/install.sh | sudo bash
```
-Если нужен маркер **`.install-free-github-pro-opt-in`** в томе (см. раздел выше) или переменные хоста при **`sudo`**, используйте **`export INSTALL_FREE_COMMUNITY_ACTIVATION=1`** и **`sudo -E bash`**.
-
После сообщения **`→ Клонирование релиза …`** несколько минут **может ничего не печататься**: идёт `curl … | tar` с GitHub. Для **полосы прогресса**: `sudo INSTALL_SCRIPT_VERBOSE=1 bash` (или передайте переменную в окружение до `|`). Если обрывает по времени или «тишина» часами задайте таймауты: **`CURL_CONNECT_TIMEOUT`** (по умолчанию 30 с к подключению), **`CURL_MAX_TIME`** (по умолчанию до 900 с на загрузку), **`CURL_RETRY`**. Полный URL tar.gz можно подменить: **`GITHUB_REPO_URL_OVERRIDE`** (должен указывать на архив того же вида **`${REPO}-${BRANCH}.tar.gz`**, после распаковки каталог будет **`ИмяРепозитория-${BRANCH}`**).
Принудительная пересборка образа: **`NO_CACHE=1`**.
@@ -194,23 +110,6 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scrip
## Частые проблемы
-### Пропало поле «токен GitHub» под плашкой FREE
-
-Редакция должна быть **`community`**. Чаще всего в **`docker inspect amnezia-admin` → Env** осталось **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=0`** (или **`false`** / **`no`** / **`off`**) — удалите переменную или поставьте **`1`** и снова выполните **`install.sh`**. Установщик при апдейте переносит **`ALLOW_*`** из **предыдущего** контейнера. Маркер **`${DATA_DIR}/.install-free-github-pro-opt-in`** при **`INSTALL_FREE_COMMUNITY_ACTIVATION=1`** снова прокинет **`ALLOW=1`**, если установщику на хосте не задали **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=0`**.
-
-### Журнал установки и docker-helper
-
-После успешной кнопки **«Установить PRO»** в той же панели открывается **живой блок «Журнал установки»** (поллинг каждые 2 с к **`GET /api/community/install-log`**; для запросов нужна сессия в браузере). Пока старый контейнер FREE снят и новый образ ещё не поднял ту же вкладку, поток журнала в браузере **прерывается** — откройте адрес панели снова после старта PRO; файл **`community-install-last.log`** на томе данных тот же. Кнопка **«Пауза»** останавливает опрос журнала.
-
-В режиме **docker-helper** панель пишет в **`community-install-last.log`** финальную строку **`--- helper завершён code=…`** **внутри** одноразового контейнера: её видно даже если процесс FREE-панели убрал **`docker rm`** и Node не успевает добавить строку **`--- завершено …`**. Если в логе после преамбулы одноразового установщика **нет** ни одной финальной строки — обновите образ панели с актуального **`amnezia_web`** и повторите установку.
-
-### `Bind for 0.0.0.0:8080 failed: port is already allocated` у `amnezia-admin-pro`
-
-На **8080** уже слушает **FREE**‑панель (**`amnezia-admin`**). Пока она работает, второй контейнер панели (PRO) на тот же порт не поднимется. **Кнопка «Установить PRO» из UI по умолчанию** использует одноразовый контейнер **`docker:cli`** (пауза → **`docker rm`** FREE / лендинг / **`amnezia-admin-pro` и любые имена вида `_…_…-amnezia-admin-pro`** из docker compose → затем ваш **`install.sh`**). Иначе вручную: **`docker rm -f amnezia-admin`**, затем установщик PRO. Отключить авто-снос: **`COMMUNITY_DISABLE_DOCKER_CLI_HELPER=1`**.
-
-### В `community-install-last.log`: **`curl: command not found`**, **`tar: invalid magic`**, **`code=2` сразу после helper
-
-Образ **`docker:26-cli`** на Alpine обычно **без `bash` и без `curl`**. PRO `install.sh` с **`#!/usr/bin/env bash`** и загрузкой архива через **`curl | tar`** без них не стартуют (**`curl: command not found`**, **`tar: invalid magic`**). Если после строки «одноразовый установщик» почти сразу **`--- завершено … code=2`** — возможны ошибка сборки prelude в **`sh`**, ошибка **`docker run … -e TOKEN=…`** из‑за спецсимволов в PAT, или **apk** не смог добавить **`bash`** (сеть из контейнера). Обновите панель с **`main`**: helper целиком шлёт **stdout/stderr в лог**, токены передаются средой процесса (без интерполяции в аргументе `-e`), prelude разделён **`;`**; перед **`install.sh`** скрипт сообщает если **нет bash/curl** (**выход по коду ~126**). Если **apk недоступен** — свой **`COMMUNITY_PRO_INSTALL_HELPER_IMAGE`** с **`bash` и `curl`**, либо в приватном `install.sh` перейти на **`#!/bin/sh`** и **`wget`**. Отключить подготовку: **`COMMUNITY_HELPER_SKIP_PREPARE_TOOLS=1`**.
### Установка «зависла» на сообщении **`→ Клонирование релиза …`** и долго без вывода
diff --git a/docs/panel-guide.md b/docs/panel-guide.md
index 46a0980..cf3c82c 100644
--- a/docs/panel-guide.md
+++ b/docs/panel-guide.md
@@ -10,7 +10,7 @@
| **Время** | Отображение часов контейнера и браузера; синхронизация времени хоста VPS по SSH (если доступно). |
| **Cloudflare WARP** | *Необязательно.* Вывод части клиентов в интернет через интерфейс `warp` в контейнере AWG. Если WARP не ставили — статус **«Не установлен»** нормален; панель и VPN без этого работают. Установка и удаление — скрипт `scripts/warp-amnezia.sh` на хосте (`install` / **`uninstall`**), подробности в основном [README](../README.md). |
| **Новый клиент под каскад** | Создание нового peer на сервере с **вашим Endpoint** (промежуточный узел); выдача готового `.conf`. |
-| **Пользователи** | В FREE: просмотр и **удаление** peer с сервера. В PRO дополнительно: вкл/выкл, переименование, даты отключения; экспорт `.conf` если в записи есть `userData.last_config`. |
+| **Пользователи** | Просмотр клиентов и **удаление** peer с сервера. Вкл/выкл, переименование, даты отключения и экспорт `.conf` в базовой версии недоступны. |
## Переключатель «Инстанс» не отображается
diff --git a/package.json b/package.json
index 60c24b0..c99b0e4 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "amnezia-admin",
"version": "1.2.29",
"private": true,
- "description": "amnezia_web — открытая веб-панель AmneziaWG на VPS (Docker): FREE — просмотр/удаление клиентов, MTProto-прокси; PRO — полное управление и обновления.",
+ "description": "amnezia_web — открытая базовая веб-панель AmneziaWG на VPS (Docker): просмотр и удаление клиентов, MTProto-прокси.",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/public/app.js b/public/app.js
index 9bba8fb..a57cb21 100644
--- a/public/app.js
+++ b/public/app.js
@@ -21,6 +21,18 @@ const warpActionsEl = document.querySelector("#warp-actions");
const warpClientListEl = document.querySelector("#warp-client-list");
const warpWgShowEl = document.querySelector("#warp-wg-show");
+const warpCfPanel = document.querySelector("#warp-cf-panel");
+const warpCfStatusLine = document.querySelector("#warp-cf-status-line");
+const warpCfActionsEl = document.querySelector("#warp-cf-actions");
+const warpCfLogEl = document.querySelector("#warp-cf-log");
+const warpCfRootPw = document.querySelector("#warp-cf-root-pw");
+const warpCfPort = document.querySelector("#warp-cf-port");
+const warpCfLicense = document.querySelector("#warp-cf-license");
+const warpCfXrayPath = document.querySelector("#warp-cf-xray-path");
+const warpCfDomains = document.querySelector("#warp-cf-domains");
+const warpCfBanner = document.querySelector("#warp-cf-banner");
+const warpCfPresetsEl = document.querySelector("#warp-cf-presets");
+
const mtprotoPanel = document.querySelector("#mtproto-panel");
const mtprotoStatusLine = document.querySelector("#mtproto-status-line");
const mtprotoActionsEl = document.querySelector("#mtproto-actions");
@@ -74,353 +86,27 @@ let warpSshPendingCmd = null;
/** Какие панели скрыты настройкой сервера (`UI_HIDE_SECTIONS`). */
let uiHidden = { users: false, warp: false, cascade: false, mtproto: false };
-const editionBanner = document.querySelector("#edition-banner");
-const DEFAULT_HEADER_SUB = document.querySelector(".top .sub")?.textContent?.trim() || "";
-
-/** POLL для живого журнала установки PRO (@type {number | null}) */
-let communityInstallLogInterval = null;
-/** Следующий offset для GET /api/community/install-log (@type {number | null}) */
-let communityInstallLogSince = null;
-/** Аккумулированный текст (дополняется кусками) */
-let communityInstallLogShown = "";
-
-function stopCommunityInstallLogPolling() {
- if (communityInstallLogInterval !== null) {
- clearInterval(communityInstallLogInterval);
- communityInstallLogInterval = null;
- }
-}
-
-async function pollCommunityInstallLogOnce() {
- const pre = editionBanner?.querySelector(".edition-banner-install-pre");
- const statusLine = editionBanner?.querySelector(".edition-banner-install-status");
- if (!pre || !editionBanner?.querySelector(".edition-banner-install-log")) {
- stopCommunityInstallLogPolling();
- return;
- }
-
- try {
- const url =
- communityInstallLogSince === null
- ? "/api/community/install-log"
- : `/api/community/install-log?since=${encodeURIComponent(String(communityInstallLogSince))}`;
- const r = await fetch(url, { credentials: "same-origin" });
- const j = await r.json().catch(() => null);
- if (!r.ok) {
- if (statusLine) {
- statusLine.textContent =
- typeof j?.error === "string" ? j.error : `Ошибка ${r.status}`;
- statusLine.classList.add("err");
- }
- return;
- }
- if (statusLine) {
- statusLine.classList.remove("err");
- if (!j.exists) {
- statusLine.textContent =
- communityInstallLogSince === null ? "Журнал пока пустой — секунду…" : "Обновление…";
- } else if (communityInstallLogSince === null && j.totalBytes > j.since1) {
- statusLine.textContent = `Хвост журнала (${fmtBytesRu(Math.max(j.since1 - j.since0, 0))} из ~${fmtBytesRu(j.totalBytes)}) · автообновление…`;
- } else {
- statusLine.textContent = `Размер журнала ≈ ${fmtBytesRu(j.totalBytes)} · автообновление каждые 2 с…`;
- }
- }
-
- const chunk = typeof j.chunk === "string" ? j.chunk : "";
- if (!j.exists) return;
-
- const replace = Boolean(j.resetSuggested || communityInstallLogSince === null);
- if (replace) communityInstallLogShown = chunk;
- else communityInstallLogShown += chunk;
-
- pre.textContent = communityInstallLogShown;
- communityInstallLogSince = typeof j.since1 === "number" ? j.since1 : communityInstallLogSince;
- pre.scrollTop = pre.scrollHeight;
- } catch (_e) {
- if (statusLine) {
- statusLine.textContent = "Не удалось запросить журнал (сеть).";
- statusLine.classList.add("err");
- }
- }
-}
-
-function fmtBytesRu(n) {
- const x = Number(n);
- if (!Number.isFinite(x)) return "";
- if (x < 1024) return `${Math.round(x)} B`;
- if (x < 1024 * 1024) return `${Math.round(x / 102.4) / 10} KiB`;
- return `${Math.round(x / (1024 * 102.4)) / 10} MiB`;
-}
-
-/** Ссылки доната и Telegram‑спонсора: шапка + подвал панели (`GET /api/panel/footer`). */
-function wirePanelPromoAnchor(el, url, label) {
- const u = typeof url === "string" ? url.trim() : "";
- const lb = typeof label === "string" ? label.trim() : "";
- if (!el || !u || !lb) {
- if (el) {
- el.classList.add("hidden");
- el.removeAttribute("href");
- el.textContent = "";
- }
- return false;
- }
- el.href = u;
- el.textContent = lb;
- el.classList.remove("hidden");
- return true;
-}
-
-async function hydratePanelPromoFooter() {
- const strip = document.getElementById("panel-promo-strip");
- const sub = document.getElementById("panel-promo-subtitle");
- const aDonTop = document.getElementById("panel-strip-donate");
- const sepDO = document.getElementById("panel-strip-sep-donate-ozon");
- const aOzonTop = document.getElementById("panel-strip-ozon");
- const sepOT = document.getElementById("panel-strip-sep-ozon-tg");
- const aTgTop = document.getElementById("panel-strip-telegram");
- const gap1 = document.getElementById("footer-promo-gap1");
- const aDonF = document.getElementById("footer-promo-donate");
- const fSepDO = document.getElementById("footer-promo-sep-donate-ozon");
- const aOzonF = document.getElementById("footer-promo-ozon");
- const fSepOT = document.getElementById("footer-promo-sep-ozon-tg");
- const aTgF = document.getElementById("footer-promo-telegram");
-
- try {
- const r = await fetch("/api/panel/footer", { credentials: "same-origin" });
- if (!r.ok) return;
- const j = await r.json().catch(() => null);
- if (!j || typeof j !== "object") return;
-
- const subtitle = typeof j.promoSubtitle === "string" ? j.promoSubtitle.trim() : "";
- if (sub) sub.textContent = subtitle;
-
- const okD = wirePanelPromoAnchor(aDonTop, j.donateUrl, j.donateLabel);
- const okO = wirePanelPromoAnchor(aOzonTop, j.ozonUrl, j.ozonLabel);
- const okT = wirePanelPromoAnchor(aTgTop, j.telegramUrl, j.telegramLabel);
-
- if (sepDO) sepDO.classList.toggle("hidden", !(okD && (okO || okT)));
- if (sepOT) sepOT.classList.toggle("hidden", !(okO && okT));
-
- wirePanelPromoAnchor(aDonF, j.donateUrl, j.donateLabel);
- wirePanelPromoAnchor(aOzonF, j.ozonUrl, j.ozonLabel);
- wirePanelPromoAnchor(aTgF, j.telegramUrl, j.telegramLabel);
-
- const footerD = aDonF && !aDonF.classList.contains("hidden");
- const footerO = aOzonF && !aOzonF.classList.contains("hidden");
- const footerT = aTgF && !aTgF.classList.contains("hidden");
-
- if (gap1) gap1.classList.toggle("hidden", !(footerD || footerO || footerT));
- if (fSepDO) fSepDO.classList.toggle("hidden", !(footerD && (footerO || footerT)));
- if (fSepOT) fSepOT.classList.toggle("hidden", !(footerO && footerT));
-
- if (strip) strip.classList.remove("hidden");
- } catch {
- /* офлайн или сбой — в HTML уже дефолтные ссылки шапки/подвала */
- }
-}
-
-function ensureEditionBannerInstallLogPanel() {
- if (!editionBanner) return null;
- let wrap = editionBanner.querySelector(".edition-banner-install-log");
- if (!wrap) {
- wrap = document.createElement("div");
- wrap.className = "edition-banner-install-log";
- const hdr = document.createElement("div");
- hdr.className = "edition-banner-install-log-head";
- hdr.innerHTML =
- 'Журнал установки ';
-
- const statusLine = document.createElement("div");
- statusLine.className = "edition-banner-install-status muted small";
- statusLine.textContent = "Запрос журнала…";
-
- const pre = document.createElement("pre");
- pre.className = "edition-banner-install-pre monospace";
-
- hdr.querySelector(".edition-install-log-pause")?.addEventListener("click", () => {
- const btn = hdr.querySelector(".edition-install-log-pause");
- if (!btn || !editionBanner.contains(wrap)) return;
- const paused = btn.dataset.paused === "1";
- if (paused) {
- btn.dataset.paused = "";
- btn.textContent = "Пауза";
- startCommunityInstallLogPollingResume();
- } else {
- btn.dataset.paused = "1";
- btn.textContent = "Продолжить";
- stopCommunityInstallLogPolling();
- }
- });
-
- wrap.append(hdr, statusLine, pre);
- editionBanner.querySelector(".edition-banner-activation")?.appendChild(wrap);
- }
- return wrap;
-}
-
-function startCommunityInstallLogPollingResume() {
- stopCommunityInstallLogPolling();
- void pollCommunityInstallLogOnce();
- communityInstallLogInterval = window.setInterval(() => {
- void pollCommunityInstallLogOnce();
- }, 2000);
-}
-
-/** Сброс и показ живого журнала (после «Установить PRO»). */
-function startCommunityInstallLogPollingFresh() {
- communityInstallLogSince = null;
- communityInstallLogShown = "";
- const wrap = ensureEditionBannerInstallLogPanel();
- const pre = wrap?.querySelector(".edition-banner-install-pre");
- const st = wrap?.querySelector(".edition-banner-install-status");
- if (pre) pre.textContent = "";
- if (st) {
- st.classList.remove("err");
- st.textContent = "Запрос журнала…";
- }
- startCommunityInstallLogPollingResume();
-}
-
-/** Состояние редакции панели (community = просмотр + удаление; остальные мутации — PRO). */
+/** Базовые возможности панели (просмотр клиентов + удаление). */
let editionState = {
- tier: "pro",
- readOnlyClients: false,
- allowDeleteClients: false,
- upgradeUrl: null,
- upgradePitch: null,
- showDebugWg: true,
- githubActivationAllowed: false,
+ readOnlyClients: true,
+ allowDeleteClients: true,
+ showDebugWg: false,
};
function applyEditionPayload(data) {
const ed = data?.edition;
if (!ed || typeof ed !== "object") return;
editionState = {
- tier: ed.tier === "community" ? "community" : "pro",
- readOnlyClients: Boolean(ed.readOnlyClients),
- allowDeleteClients: Boolean(ed.allowDeleteClients),
- upgradeUrl: typeof ed.upgradeUrl === "string" ? ed.upgradeUrl : null,
- upgradePitch: typeof ed.upgradePitch === "string" ? ed.upgradePitch : null,
- showDebugWg: ed.showDebugWg !== false,
- githubActivationAllowed: Boolean(ed.githubActivationAllowed),
+ readOnlyClients: ed.readOnlyClients !== false,
+ allowDeleteClients: ed.allowDeleteClients !== false,
+ showDebugWg: Boolean(ed.showDebugWg),
};
const titleEl = document.querySelector(".top h1");
- if (titleEl) {
- titleEl.textContent =
- editionState.tier === "community"
- ? "Пользователи AmneziaWG FREE"
- : "Пользователи AmneziaWG PRO";
- }
+ if (titleEl) titleEl.textContent = "Пользователи AmneziaWG";
const subEl = document.querySelector(".top .sub");
if (subEl) {
- if (editionState.tier === "community") {
- subEl.textContent =
- "Базовая панель amnezia_web: просмотр клиентов и статусов, удаление клиента с сервера, установка Telegram MTProto‑прокси (Docker) в этом интерфейсе. Включение/выключение туннеля, даты, переименование, экспорт .conf, каскад, Cloudflare WARP и синхронизация времени хоста — в версии PRO.";
- } else {
- subEl.textContent = DEFAULT_HEADER_SUB;
- }
- }
- if (editionBanner) {
- if (editionState.tier === "community") {
- const preservedInstallLog = editionBanner.querySelector(".edition-banner-install-log");
- editionBanner.classList.remove("hidden");
- editionBanner.innerHTML = "";
- const wrap = document.createElement("div");
- wrap.className = "edition-banner-inner";
- const textCol = document.createElement("div");
- textCol.className = "edition-banner-text";
- const strong = document.createElement("strong");
- strong.textContent = "Базовая версия · просмотр и удаление клиентов";
- const pitch = document.createElement("p");
- pitch.className = "edition-banner-pitch muted";
- pitch.textContent = editionState.upgradePitch || "";
- textCol.append(strong, pitch);
- const cta = document.createElement("a");
- cta.className = "btn small primary edition-banner-cta";
- cta.rel = "noopener noreferrer";
- cta.target = "_blank";
- cta.href =
- editionState.upgradeUrl ||
- "https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link";
- cta.textContent = "Разблокировать PRO (Boosty)";
- wrap.append(textCol, cta);
- editionBanner.appendChild(wrap);
-
- {
- const act = document.createElement("div");
- act.className = "edition-banner-activation muted";
- const cap = document.createElement("div");
- cap.className = "edition-banner-act-title";
- cap.textContent =
- "Есть GitHub‑токен к приватному репозиторию PRO? Вставьте ниже — установка заменит FREE на PRO (порт сохранится):";
-
- const row = document.createElement("div");
- row.className = "edition-banner-act-row";
-
- const inp = document.createElement("input");
- inp.type = "password";
- inp.autocomplete = "new-password";
- inp.spellcheck = false;
- inp.placeholder = "GitHub PAT (classic: repo или fine‑grained: Contents Read)";
- inp.className = "edition-banner-act-input monospace";
-
- const go = document.createElement("button");
- go.type = "button";
- go.className = "btn small primary";
- go.textContent = "Установить PRO";
-
- const msg = document.createElement("p");
- msg.className = "edition-banner-act-msg muted";
- msg.setAttribute("role", "status");
- msg.textContent = "";
-
- go.addEventListener("click", async () => {
- msg.textContent = "";
- msg.className = "edition-banner-act-msg muted";
- const tok = inp.value.trim();
- if (!tok) {
- msg.className = "edition-banner-act-msg status err";
- msg.textContent = "Вставьте токен.";
- return;
- }
- go.disabled = true;
- try {
- const r = await fetch("/api/community/run-private-install", {
- method: "POST",
- credentials: "same-origin",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ githubToken: tok }),
- });
- const j = await r.json().catch(() => ({}));
- if (!r.ok) {
- msg.className = "edition-banner-act-msg status err";
- msg.textContent = j.error || `Ошибка ${r.status}`;
- go.disabled = false;
- return;
- }
- msg.className = "edition-banner-act-msg edition-act-ok muted";
- msg.textContent =
- j.message ||
- "Установка запущена. Через 2–5 минут откройте панель снова по тому же адресу (или несколько раз обновите страницу).";
- inp.value = "";
- startCommunityInstallLogPollingFresh();
- } catch (e) {
- msg.className = "edition-banner-act-msg status err";
- msg.textContent = String(e?.message || e);
- go.disabled = false;
- }
- });
-
- row.append(inp, go);
- act.append(cap, row, msg);
- if (preservedInstallLog) act.appendChild(preservedInstallLog);
- editionBanner.appendChild(act);
- }
- } else {
- stopCommunityInstallLogPolling();
- editionBanner.classList.add("hidden");
- editionBanner.innerHTML = "";
- }
+ subEl.textContent =
+ "Базовая панель: просмотр клиентов и статусов, удаление клиента с сервера, установка Telegram MTProto‑прокси (Docker).";
}
document.querySelector(".clock-host-sync")?.classList.toggle("hidden", editionState.readOnlyClients);
if (wgRawDetails) wgRawDetails.hidden = uiHidden.users || !editionState.showDebugWg;
@@ -627,6 +313,7 @@ function applyUiHiddenFromPayload(data) {
if (wgRawDetails) wgRawDetails.hidden = uiHidden.users || !editionState.showDebugWg;
if (cascadePanel) cascadePanel.hidden = uiHidden.cascade;
if (warpPanel) warpPanel.hidden = uiHidden.warp;
+ if (warpCfPanel) warpCfPanel.hidden = editionState.readOnlyClients;
if (mtprotoPanel) mtprotoPanel.hidden = uiHidden.mtproto;
void refreshMtprotoPanel();
}
@@ -1090,10 +777,10 @@ async function loadTimeSyncCaps() {
const btn = document.querySelector("#sync-host-time");
try {
const c = await api("/api/time-sync-capabilities");
- if (editionState.readOnlyClients || c.communityBlocked) {
+ if (editionState.readOnlyClients) {
if (hint) {
hint.textContent =
- "В базовой версии синхронизация времени хоста по SSH недоступна — это функция PRO.";
+ "Синхронизация времени хоста по SSH недоступна в базовой версии панели.";
}
if (btn) btn.disabled = true;
return;
@@ -1216,7 +903,7 @@ function renderRows(clients) {
roHint.className = "muted hint-mini";
roHint.style.margin = "0.35rem 0 0";
roHint.textContent = c.exportAvailable
- ? "На сервере есть данные для .conf — скачивание доступно в PRO."
+ ? "На сервере есть данные для .conf — экспорт недоступен в базовой версии."
: "Нет last_config на сервере — полный конфиг в приложении Amnezia.";
nameWrap.appendChild(roHint);
}
@@ -1258,13 +945,13 @@ function renderRows(clients) {
note.className = "muted";
note.style.fontSize = "0.85rem";
note.style.marginBottom = "0.35rem";
- note.textContent = "Вкл/выкл, даты, переименование, экспорт — PRO";
+ note.textContent = "Вкл/выкл, даты, переименование, экспорт недоступны";
actTd.appendChild(note);
actTd.appendChild(btn("Удалить", "btn small warn", () => confirmDelete(c.name, c.clientId)));
} else {
const lock = document.createElement("span");
lock.className = "muted";
- lock.textContent = "Только PRO";
+ lock.textContent = "Недоступно";
actTd.appendChild(lock);
}
} else {
@@ -1487,6 +1174,217 @@ function renderWarpPanel(data) {
);
}
+function normalizeWarpCfPort() {
+ const raw = warpCfPort?.value?.trim();
+ const n = raw ? Number.parseInt(raw, 10) : 40000;
+ if (!Number.isFinite(n) || n < 512 || n > 65535) return 40000;
+ return n;
+}
+
+function warpCfPresetText(port) {
+ const p = Number(port) || 40000;
+ return (
+ "### 3x-ui / Xray (на хосте)\n" +
+ "SOCKS5 адрес: 127.0.0.1:" + p + "\n\n" +
+ "Outbound (пример):\n" +
+ "{\n" +
+ ' \"tag\": \"warp-socks\",\n' +
+ ' \"protocol\": \"socks\",\n' +
+ ' \"settings\": {\"servers\": [{\"address\": \"127.0.0.1\", \"port\": ' + p + '}]}\n' +
+ "}\n\n" +
+ "Routing rule (пример):\n" +
+ "{\n" +
+ ' \"type\": \"field\",\n' +
+ ' \"outboundTag\": \"warp-socks\",\n' +
+ ' \"domain\": [\"geosite:google\", \"geosite:openai\"]\n' +
+ "}\n\n" +
+ "### Amnezia (xray в Docker)\n" +
+ "SOCKS5 адрес: 172.17.0.1:" + p + " (из контейнера до хоста)\n\n" +
+ "Outbound (пример):\n" +
+ "{\n" +
+ ' \"tag\": \"warp-socks\",\n' +
+ ' \"protocol\": \"socks\",\n' +
+ ' \"settings\": {\"servers\": [{\"address\": \"172.17.0.1\", \"port\": ' + p + '}]}\n' +
+ "}\n"
+ );
+}
+
+function redrawWarpCfPresets() {
+ if (!warpCfPresetsEl) return;
+ warpCfPresetsEl.textContent = warpCfPresetText(normalizeWarpCfPort());
+}
+
+async function refreshWarpCfPanel() {
+ if (!warpCfPanel || !warpCfStatusLine || !warpCfActionsEl || !warpCfLogEl) return;
+ if (editionState.readOnlyClients) {
+ warpCfPanel.hidden = true;
+ return;
+ }
+ warpCfPanel.hidden = false;
+ redrawWarpCfPresets();
+
+ warpCfActionsEl.innerHTML = "";
+ if (warpCfBanner) {
+ warpCfBanner.classList.add("hidden");
+ warpCfBanner.textContent = "";
+ }
+
+ const pw = String(warpCfRootPw?.value || "").trim();
+ const port = normalizeWarpCfPort();
+
+ const explain = document.createElement("p");
+ explain.className = "muted warp-muted";
+ explain.textContent =
+ "Для статуса/установки/удаления нужен пароль root по SSH на хост VPS. Пароль не сохраняется — вводите при каждом действии.";
+ warpCfActionsEl.appendChild(explain);
+
+ warpCfActionsEl.appendChild(
+ btn("Статус", "btn small ghost", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ setStatus("WARP (официальный): статус…", false);
+ warpCfLogEl.textContent = "Запрос статуса…";
+ const j = await api("/api/warp-cf/status", {
+ method: "POST",
+ body: JSON.stringify({ rootPassword: pw }),
+ });
+ warpCfStatusLine.textContent =
+ j.installed
+ ? `${j.running ? "подключён" : "не подключён"} · mode ${j.mode || "?"}` +
+ (j.proxyPort ? ` · SOCKS :${j.proxyPort}` : "") +
+ (j.account ? ` · ${j.account}` : "")
+ : "не установлен";
+ warpCfLogEl.textContent = (j.rawStatus || "") + (j.rawAccount ? "\n\n--- account ---\n" + j.rawAccount : "");
+ setStatus("", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+
+ warpCfActionsEl.appendChild(
+ btn("Установить (proxy mode)", "btn small primary", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ if (!confirm(`Установить официальный Cloudflare WARP на хост VPS и поднять SOCKS5 на 127.0.0.1:${port}?`)) {
+ return;
+ }
+ setStatus("WARP (официальный): установка…", false);
+ warpCfLogEl.textContent = "Установка…";
+ const j = await api("/api/warp-cf/install", {
+ method: "POST",
+ body: JSON.stringify({ rootPassword: pw, socksPort: port }),
+ });
+ warpCfLogEl.textContent = j.output || "Готово.";
+ if (warpCfBanner) {
+ warpCfBanner.textContent = `Готово: SOCKS5 на 127.0.0.1:${port}`;
+ warpCfBanner.classList.remove("hidden");
+ }
+ setStatus("", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+
+ warpCfActionsEl.appendChild(
+ btn("Применить WARP+ ключ", "btn small ghost", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ const key = String(warpCfLicense?.value || "").trim();
+ if (!key) throw new Error("Вставьте WARP+ ключ.");
+ setStatus("WARP (официальный): применяю ключ…", false);
+ warpCfLogEl.textContent = "Применение ключа…";
+ const j = await api("/api/warp-cf/license", {
+ method: "POST",
+ body: JSON.stringify({ rootPassword: pw, licenseKey: key }),
+ });
+ warpCfLogEl.textContent = j.output || "Ключ применён.";
+ if (warpCfLicense) warpCfLicense.value = "";
+ setStatus("", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+
+ warpCfActionsEl.appendChild(
+ btn("Удалить", "btn small warn", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ if (!confirm("Удалить официальный Cloudflare WARP с хоста VPS (apt remove cloudflare-warp)?")) return;
+ setStatus("WARP (официальный): удаление…", false);
+ warpCfLogEl.textContent = "Удаление…";
+ const j = await api("/api/warp-cf/uninstall", {
+ method: "POST",
+ body: JSON.stringify({ rootPassword: pw }),
+ });
+ warpCfLogEl.textContent = j.output || "Удалено.";
+ warpCfStatusLine.textContent = "не установлен";
+ setStatus("", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+
+ warpCfActionsEl.appendChild(
+ btn("Применить пресет в Xray (3x‑ui: 127.0.0.1)", "btn small ghost", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ const filePath = String(warpCfXrayPath?.value || "").trim();
+ if (!filePath) throw new Error("Укажите путь к JSON конфигу Xray.");
+ const domains = String(warpCfDomains?.value || "").trim();
+ if (!confirm(`Внести изменения в ${filePath} (создадим .bak* рядом)?`)) return;
+ setStatus("Xray preset: применяю…", false);
+ warpCfLogEl.textContent = "Применение…";
+ const j = await api("/api/warp-cf/xray-apply", {
+ method: "POST",
+ body: JSON.stringify({
+ rootPassword: pw,
+ filePath,
+ socksAddress: "127.0.0.1",
+ socksPort: port,
+ domains,
+ }),
+ });
+ warpCfLogEl.textContent = JSON.stringify(j, null, 2);
+ setStatus("Готово (проверьте и перезапустите Xray/x-ui).", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+
+ warpCfActionsEl.appendChild(
+ btn("Применить пресет в Xray (Docker: 172.17.0.1)", "btn small ghost", async () => {
+ try {
+ if (!pw) throw new Error("Введите пароль root.");
+ const filePath = String(warpCfXrayPath?.value || "").trim();
+ if (!filePath) throw new Error("Укажите путь к JSON конфигу Xray.");
+ const domains = String(warpCfDomains?.value || "").trim();
+ if (!confirm(`Внести изменения в ${filePath} (создадим .bak* рядом)?`)) return;
+ setStatus("Xray preset: применяю…", false);
+ warpCfLogEl.textContent = "Применение…";
+ const j = await api("/api/warp-cf/xray-apply", {
+ method: "POST",
+ body: JSON.stringify({
+ rootPassword: pw,
+ filePath,
+ socksAddress: "172.17.0.1",
+ socksPort: port,
+ domains,
+ }),
+ });
+ warpCfLogEl.textContent = JSON.stringify(j, null, 2);
+ setStatus("Готово (проверьте и перезапустите Xray/контейнер).", false);
+ } catch (e) {
+ setStatus(String(e?.message || e), true);
+ }
+ }),
+ );
+}
+
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
@@ -1691,6 +1589,7 @@ async function loadClients() {
wgShowEl.textContent = data.wgShow || "";
}
renderWarpPanel(data);
+ void refreshWarpCfPanel();
renderRows(data.clients);
setStatus("", false);
void refreshServerClock();
@@ -1707,6 +1606,7 @@ async function loadClients() {
wgShowEl.textContent = "";
peerCountEl.textContent = "";
if (warpPanel) warpPanel.hidden = true;
+ if (warpCfPanel) warpCfPanel.hidden = true;
if (mtprotoPanel) mtprotoPanel.hidden = true;
}
}
@@ -1803,4 +1703,5 @@ function initMtprotoLinkCopy() {
initThemeSwitch();
initMtprotoLinkCopy();
void hydratePanelPromoFooter();
+if (warpCfPort) warpCfPort.addEventListener("input", () => redrawWarpCfPresets());
boot();
diff --git a/public/index.html b/public/index.html
index 497b10a..b180b7f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -29,7 +29,7 @@
Панель сервера
-Базовая открытая панель: просмотр клиентов AmneziaWG и (после входа) установка Telegram MTProto‑прокси в Docker без PRO. Полное управление peer, экспорт .conf, каскад, Cloudflare WARP и синхронизация времени хоста — в версии PRO.
+Базовая панель: просмотр клиентов AmneziaWG, удаление peer с сервера и (после входа) установка Telegram MTProto‑прокси в Docker.