version 1.4.1: per-user WARP флаг, fallback-установка PyPI
- app.py: full_warp флаг в warp_rules, _rebuild_egress читает с диска, race condition fix - bot.py: full_warp поддержка в toggle_warp - templates/index.html: чекбокс «Весь трафик через WARP» с full_warp, не авто-включается - install-panel.sh: зеркала PyPI (aliyun), таймаут 120с на pip, fallback на apt --system-site-packages - install-bot.sh: зеркала и fallback для python-telegram-bot - README: версия 1.4, обновлён roadmap
This commit is contained in:
@@ -66,7 +66,7 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
|
||||
|
||||
## Дорожная карта
|
||||
|
||||
### ✅ Версия 1.2 (реализовано)
|
||||
### ✅ Версия 1.4 (реализовано)
|
||||
|
||||
- [x] Автоматическая установка mita сервера (systemd)
|
||||
- [x] Интеграция Cloudflare WARP через Docker
|
||||
@@ -77,7 +77,7 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
|
||||
- [x] Онлайн-статус пользователей
|
||||
- [x] Генерация клиентских конфигов (mieru JSON и sing-box JSON)
|
||||
- [x] Скачивание конфигов и отображение QR-кодов
|
||||
- [x] Управление WARP на пользователя (toggle + правила: домены, IP, geosite, geoip, URL источники)
|
||||
- [x] Управление WARP: per-user флаг full_warp, правила доменов/IP, всё/ничего, toggle в UI и боте
|
||||
- [x] SSL-сертификаты: самоподписной (10 лет) авто при установке, LE через UI, кнопка перезапуска
|
||||
- [x] Переключение режима доступа: IP / SSH-туннель через вкладку SSL в веб-панели
|
||||
- [x] Автоопределение Telegram ID через API бота при установке (веб-панель, CLI, install.sh)
|
||||
@@ -95,6 +95,7 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
|
||||
- [x] Telegram-бот: inline-кнопки, создание/удаление/WARP/дашборд, конфиг/Karing, fail2ban-безопасность
|
||||
- [x] Установка бота: веб-панель, CLI, полный установщик; автоопределение Telegram ID через API
|
||||
- [x] Бот в venv (обход externally-managed), авто-SSL, SSH-туннель, обновление из GitHub
|
||||
- [x] Fallback-установка: зеркала PyPI + таймауты → apt при недоступности pip
|
||||
|
||||
### ⬜ Запланировано
|
||||
|
||||
|
||||
@@ -296,7 +296,6 @@ def build_singbox_config(name, password):
|
||||
proto = bindings[0].get("protocol", "TCP").upper() if bindings else "TCP"
|
||||
port_range = bindings[0].get("portRange",
|
||||
str(bindings[0].get("port", "2100"))) if bindings else "2100"
|
||||
# Берём первый порт из диапазона для sing-box (он не поддерживает диапазоны)
|
||||
first_port = int(port_range.split("-")[0]) if "-" in port_range else int(port_range)
|
||||
server_ip = get_server_ip()
|
||||
|
||||
@@ -554,15 +553,11 @@ def api_users_delete():
|
||||
@app.route(f"{BASE}/api/users/warp", methods=["POST"])
|
||||
@login_required
|
||||
def api_users_warp():
|
||||
"""Включить/выключить WARP для конкретного пользователя через egress.users."""
|
||||
"""Включить/выключить WARP для конкретного пользователя. При включении весь трафик идёт через WARP."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
name = data.get("name", "")
|
||||
enabled = bool(data.get("enabled", False))
|
||||
cfg = load_mita_config()
|
||||
|
||||
# egress.userGroups не поддерживается в mita — используем workaround:
|
||||
# храним список warp-пользователей в panel.json,
|
||||
# добавляем их домены в egress (все остальные — DIRECT).
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
if enabled:
|
||||
@@ -572,10 +567,8 @@ def api_users_warp():
|
||||
pc["warp_users"] = list(warp_users)
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
|
||||
# Примечание: mita не поддерживает per-user egress нативно.
|
||||
# Флаг хранится в panel.json и отображается в UI как справочная информация.
|
||||
# Реальное разделение — через отдельные порты (см. README).
|
||||
return jsonify({"ok": True, "note": "stored_in_panel_config"})
|
||||
_rebuild_egress()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
@app.route(f"{BASE}/api/users/warp_status")
|
||||
@login_required
|
||||
@@ -861,7 +854,8 @@ def api_warp_rules_get():
|
||||
rules = pc.get("warp_rules", {}).get(name, {
|
||||
"domains": [],
|
||||
"ips": [],
|
||||
"sources": [], # URL или geosite:/geoip: ссылки
|
||||
"sources": [],
|
||||
"full_warp": False,
|
||||
})
|
||||
return jsonify({"ok": True, "name": name, "rules": rules})
|
||||
|
||||
@@ -870,9 +864,10 @@ def api_warp_rules_get():
|
||||
def api_warp_rules_set():
|
||||
data = request.get_json(silent=True) or {}
|
||||
name = data.get("name", "")
|
||||
domains = data.get("domains", []) # список строк
|
||||
domains = data.get("domains", [])
|
||||
ips = data.get("ips", [])
|
||||
sources = data.get("sources", []) # geosite:xxx / geoip:xxx / URL
|
||||
sources = data.get("sources", [])
|
||||
full_warp = data.get("full_warp", False)
|
||||
|
||||
if not name:
|
||||
return jsonify({"ok": False, "error": "Не указано имя"}), 400
|
||||
@@ -882,146 +877,73 @@ def api_warp_rules_set():
|
||||
"domains": [d.strip() for d in domains if d.strip()],
|
||||
"ips": [i.strip() for i in ips if i.strip()],
|
||||
"sources": [s.strip() for s in sources if s.strip()],
|
||||
"full_warp": bool(full_warp),
|
||||
}
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
|
||||
# Перегенерировать egress в mita config на основе всех пользователей
|
||||
_rebuild_egress(pc)
|
||||
_rebuild_egress()
|
||||
|
||||
return jsonify({"ok": True})
|
||||
|
||||
def _get_geosite_data():
|
||||
def _rebuild_egress(pc=None):
|
||||
"""
|
||||
Скачивает (с кэшированием на 7 дней) единый YAML файл со всеми
|
||||
geosite-категориями и возвращает распарсенный dict.
|
||||
Старый способ (отдельный .txt на каждую категорию на ветке release)
|
||||
больше не поддерживается проектом v2fly — теперь только единый
|
||||
dlc.dat_plain.yml в latest release.
|
||||
Глобальный WARP-egress:
|
||||
- full_warp=True у любого пользователя → весь трафик через WARP
|
||||
- full_warp=False + есть правила → указанные домены/IP через WARP, остальное DIRECT
|
||||
- WARP выкл у всех → egress удаляется
|
||||
"""
|
||||
import urllib.request, os, time, yaml
|
||||
|
||||
cache_path = "/var/cache/mita-geosite.yml"
|
||||
url = "https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat_plain.yml"
|
||||
|
||||
need_download = True
|
||||
if os.path.exists(cache_path):
|
||||
age_days = (time.time() - os.path.getmtime(cache_path)) / 86400
|
||||
if age_days < 7:
|
||||
need_download = False
|
||||
|
||||
if need_download:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(cache_path), exist_ok=True)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "mita-panel"})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
data = resp.read()
|
||||
with open(cache_path, "wb") as f:
|
||||
f.write(data)
|
||||
except Exception:
|
||||
pass # используем старый кэш, если есть
|
||||
|
||||
if not os.path.exists(cache_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(cache_path) as f:
|
||||
return yaml.safe_load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _geosite_category_domains(category):
|
||||
"""Возвращает список доменов для одной geosite-категории."""
|
||||
data = _get_geosite_data()
|
||||
if not data:
|
||||
return []
|
||||
domains = []
|
||||
cat_lower = category.lower()
|
||||
for entry in data.get("lists", []):
|
||||
if entry.get("name", "").lower() == cat_lower:
|
||||
for rule in entry.get("rules", []):
|
||||
for prefix in ("domain:", "full:"):
|
||||
if rule.startswith(prefix):
|
||||
domains.append(rule[len(prefix):])
|
||||
break
|
||||
# regexp: и include: пропускаем — не прямые доменные правила
|
||||
break
|
||||
return domains
|
||||
|
||||
def _rebuild_egress(pc):
|
||||
"""
|
||||
Собирает egress.rules из warp_rules всех пользователей и записывает в mita config.
|
||||
mita не поддерживает per-user egress — правила глобальные, но мы объединяем
|
||||
домены/IP всех пользователей у которых WARP включён.
|
||||
Источники (geosite:/geoip:/URL) разворачиваем в реальные списки.
|
||||
"""
|
||||
import urllib.request
|
||||
|
||||
pc = load_panel_config()
|
||||
warp_users = set(pc.get("warp_users", []))
|
||||
all_domains = set()
|
||||
all_ips = set()
|
||||
|
||||
GEOIP_BASE = "https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4"
|
||||
|
||||
for uname, rules in pc.get("warp_rules", {}).items():
|
||||
if uname not in warp_users:
|
||||
continue # пользователь не включён в WARP — пропускаем
|
||||
|
||||
all_domains.update(rules.get("domains", []))
|
||||
all_ips.update(rules.get("ips", []))
|
||||
|
||||
for src in rules.get("sources", []):
|
||||
src = src.strip()
|
||||
try:
|
||||
if src.startswith("geosite:"):
|
||||
cat = src[len("geosite:"):]
|
||||
all_domains.update(_geosite_category_domains(cat))
|
||||
elif src.startswith("geoip:"):
|
||||
country = src[len("geoip:"):]
|
||||
url = f"{GEOIP_BASE}/{country}.cidr"
|
||||
lines = urllib.request.urlopen(url, timeout=10).read().decode().splitlines()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
all_ips.add(line)
|
||||
elif src.startswith("http"):
|
||||
lines = urllib.request.urlopen(src, timeout=10).read().decode().splitlines()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
# Определяем домен или IP по наличию /
|
||||
if "/" in line or line.replace(".","").isdigit():
|
||||
all_ips.add(line)
|
||||
else:
|
||||
all_domains.add(line)
|
||||
except Exception:
|
||||
pass # не критично — продолжаем без этого источника
|
||||
|
||||
if not warp_users:
|
||||
cfg = load_mita_config()
|
||||
|
||||
if not all_domains and not all_ips:
|
||||
# Нет правил — убираем egress полностью
|
||||
cfg.pop("egress", None)
|
||||
else:
|
||||
domain_list = sorted(all_domains)
|
||||
ip_list = sorted(all_ips)
|
||||
warp_rule = {"action": "PROXY", "proxyNames": ["warp"]}
|
||||
if domain_list:
|
||||
warp_rule["domainNames"] = domain_list
|
||||
if ip_list:
|
||||
warp_rule["ipRanges"] = ip_list
|
||||
save_mita_config(cfg)
|
||||
return
|
||||
|
||||
cfg["egress"] = {
|
||||
"proxies": [{
|
||||
warp_proxy = {
|
||||
"name": "warp",
|
||||
"protocol": "SOCKS5_PROXY_PROTOCOL",
|
||||
"host": "127.0.0.1",
|
||||
"port": WARP_PORT,
|
||||
}],
|
||||
}
|
||||
|
||||
all_domains = set()
|
||||
all_ips = set()
|
||||
has_full = False
|
||||
|
||||
for uname in warp_users:
|
||||
rules = pc.get("warp_rules", {}).get(uname, {})
|
||||
if rules.get("full_warp"):
|
||||
has_full = True
|
||||
all_domains.update(d for d in rules.get("domains", []) if d)
|
||||
all_ips.update(i for i in rules.get("ips", []) if i)
|
||||
|
||||
cfg = load_mita_config()
|
||||
|
||||
if has_full:
|
||||
cfg["egress"] = {
|
||||
"proxies": [warp_proxy],
|
||||
"rules": [
|
||||
{"ipRanges": ["*"], "domainNames": ["*"],
|
||||
"action": "PROXY", "proxyNames": ["warp"]},
|
||||
],
|
||||
}
|
||||
elif all_domains or all_ips:
|
||||
warp_rule = {"action": "PROXY", "proxyNames": ["warp"]}
|
||||
if all_domains:
|
||||
warp_rule["domainNames"] = sorted(all_domains)
|
||||
if all_ips:
|
||||
warp_rule["ipRanges"] = sorted(all_ips)
|
||||
cfg["egress"] = {
|
||||
"proxies": [warp_proxy],
|
||||
"rules": [
|
||||
warp_rule,
|
||||
{"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"},
|
||||
],
|
||||
}
|
||||
else:
|
||||
cfg.pop("egress", None)
|
||||
|
||||
save_mita_config(cfg)
|
||||
|
||||
|
||||
@@ -349,6 +349,43 @@ async def toggle_warp(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
status = "включён"
|
||||
pc["warp_users"] = list(warp_users)
|
||||
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||
|
||||
# Перестроить egress
|
||||
cfg = load_mita_config()
|
||||
warp_users_after = set(pc.get("warp_users", []))
|
||||
if not warp_users_after:
|
||||
cfg.pop("egress", None)
|
||||
else:
|
||||
wp = {"name": "warp", "protocol": "SOCKS5_PROXY_PROTOCOL",
|
||||
"host": "127.0.0.1", "port": 40000}
|
||||
has_full = any(pc.get("warp_rules", {}).get(u, {}).get("full_warp") for u in warp_users_after)
|
||||
if has_full:
|
||||
cfg["egress"] = {
|
||||
"proxies": [wp],
|
||||
"rules": [{"ipRanges": ["*"], "domainNames": ["*"],
|
||||
"action": "PROXY", "proxyNames": ["warp"]}],
|
||||
}
|
||||
else:
|
||||
all_dom = set()
|
||||
all_ip = set()
|
||||
for uname in warp_users_after:
|
||||
r = pc.get("warp_rules", {}).get(uname, {})
|
||||
all_dom.update(d for d in r.get("domains", []) if d)
|
||||
all_ip.update(i for i in r.get("ips", []) if i)
|
||||
if all_dom or all_ip:
|
||||
wr = {"action": "PROXY", "proxyNames": ["warp"]}
|
||||
if all_dom: wr["domainNames"] = sorted(all_dom)
|
||||
if all_ip: wr["ipRanges"] = sorted(all_ip)
|
||||
cfg["egress"] = {
|
||||
"proxies": [wp],
|
||||
"rules": [wr, {"ipRanges": ["*"], "domainNames": ["*"], "action": "DIRECT"}],
|
||||
}
|
||||
else:
|
||||
cfg.pop("egress", None)
|
||||
Path(MITA_CONFIG).write_text(json.dumps(cfg, indent=2, ensure_ascii=False))
|
||||
subprocess.run(["mita", "apply", "config", MITA_CONFIG], capture_output=True)
|
||||
subprocess.run(["systemctl", "restart", "mita"], capture_output=True)
|
||||
|
||||
await query.edit_message_text(
|
||||
f"🔄 WARP для `{escape_md(name)}`: *{status}*",
|
||||
reply_markup=InlineKeyboardMarkup([
|
||||
|
||||
+24
-2
@@ -73,8 +73,30 @@ if [[ ! -d "$BOT_VENV" ]]; then
|
||||
python3 -m venv "$BOT_VENV"
|
||||
fi
|
||||
|
||||
"$BOT_VENV/bin/pip" install -q python-telegram-bot psutil 2>/dev/null
|
||||
ok "Python-зависимости установлены в виртуальное окружение"
|
||||
info "Установка python-telegram-bot (может занять до минуты)..."
|
||||
_bot_pip() {
|
||||
timeout 120 "$BOT_VENV/bin/pip" install -q --disable-pip-version-check --timeout 15 "$@"
|
||||
}
|
||||
bot_pip_ok=false
|
||||
for mirror in "https://pypi.org/simple/" "https://mirrors.aliyun.com/pypi/simple/"; do
|
||||
if _bot_pip --index-url "$mirror" python-telegram-bot psutil 2>/dev/null; then
|
||||
bot_pip_ok=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $bot_pip_ok; then
|
||||
warn "pip не смог установить python-telegram-bot. Пробую через apt..."
|
||||
rm -rf "$BOT_VENV"
|
||||
python3 -m venv --system-site-packages "$BOT_VENV"
|
||||
apt-get install -y -qq python3-psutil 2>/dev/null
|
||||
apt-get install -y -qq python3-telegram-bot 2>/dev/null || \
|
||||
"$BOT_VENV/bin/pip" install --timeout 15 --index-url https://mirrors.aliyun.com/pypi/simple/ python-telegram-bot 2>/dev/null || {
|
||||
warn "Не удалось установить зависимости бота."
|
||||
warn "Установите вручную: /opt/mita-bot/venv/bin/pip install python-telegram-bot psutil"
|
||||
}
|
||||
fi
|
||||
ok "Python-зависимости бота установлены"
|
||||
|
||||
# ── копирование файлов ──────────────────────────────────────────────
|
||||
section "Установка бота"
|
||||
|
||||
+37
-1
@@ -74,8 +74,44 @@ PANEL_DIR="/opt/mita-panel"
|
||||
mkdir -p "$PANEL_DIR"
|
||||
|
||||
python3 -m venv "$PANEL_DIR/venv"
|
||||
"$PANEL_DIR/venv/bin/pip" install -q flask gunicorn pyyaml psutil 2>/dev/null
|
||||
info "Установка Flask и зависимостей (может занять до минуты)..."
|
||||
|
||||
_pip_install() {
|
||||
# Принудительно IPv4 + таймаут 15 секунд на соединение
|
||||
timeout 120 "$PANEL_DIR/venv/bin/pip" install \
|
||||
--no-input --quiet --disable-pip-version-check \
|
||||
--timeout 15 \
|
||||
"$@"
|
||||
}
|
||||
|
||||
MIRRORS=(
|
||||
"https://pypi.org/simple/"
|
||||
"https://mirrors.aliyun.com/pypi/simple/"
|
||||
)
|
||||
|
||||
pip_ok=false
|
||||
for mirror in "${MIRRORS[@]}"; do
|
||||
info "Пробую зеркало: $mirror"
|
||||
if _pip_install --index-url "$mirror" flask gunicorn pyyaml psutil; then
|
||||
pip_ok=true
|
||||
break
|
||||
fi
|
||||
warn "Зеркало $mirror недоступно, пробую дальше..."
|
||||
done
|
||||
|
||||
if ! $pip_ok; then
|
||||
warn "pip не смог установить зависимости. Пробую через apt..."
|
||||
apt-get install -y -qq python3-flask python3-gunicorn python3-yaml python3-psutil 2>/dev/null || {
|
||||
error "Не удалось установить Flask ни через pip, ни через apt"
|
||||
exit 1
|
||||
}
|
||||
# Создаём venv с системными пакетами чтобы gunicorn был доступен
|
||||
rm -rf "$PANEL_DIR/venv"
|
||||
python3 -m venv --system-site-packages "$PANEL_DIR/venv"
|
||||
ok "Flask установлен через системные пакеты (venv с --system-site-packages)"
|
||||
else
|
||||
ok "Python venv, Flask и зависимости установлены"
|
||||
fi
|
||||
|
||||
# ── копирование файлов ─────────────────────────────────────────────
|
||||
section "Копирование файлов панели"
|
||||
|
||||
+28
-23
@@ -773,11 +773,24 @@ async function setWarp(name,enabled){
|
||||
async function openWarpRules(name){
|
||||
// Загрузить текущие правила
|
||||
const d=await api('/api/users/warp_rules?name='+encodeURIComponent(name));
|
||||
const r=d?.rules||{domains:[],ips:[],sources:[]};
|
||||
const r=d?.rules||{domains:[],ips:[],sources:[],full_warp:false};
|
||||
|
||||
const html=`
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px;padding:12px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:8px">
|
||||
<label class="toggle" style="flex-shrink:0">
|
||||
<input type="checkbox" id="wr-full-warp" ${r.full_warp?'checked':''}
|
||||
onchange="toggleFullWarp(this.checked)">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.9rem">Весь трафик через WARP</div>
|
||||
<div style="font-size:.75rem;color:var(--muted)">Если включено — правила ниже игнорируются, всё идёт через WARP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wr-rules-section">
|
||||
<p style="color:var(--muted);font-size:.85rem;margin-bottom:18px;line-height:1.6">
|
||||
Трафик этого пользователя через <strong style="color:var(--accent)">WARP</strong> только для указанных доменов, IP и источников.<br>
|
||||
Если «Весь трафик» выключен — через <strong style="color:var(--accent)">WARP</strong> пойдут только указанные домены и IP.<br>
|
||||
Всё остальное идёт напрямую с IP вашего сервера.
|
||||
</p>
|
||||
|
||||
@@ -799,23 +812,9 @@ async function openWarpRules(name){
|
||||
color:var(--text);padding:10px 12px;border-radius:8px;font-family:var(--mono);font-size:.82rem;
|
||||
resize:vertical;outline:none;line-height:1.6" placeholder="104.16.0.0/12 172.64.0.0/13">${escH(r.ips.join('\n'))}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:20px">
|
||||
<label style="font-size:.8rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;display:block;margin-bottom:8px">
|
||||
Источники — geosite / geoip / URL (по одному на строку)
|
||||
</label>
|
||||
<textarea id="wr-sources" rows="4" style="width:100%;background:var(--bg);border:1px solid var(--border);
|
||||
color:var(--text);padding:10px 12px;border-radius:8px;font-family:var(--mono);font-size:.82rem;
|
||||
resize:vertical;outline:none;line-height:1.6"
|
||||
placeholder="geosite:openai geosite:netflix geoip:us https://example.com/domains.txt">${escH(r.sources.join('\n'))}</textarea>
|
||||
<div style="font-size:.75rem;color:var(--muted);margin-top:6px;line-height:1.7">
|
||||
<strong style="color:var(--text)">geosite:категория</strong> — из базы v2fly (openai, google, netflix, category-ru…)<br>
|
||||
<strong style="color:var(--text)">geoip:страна</strong> — IP-диапазоны страны (us, gb, de…)<br>
|
||||
<strong style="color:var(--text)">https://…</strong> — прямая ссылка на текстовый файл с доменами или CIDR
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px">
|
||||
<div style="display:flex;gap:10px;margin-top:16px">
|
||||
<button class="btn btn-primary" onclick="saveWarpRules('${escH(name)}')">
|
||||
Сохранить и применить
|
||||
</button>
|
||||
@@ -826,23 +825,29 @@ async function openWarpRules(name){
|
||||
openModal('WARP-правила: '+name, html);
|
||||
}
|
||||
|
||||
function toggleFullWarp(checked){
|
||||
const el=document.getElementById('wr-rules-section');
|
||||
if(el)el.style.display=checked?'none':'';
|
||||
}
|
||||
|
||||
async function saveWarpRules(name){
|
||||
const btn=document.querySelector('#modal-body .btn-primary');
|
||||
btn.disabled=true;
|
||||
btn.innerHTML='<span class="spinner"></span> Применяю…';
|
||||
|
||||
const domains=document.getElementById('wr-domains').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
const ips=document.getElementById('wr-ips').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
const sources=document.getElementById('wr-sources').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
const fullWarp=document.getElementById('wr-full-warp')?.checked||false;
|
||||
const domains=fullWarp?[]:document.getElementById('wr-domains').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
const ips=fullWarp?[]:document.getElementById('wr-ips').value.split('\n').map(s=>s.trim()).filter(Boolean);
|
||||
|
||||
const d=await api('/api/users/warp_rules','POST',{name,domains,ips,sources});
|
||||
const d=await api('/api/users/warp_rules','POST',{name,domains,ips,sources:[],full_warp:fullWarp});
|
||||
btn.disabled=false;
|
||||
btn.textContent='Сохранить и применить';
|
||||
|
||||
const el=document.getElementById('wr-result');
|
||||
if(d?.ok){
|
||||
el.innerHTML='<div class="alert alert-ok">✓ Правила сохранены и применены в mita</div>';
|
||||
showToast('Правила WARP обновлены');
|
||||
el.innerHTML=`<div class="alert alert-ok">✓ ${fullWarp?'Весь трафик через WARP':'Правила сохранены'} и применены в mita</div>`;
|
||||
showToast(fullWarp?'WARP: весь трафик для '+name:'Правила WARP обновлены');
|
||||
loadUsers();
|
||||
}else{
|
||||
el.innerHTML=`<div class="alert alert-err">Ошибка: ${escH(d?.error||'')}</div>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user