Добавил загрузку конфигов WG

This commit is contained in:
Petro1990 2025-11-26 13:59:29 +03:00
parent 39a8308f16
commit c53554ba4e
1 changed files with 358 additions and 139 deletions

View File

@ -100,9 +100,7 @@ def parse_wireguard(config_text, custom_name=None):
conf = {"interface": {}, "peer": {}} conf = {"interface": {}, "peer": {}}
section = None section = None
# 1. Читаем построчно, чистим комментарии и собираем секции
for line in config_text.splitlines(): for line in config_text.splitlines():
# Удаляем inline комментарии (# или ;)
line = line.split('#')[0].split(';')[0].strip() line = line.split('#')[0].split(';')[0].strip()
if not line: continue if not line: continue
@ -124,11 +122,9 @@ def parse_wireguard(config_text, custom_name=None):
if not iface or not peer: if not iface or not peer:
return None, "Invalid WireGuard config: missing Interface or Peer" return None, "Invalid WireGuard config: missing Interface or Peer"
# 2. Endpoint (Server + Port)
endpoint = peer.get('endpoint', '') endpoint = peer.get('endpoint', '')
if not endpoint: return None, "No Endpoint found" if not endpoint: return None, "No Endpoint found"
# Обработка IPv6 в Endpoint [::1]:port
if ']:' in endpoint: if ']:' in endpoint:
server = endpoint.split(']:')[0][1:] server = endpoint.split(']:')[0][1:]
port = endpoint.split(']:')[1] port = endpoint.split(']:')[1]
@ -137,19 +133,16 @@ def parse_wireguard(config_text, custom_name=None):
else: else:
return None, "Invalid Endpoint format" return None, "Invalid Endpoint format"
# 3. Name Logic
name = "WireGuard" name = "WireGuard"
if custom_name: if custom_name:
name = custom_name name = custom_name
else: else:
# Пытаемся взять имя из первой строки оригинального текста, если там комментарий
first_line = config_text.splitlines()[0].strip() first_line = config_text.splitlines()[0].strip()
if first_line.startswith('#') and len(first_line) > 2: if first_line.startswith('#') and len(first_line) > 2:
name = first_line[1:].strip() name = first_line[1:].strip()
else: else:
name = f"WG_{server}" name = f"WG_{server}"
# 4. Address (IP + IPv6)
address_raw = iface.get('address', '') address_raw = iface.get('address', '')
if not address_raw: return None, "No Address found" if not address_raw: return None, "No Address found"
@ -158,7 +151,6 @@ def parse_wireguard(config_text, custom_name=None):
ip_v6 = None ip_v6 = None
for ip in ips: for ip in ips:
# Убираем маску /32, /24 и т.д.
clean_ip = ip.split('/')[0] clean_ip = ip.split('/')[0]
if ':' in clean_ip: if ':' in clean_ip:
if not ip_v6: ip_v6 = clean_ip if not ip_v6: ip_v6 = clean_ip
@ -168,7 +160,6 @@ def parse_wireguard(config_text, custom_name=None):
if not ip_v4 and not ip_v6: if not ip_v4 and not ip_v6:
return None, "No valid IP address found" return None, "No valid IP address found"
# 5. Сборка YAML
y = [] y = []
y.append(f'- name: "{name}"') y.append(f'- name: "{name}"')
y.append(f' type: wireguard') y.append(f' type: wireguard')
@ -184,11 +175,9 @@ def parse_wireguard(config_text, custom_name=None):
pubk = peer.get('publickey') pubk = peer.get('publickey')
if pubk: y.append(f' public-key: {pubk}') if pubk: y.append(f' public-key: {pubk}')
# Исправлено: pre-shared-key (с дефисом)
psk = peer.get('presharedkey') psk = peer.get('presharedkey')
if psk: y.append(f' pre-shared-key: {psk}') if psk: y.append(f' pre-shared-key: {psk}')
# DNS
dns_raw = iface.get('dns') dns_raw = iface.get('dns')
if dns_raw: if dns_raw:
dns_list = [d.strip() for d in dns_raw.split(',')] dns_list = [d.strip() for d in dns_raw.split(',')]
@ -199,27 +188,24 @@ def parse_wireguard(config_text, custom_name=None):
y.append(' udp: true') y.append(' udp: true')
# 6. AmneziaWG Specific
amnezia_keys = ['jc', 'jmin', 'jmax', 's1', 's2', 'h1', 'h2', 'h3', 'h4'] amnezia_keys = ['jc', 'jmin', 'jmax', 's1', 's2', 'h1', 'h2', 'h3', 'h4']
amn_opts = {} amn_opts = {}
for k in amnezia_keys: for k in amnezia_keys:
if k in iface: if k in iface:
val = iface[k] val = iface[k]
if val.isdigit(): if val.isdigit():
amn_opts[k] = int(val) # Важно: int, не string amn_opts[k] = int(val)
if amn_opts: if amn_opts:
y.append(' amnezia-wg-option:') y.append(' amnezia-wg-option:')
for k, v in amn_opts.items(): for k, v in amn_opts.items():
y.append(f' {k}: {v}') y.append(f' {k}: {v}')
# 7. AllowedIPs (добавлено)
allowed = peer.get('allowedips') allowed = peer.get('allowedips')
if allowed: if allowed:
al_list = [x.strip() for x in allowed.split(',')] al_list = [x.strip() for x in allowed.split(',')]
y.append(f' allowed-ips: {json.dumps(al_list)}') y.append(f' allowed-ips: {json.dumps(al_list)}')
# Исправлено: persistent-keepalive
ka = peer.get('persistentkeepalive') ka = peer.get('persistentkeepalive')
if ka: if ka:
y.append(f' persistent-keepalive: {ka}') y.append(f' persistent-keepalive: {ka}')
@ -250,7 +236,6 @@ def insert_proxy_logic(content, proxy_name, target_groups):
if is_new_group: if is_new_group:
if in_proxies_list and current_group_name in target_groups and current_group_name not in inserted_in_group: if in_proxies_list and current_group_name in target_groups and current_group_name not in inserted_in_group:
# End of list section
prefix = " " * (proxies_list_indent + 2) prefix = " " * (proxies_list_indent + 2)
new_lines.append(prefix + '- "' + proxy_name + '"') new_lines.append(prefix + '- "' + proxy_name + '"')
inserted_in_group.add(current_group_name) inserted_in_group.add(current_group_name)
@ -269,14 +254,11 @@ def insert_proxy_logic(content, proxy_name, target_groups):
current_group_name = raw_name.strip("'").strip('"') current_group_name = raw_name.strip("'").strip('"')
if current_group_name in target_groups and stripped.startswith('proxies:'): if current_group_name in target_groups and stripped.startswith('proxies:'):
# Check for inline list style: proxies: [a, b]
if '[' in stripped and stripped.rstrip().endswith(']'): if '[' in stripped and stripped.rstrip().endswith(']'):
start = line.find('[') start = line.find('[')
end = line.rfind(']') end = line.rfind(']')
if start != -1 and end != -1: if start != -1 and end != -1:
content_inner = line[start + 1:end] content_inner = line[start + 1:end]
# Check if proxy already exists in the list
# Simple check: name in text (robust enough for simple cases)
if proxy_name not in content_inner: if proxy_name not in content_inner:
sep = ", " if content_inner.strip() else "" sep = ", " if content_inner.strip() else ""
new_content = content_inner + sep + f'"{proxy_name}"' new_content = content_inner + sep + f'"{proxy_name}"'
@ -327,8 +309,6 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
found_target = False found_target = False
replaced = False replaced = False
# Регулярка для поиска начала блока прокси с указанным именем
# Учитывает кавычки и пробелы: - name: "target_name"
name_pattern = re.compile(r'^\s*-\s+name:\s*(["\'])?' + re.escape(target_name) + r'(\1)?\s*$') name_pattern = re.compile(r'^\s*-\s+name:\s*(["\'])?' + re.escape(target_name) + r'(\1)?\s*$')
i = 0 i = 0
@ -336,33 +316,19 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
line = lines[i] line = lines[i]
stripped = line.strip() stripped = line.strip()
# Определяем секцию proxies
if stripped.startswith('proxies:'): if stripped.startswith('proxies:'):
in_proxies = True in_proxies = True
new_content_lines.append(line) new_content_lines.append(line)
i += 1 i += 1
continue continue
# Если вышли из proxies (новая секция начинается без отступа)
if in_proxies and line and not line.startswith(' ') and not line.startswith('\t') and not line.startswith('#'): if in_proxies and line and not line.startswith(' ') and not line.startswith('\t') and not line.startswith('#'):
in_proxies = False in_proxies = False
if in_proxies and not replaced: if in_proxies and not replaced:
if name_pattern.match(stripped): if name_pattern.match(stripped):
# Нашли целевой прокси.
# 1. Определяем отступ этого блока (обычно 2 пробела)
indent_len = len(line) - len(line.lstrip()) indent_len = len(line) - len(line.lstrip())
# 2. Добавляем новый YAML.
# new_yaml_lines приходят без отступов (или с базовыми).
# Нам нужно убедиться, что первая строка имеет дефис, а остальные - отступ.
# Обычно new_yaml_lines[0] уже "- name: ...".
# Просто добавим отступ всем строкам.
# Принудительно меняем имя в новом YAML на целевое, чтобы сохранить структуру
# (хотя парсеры уже могли вернуть новое имя, но лучше перестраховаться)
if new_yaml_lines and "name:" in new_yaml_lines[0]: if new_yaml_lines and "name:" in new_yaml_lines[0]:
# Заменяем имя в первой строке нового конфига на старое (target_name)
new_yaml_lines[0] = re.sub(r'name:\s*".*"', f'name: "{target_name}"', new_yaml_lines[0]) new_yaml_lines[0] = re.sub(r'name:\s*".*"', f'name: "{target_name}"', new_yaml_lines[0])
for n_line in new_yaml_lines: for n_line in new_yaml_lines:
@ -371,28 +337,16 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
replaced = True replaced = True
found_target = True found_target = True
# 3. Пропускаем старый блок
# Читаем дальше, пока не найдем строку с ТАКИМ ЖЕ отступом, начинающуюся с '-' (следующий элемент списка)
# или строку с МЕНЬШИМ отступом (конец секции)
i += 1 i += 1
while i < len(lines): while i < len(lines):
next_line = lines[i] next_line = lines[i]
next_stripped = next_line.strip() next_stripped = next_line.strip()
next_indent = len(next_line) - len(next_line.lstrip()) next_indent = len(next_line) - len(next_line.lstrip())
if not next_stripped:
if not next_stripped: # Пустые строки пропускаем/удаляем внутри блока
i += 1 i += 1
continue continue
if next_indent < indent_len: break
if next_indent < indent_len: if next_indent == indent_len and next_stripped.startswith('-'): break
# Конец секции proxies
break
if next_indent == indent_len and next_stripped.startswith('-'):
# Следующий элемент списка
break
# Это всё еще часть старого блока, пропускаем
i += 1 i += 1
continue continue
@ -403,11 +357,11 @@ def replace_proxy_block(content, target_name, new_yaml_lines):
HTML_TEMPLATE = """<!DOCTYPE html> HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="ru"> <html>
<head> <head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>Mihomo Editor v18.8</title> <title>Mihomo Editor v18.9</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.7/ace.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.7/ace.js"></script>
<style> <style>
:root { :root {
@ -550,155 +504,417 @@ button:hover{filter:brightness(1.1)}
</style> </style>
</head> </head>
<body> <body>
<div class="toast" id="toast"> Успешно сохранено</div> <div class="toast" id="toast" data-i18n="toast_saved"> Успешно сохранено</div>
<div class="hdr"> <div class="hdr">
<div style="display:flex;align-items:center;gap:10px"> <div style="display:flex;align-items:center;gap:10px">
<h2 style="margin:0;color:#4caf50">Mihomo Studio</h2> <h2 style="margin:0;color:#4caf50" data-i18n="title">Mihomo Studio</h2>
<span style="color:var(--txt-sec);font-size:12px">v18.8 Auto-Panel</span> <span style="color:var(--txt-sec);font-size:12px">v18.9 Auto-Panel</span>
</div> </div>
<div id="last-load">Loaded: __TIME__</div> <div id="last-load">Loaded: __TIME__</div>
</div> </div>
<div class="bar"> <div class="bar">
<button onclick="save('save')" class="btn-s">💾 Сохранить</button> <button onclick="save('save')" class="btn-s" data-i18n="save">💾 Сохранить</button>
<button onclick="save('restart')" class="btn-r">🚀 Рестарт</button> <button onclick="save('restart')" class="btn-r" data-i18n="restart">🚀 Рестарт</button>
<button onclick="openPanel()" class="btn-g" title="Открыть встроенную панель Mihomo">🌐 Панель</button> <button onclick="openPanel()" class="btn-g" title="Открыть встроенную панель Mihomo" data-i18n="panel">🌐 Панель</button>
<select id="theme-sel" onchange="setTheme(this.value)" style="max-width:120px; padding:0 10px; margin:0;">
<option value="dark">🌑 Dark</option> <div style="display:flex; gap:5px; margin-left:auto;">
<option value="light"> Light</option> <select id="lang-sel" onchange="setLang(this.value)" style="width:100px; padding:0 5px;">
<option value="midnight">🌃 Midnight</option> <option value="ru">🇷🇺 RU</option>
<option value="cyber">👾 Cyber</option> <option value="en">🇺🇸 EN</option>
<option value="uk">🇺🇦 UA</option>
</select> </select>
<select id="theme-sel" onchange="setTheme(this.value)" style="width:120px; padding:0 5px;">
<option value="dark" data-i18n="theme_dark">🌑 Dark</option>
<option value="light" data-i18n="theme_light"> Light</option>
<option value="midnight" data-i18n="theme_midnight">🌃 Midnight</option>
<option value="cyber" data-i18n="theme_cyber">👾 Cyber</option>
</select>
</div>
</div> </div>
<div class="main"> <div class="main">
<div id="ed"></div> <div id="ed"></div>
<div class="sb"> <div class="sb">
<div class="sec"> <div class="sec">
<h3>Профили</h3> <h3><span data-i18n="profiles">Профили</span></h3>
<div class="prof-row"> <div class="prof-row">
<select id="prof-sel">__PROFILES__</select> <select id="prof-sel">__PROFILES__</select>
<button onclick="switchProf()" class="btn-s" style="padding:0; width:36px; justify-content:center;" title="Выбрать"></button> <button onclick="switchProf()" class="btn-s" style="padding:0; width:36px; justify-content:center;" title="Выбрать" data-i18n="select"></button>
<button onclick="downloadProf()" class="btn-g" style="padding:0; width:36px; justify-content:center;" title="Скачать">💾</button> <button onclick="downloadProf()" class="btn-g" style="padding:0; width:36px; justify-content:center;" title="Скачать" data-i18n="download">💾</button>
</div> </div>
<div class="prof-btns"> <div class="prof-btns">
<button onclick="openAddProf()" class="btn-u"> Создать</button> <button onclick="openAddProf()" class="btn-u" data-i18n="create"> Создать</button>
<button onclick="delProf()" class="btn-d">🗑 Удалить</button> <button onclick="delProf()" class="btn-d" data-i18n="delete">🗑 Удалить</button>
</div> </div>
</div> </div>
<div class="sec"> <div class="sec">
<h3>Управление прокси</h3> <h3><span data-i18n="proxy_mgmt">Управление</span></h3>
<div class="proxy-grid"> <div class="proxy-grid">
<button onclick="openAddProxyModal()" class="btn-s"> Добавить</button> <button onclick="openAddProxyModal()" class="btn-s" data-i18n="add"> Добавить</button>
<button onclick="openEditProxyModal()" class="btn-u"> Изменить</button> <button onclick="openEditProxyModal()" class="btn-u" data-i18n="edit"> Заменить</button>
<button onclick="showRename()" class="btn-g">Aa Переимен.</button> <button onclick="showRename()" class="btn-g" data-i18n="rename">Переименовать</button>
<button onclick="showDel()" class="btn-d">🗑 Удалить</button> <button onclick="showDel()" class="btn-d" data-i18n="delete">🗑 Удалить</button>
</div> </div>
</div> </div>
<div class="sec"> <div class="sec">
<h3>Бэкапы</h3> <h3><span data-i18n="backups">Бэкапы</span></h3>
<div class="bk-controls"> <div class="bk-controls">
<span>Оставить:</span> <span data-i18n="keep">Оставить:</span>
<input type="number" id="bk-lim" value="5" min="1" max="50"> <input type="number" id="bk-lim" value="5" min="1" max="50">
<button onclick="cleanBackups()" class="btn-g">Очистить</button> <button onclick="cleanBackups()" class="btn-g" data-i18n="clean">Очистить</button>
</div> </div>
<div id="bk-list">__BACKUPS__</div> <div id="bk-list">__BACKUPS__</div>
</div> </div>
</div> </div>
</div> </div>
<div id="m-grp" class="ovl"><div class="mod"><h3>Добавить в группы:</h3> <div id="m-grp" class="ovl"><div class="mod"><h3 data-i18n="modal_groups">Добавить в группы:</h3>
<div style="display:flex; gap:10px; margin-bottom:10px"><button onclick="tgGrp(true)" class="btn-g" style="flex:1; justify-content:center"> Выбрать все</button><button onclick="tgGrp(false)" class="btn-g" style="flex:1; justify-content:center"> Снять все</button></div> <div style="display:flex; gap:10px; margin-bottom:10px"><button onclick="tgGrp(true)" class="btn-g" style="flex:1; justify-content:center" data-i18n="btn_sel_all"> Выбрать все</button><button onclick="tgGrp(false)" class="btn-g" style="flex:1; justify-content:center" data-i18n="btn_sel_none"> Снять все</button></div>
<div id="g-cnt" class="g-list"></div> <div id="g-cnt" class="g-list"></div>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)"><button onclick="applyVless()" class="btn-s" style="flex:1;justify-content:center">Добавить</button><button onclick="closeM('m-grp')" class="btn-g" style="flex:1;justify-content:center">Отмена</button></div></div></div> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)"><button onclick="applyVless()" class="btn-s" style="flex:1;justify-content:center" data-i18n="btn_add">Добавить</button><button onclick="closeM('m-grp')" class="btn-g" style="flex:1;justify-content:center" data-i18n="btn_cancel">Отмена</button></div></div></div>
<div id="m-del" class="ovl"><div class="mod"><h3>Удалить прокси</h3><select id="sel-del"></select><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="doDel()" class="btn-d">Удалить</button><button onclick="closeM('m-del')" class="btn-g">Отмена</button></div></div></div> <div id="m-del" class="ovl"><div class="mod"><h3 data-i18n="modal_del_proxy">Удалить прокси</h3><select id="sel-del"></select><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="doDel()" class="btn-d" data-i18n="delete">Удалить</button><button onclick="closeM('m-del')" class="btn-g" data-i18n="btn_cancel">Отмена</button></div></div></div>
<div id="m-con" class="ovl"><div class="mod"><h3>Консоль</h3><div id="cons">...</div><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="location.reload()" class="btn-s">Обновить</button><button onclick="closeM('m-con')" class="btn-g">Закрыть</button></div></div></div> <div id="m-con" class="ovl"><div class="mod"><h3 data-i18n="modal_console">Консоль</h3><div id="cons">...</div><div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"><button onclick="location.reload()" class="btn-s" data-i18n="btn_update">Обновить</button><button onclick="closeM('m-con')" class="btn-g" data-i18n="btn_close">Закрыть</button></div></div></div>
<div id="m-ren" class="ovl"><div class="mod"> <div id="m-ren" class="ovl"><div class="mod">
<h3>Переименовать прокси</h3> <h3 data-i18n="modal_ren_proxy">Переименовать прокси</h3>
<p style="margin-top:0;font-size:13px;color:var(--txt-sec)">Выберите прокси для переименования:</p> <p style="margin-top:0;font-size:13px;color:var(--txt-sec)" data-i18n="lbl_sel_ren">Выберите прокси для переименования:</p>
<select id="sel-ren-proxy"></select> <select id="sel-ren-proxy"></select>
<p style="margin-top:15px;font-size:13px;color:var(--txt-sec)">Новое имя:</p> <p style="margin-top:15px;font-size:13px;color:var(--txt-sec)" data-i18n="lbl_new_name">Новое имя:</p>
<input id="inp-ren-newname" placeholder="Введите новое имя"> <input id="inp-ren-newname" placeholder="Введите новое имя" data-i18n-ph="ph_new_name">
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px">
<button onclick="doRename()" class="btn-s">Переименовать</button> <button onclick="doRename()" class="btn-s" data-i18n="btn_rename">Переименовать</button>
<button onclick="closeM('m-ren')" class="btn-g">Отмена</button> <button onclick="closeM('m-ren')" class="btn-g" data-i18n="btn_cancel">Отмена</button>
</div> </div>
</div></div> </div></div>
<div id="m-add-prof" class="ovl"><div class="mod"> <div id="m-add-prof" class="ovl"><div class="mod">
<h3>Новый профиль</h3> <h3 data-i18n="modal_new_prof">Новый профиль</h3>
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя (англ, без пробелов):</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_prof_name">Имя (англ, без пробелов):</label>
<input id="np-name" placeholder="my_config" style="margin-bottom:10px"> <input id="np-name" placeholder="my_config" style="margin-bottom:10px">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Содержимое:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_content">Содержимое:</label>
<div style="display:flex; gap:5px; margin-bottom:5px"> <div style="display:flex; gap:5px; margin-bottom:5px">
<button onclick="document.getElementById('np-file').click()" class="btn-u" style="flex:1;justify-content:center">📂 Загрузить файл</button> <button onclick="document.getElementById('np-file').click()" class="btn-u" style="flex:1;justify-content:center" data-i18n="btn_load_file">📂 Загрузить файл</button>
</div> </div>
<input type="file" id="np-file" style="display:none" onchange="loadProfFile(this)"> <input type="file" id="np-file" style="display:none" onchange="loadProfFile(this)">
<textarea id="np-content" rows="10" placeholder="Вставьте YAML конфиг сюда..."></textarea> <textarea id="np-content" rows="10" placeholder="Вставьте YAML конфиг сюда..." data-i18n-ph="ph_paste_yaml"></textarea>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px">
<button onclick="saveNewProf()" class="btn-s">Сохранить</button> <button onclick="saveNewProf()" class="btn-s" data-i18n="btn_save">Сохранить</button>
<button onclick="closeM('m-add-prof')" class="btn-g">Отмена</button> <button onclick="closeM('m-add-prof')" class="btn-g" data-i18n="btn_cancel">Отмена</button>
</div> </div>
</div></div> </div></div>
<div id="addProxyModal" class="ovl"><div class="mod"> <div id="addProxyModal" class="ovl"><div class="mod">
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--bd); padding-bottom:10px; margin-bottom:0;"> <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--bd); padding-bottom:10px; margin-bottom:0;">
<h3 id="proxyModalTitle" style="margin:0; padding:0; border:0;">Добавить прокси</h3> <h3 id="proxyModalTitle" style="margin:0; padding:0; border:0;" data-i18n="modal_add_proxy">Добавить прокси</h3>
<button onclick="closeM('addProxyModal')" style="width:32px; height:32px; padding:0; background:var(--bg-ter); color:var(--txt); font-size:18px;"></button> <button onclick="closeM('addProxyModal')" style="width:32px; height:32px; padding:0; background:var(--bg-ter); color:var(--txt); font-size:18px;"></button>
</div> </div>
<div id="edit-proxy-container" style="display:none; margin-bottom:10px;"> <div id="edit-proxy-container" style="display:none; margin-bottom:10px;">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Выберите прокси для изменения:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_select_edit">Выберите прокси для изменения:</label>
<select id="edit-proxy-sel"></select> <select id="edit-proxy-sel"></select>
<div style="font-size:11px; color:var(--btn-u); margin-top:5px;"> Данные этого прокси будут полностью заменены новыми!</div> <div style="font-size:11px; color:var(--btn-u); margin-top:5px;" data-i18n="warn_edit"> Данные этого прокси будут полностью заменены новыми!</div>
</div> </div>
<div class="modal-tabs"> <div class="modal-tabs">
<button class="active" onclick="switchTab(event, 'vlessTab')">VLESS</button> <button class="active" onclick="switchTab(event, 'vlessTab')" data-i18n="tab_vless">VLESS</button>
<button onclick="switchTab(event, 'wgTab')">WireGuard</button> <button onclick="switchTab(event, 'wgTab')" data-i18n="tab_wg">WireGuard</button>
</div> </div>
<div id="vlessTab" class="tab-content active"> <div id="vlessTab" class="tab-content active">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Ссылка VLESS:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_vless_link">Ссылка VLESS:</label>
<input id="vlessLink" placeholder="vless://..." style="margin-bottom:10px;"> <input id="vlessLink" placeholder="vless://..." style="margin-bottom:10px;">
<div id="vless-name-block"> <div id="vless-name-block">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_proxy_name">Имя прокси (необязательно):</label>
<input id="vlessProxyName" placeholder="Автоматически из ссылки" style="margin-bottom:10px;"> <input id="vlessProxyName" placeholder="Автоматически из ссылки" style="margin-bottom:10px;">
</div> </div>
<button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;">Сохранить</button> <button onclick="parseVless()" class="btn-s" style="width:100%; justify-content:center;" data-i18n="btn_save">Сохранить</button>
</div> </div>
<div id="wgTab" class="tab-content"> <div id="wgTab" class="tab-content">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Конфигурация WireGuard:</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_wg_conf">Конфигурация WireGuard:</label>
<textarea id="wgConfig" rows="8" placeholder="Вставьте содержимое .conf файла сюда..." style="width:100%; margin-bottom:10px;"></textarea> <textarea id="wgConfig" rows="8" placeholder="Вставьте содержимое .conf файла сюда..." style="width:100%; margin-bottom:10px;"></textarea>
<div id="wg-name-block"> <div id="wg-name-block">
<label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)">Имя прокси (необязательно):</label> <label style="font-size:12px; margin-bottom:5px; color:var(--txt-sec)" data-i18n="lbl_proxy_name">Имя прокси (необязательно):</label>
<input id="wgProxyName" placeholder="Автоматически из Endpoint" style="margin-bottom:10px;"> <input id="wgProxyName" placeholder="Автоматически из Endpoint" style="margin-bottom:10px;">
</div> </div>
<input type="file" id="wgFile" accept=".conf" style="display:none" onchange="loadWgFile(this)"> <input type="file" id="wgFile" accept=".conf" style="display:none" onchange="loadWgFile(this)">
<button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;">📂 Или загрузить .conf файл</button> <button onclick="document.getElementById('wgFile').click()" class="btn-u" style="width:100%; justify-content:center; margin-bottom:10px;" data-i18n="btn_load_file">📂 Или загрузить .conf файл</button>
<button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;">Сохранить</button> <button onclick="addWireguard()" class="btn-s" style="width:100%; justify-content:center;" data-i18n="btn_save">Сохранить</button>
</div> </div>
</div></div> </div></div>
<div id="m-view-bk" class="ovl"><div class="mod"> <div id="m-view-bk" class="ovl"><div class="mod">
<h3>Просмотр бэкапа</h3> <h3 data-i18n="modal_view_bk">Просмотр бэкапа</h3>
<pre id="bk-content" style="flex-grow:1; overflow-y:auto; min-height: 200px; max-height:60vh;"></pre> <pre id="bk-content" style="flex-grow:1; overflow-y:auto; min-height: 200px; max-height:60vh;"></pre>
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)"> <div style="display:flex;justify-content:flex-end;gap:10px;margin-top:15px;padding-top:10px;border-top:1px solid var(--bd)">
<button id="bk-restore-btn" class="btn-r">Восстановить</button> <button id="bk-restore-btn" class="btn-r" data-i18n="btn_restore">Восстановить</button>
<button onclick="closeM('m-view-bk')" class="btn-g">Закрыть</button> <button onclick="closeM('m-view-bk')" class="btn-g" data-i18n="btn_close">Закрыть</button>
</div> </div>
</div></div> </div></div>
<script> <script>
var ed=ace.edit("ed");ed.setTheme("ace/theme/monokai");ed.session.setMode("ace/mode/yaml");ed.setOptions({fontSize:14,tabSize:2,useSoftTabs:true}); var ed=ace.edit("ed");ed.setTheme("ace/theme/monokai");ed.session.setMode("ace/mode/yaml");ed.setOptions({fontSize:14,tabSize:2,useSoftTabs:true});
var pData=null, GRP_KEY="mihomo_grp_sel", LIM_KEY="mihomo_bk_lim", THM_KEY="mihomo_theme"; var pData=null, GRP_KEY="mihomo_grp_sel", LIM_KEY="mihomo_bk_lim", THM_KEY="mihomo_theme", LANG_KEY="mihomo_lang";
var initialConfig = __JSON_CONTENT__; var initialConfig = __JSON_CONTENT__;
var isEditMode = false; var isEditMode = false;
var currLang = 'ru';
const TR = {
ru: {
title: "Mihomo Studio",
save: "💾 Сохранить",
restart: "🚀 Рестарт",
panel: "🌐 Панель",
profiles: "Профили",
create: " Создать",
delete: "🗑 Удалить",
select: "",
download: "💾",
proxy_mgmt: "Управление",
add: " Добавить",
edit: "✏️ Заменить",
rename: "Переименовать",
backups: "Бэкапы",
clean: "Очистить",
keep: "Оставить:",
theme_dark: "🌑 Dark",
theme_light: "☀️ Light",
theme_midnight: "🌃 Midnight",
theme_cyber: "👾 Cyber",
toast_saved: "✅ Успешно сохранено",
toast_cleaned: "🧹 Очищено",
toast_deleted: "🗑 Удалено",
toast_restored: "♻️ Восстановлено",
toast_added: "✅ Добавлено",
toast_renamed: "✏️ Прокси переименован",
toast_updated: "✏️ Данные прокси обновлены",
confirm_switch: "Переключиться на профиль {0}?",
confirm_del_prof: "Удалить профиль {0}? Это действие необратимо.",
confirm_del_bk: "Удалить бэкап {0}?",
confirm_clean: "Оставить только {0} последних бэкапов?",
confirm_restore: "Восстановить {0}? Текущий конфиг будет перезаписан.",
confirm_del_proxy: "Удалить?",
confirm_replace: "Заменить данные прокси '{0}'?",
prompt_enter_name: "Введите имя!",
error_invalid_name: "Недопустимое имя!",
error_exists: "Профиль с таким именем уже существует",
error_no_proxy_edit: "Выберите прокси для редактирования",
error_empty_wg: "Конфигурація WireGuard не може бути порожньою.",
modal_add_proxy: "Добавить прокси",
modal_edit_proxy: "Изменить прокси",
lbl_vless_link: "Ссылка VLESS:",
lbl_proxy_name: "Имя прокси (необязательно):",
lbl_wg_conf: "Конфигурация WireGuard:",
btn_add: "Добавить",
btn_save: "Сохранить",
btn_cancel: "Отмена",
btn_restore: "Восстановить",
btn_close: "Закрыть",
btn_update: "Обновить",
tab_vless: "VLESS",
tab_wg: "WireGuard",
lbl_select_edit: "Выберите прокси для изменения:",
warn_edit: "⚠️ Данные этого прокси будут полностью заменены новыми!",
modal_new_prof: "Новый профиль",
lbl_prof_name: "Имя (англ, без пробелов):",
lbl_content: "Содержимое:",
btn_load_file: "📂 Загрузить файл",
ph_paste_yaml: "Вставьте YAML конфиг сюда...",
modal_groups: "Добавить в группы:",
btn_sel_all: "☑ Выбрать все",
btn_sel_none: "☐ Снять все",
modal_del_proxy: "Удалить прокси",
modal_ren_proxy: "Переименовать прокси",
lbl_sel_ren: "Выберите прокси для переименования:",
lbl_new_name: "Новое имя:",
ph_new_name: "Введите новое имя",
btn_rename: "Переименовать",
modal_console: "Консоль",
modal_view_bk: "Просмотр бэкапа",
log_loading: "⏳ Выполнение xkeen -restart...",
last_load: "Загружено:",
last_saved: "Сохранено:"
},
uk: {
title: "Mihomo Studio",
save: "💾 Зберегти",
restart: "🚀 Рестарт",
panel: "🌐 Панель",
profiles: "Профілі",
create: " Створити",
delete: "🗑 Видалити",
select: "",
download: "💾",
proxy_mgmt: "Керування",
add: " Додати",
edit: "✏️ Замінити",
rename: "Перейменувати",
backups: "Бекапи",
clean: "Очистити",
keep: "Залишити:",
theme_dark: "🌑 Темна",
theme_light: "☀️ Світла",
theme_midnight: "🌃 Північ",
theme_cyber: "👾 Кібер",
toast_saved: "✅ Успішно збережено",
toast_cleaned: "🧹 Очищено",
toast_deleted: "🗑 Видалено",
toast_restored: "♻️ Відновлено",
toast_added: "✅ Додано",
toast_renamed: "✏️ Проксі перейменовано",
toast_updated: "✏️ Дані проксі оновлено",
confirm_switch: "Переключитися на профіль {0}?",
confirm_del_prof: "Видалити профіль {0}? Ця дія незворотна.",
confirm_del_bk: "Видалити бекап {0}?",
confirm_clean: "Залишити тільки {0} останніх бекапів?",
confirm_restore: "Відновити {0}? Поточний конфіг буде перезаписано.",
confirm_del_proxy: "Видалити?",
confirm_replace: "Замінити дані проксі '{0}'?",
prompt_enter_name: "Введіть ім'я!",
error_invalid_name: "Неприпустиме ім'я!",
error_exists: "Профіль з таким ім'ям вже існує",
error_no_proxy_edit: "Виберіть проксі для редагування",
error_empty_wg: "Конфігурація WireGuard не може бути порожньою.",
modal_add_proxy: "Додати проксі",
modal_edit_proxy: "Змінити проксі",
lbl_vless_link: "Посилання VLESS:",
lbl_proxy_name: "Ім'я проксі (необов'язково):",
lbl_wg_conf: "Конфігурація WireGuard:",
btn_add: "Додати",
btn_save: "Зберегти",
btn_cancel: "Скасувати",
btn_restore: "Відновити",
btn_close: "Закрити",
btn_update: "Оновити",
tab_vless: "VLESS",
tab_wg: "WireGuard",
lbl_select_edit: "Виберіть проксі для зміни:",
warn_edit: "⚠️ Дані цього проксі будуть повністю замінені новими!",
modal_new_prof: "Новий профіль",
lbl_prof_name: "Ім'я (англ, без пробілів):",
lbl_content: "Вміст:",
btn_load_file: "📂 Завантажити файл",
ph_paste_yaml: "Вставте YAML конфіг сюди...",
modal_groups: "Додати в групи:",
btn_sel_all: "☑ Обрати всі",
btn_sel_none: "☐ Зняти всі",
modal_del_proxy: "Видалити проксі",
modal_ren_proxy: "Перейменувати проксі",
lbl_sel_ren: "Виберіть проксі для перейменування:",
lbl_new_name: "Нове ім'я:",
ph_new_name: "Введіть нове ім'я",
btn_rename: "Перейменувати",
modal_console: "Консоль",
modal_view_bk: "Перегляд бекапу",
log_loading: "⏳ Виконання xkeen -restart...",
last_load: "Завантажено:",
last_saved: "Збережено:"
},
en: {
title: "Mihomo Studio",
save: "💾 Save",
restart: "🚀 Restart",
panel: "🌐 Panel",
profiles: "Profiles",
create: " Create",
delete: "🗑 Delete",
select: "",
download: "💾",
proxy_mgmt: "Management",
add: " Add",
edit: "✏️ Replace",
rename: "Rename",
backups: "Backups",
clean: "Clean",
keep: "Keep:",
theme_dark: "🌑 Dark",
theme_light: "☀️ Light",
theme_midnight: "🌃 Midnight",
theme_cyber: "👾 Cyber",
toast_saved: "✅ Saved successfully",
toast_cleaned: "🧹 Cleaned",
toast_deleted: "🗑 Deleted",
toast_restored: "♻️ Restored",
toast_added: "✅ Added",
toast_renamed: "✏️ Proxy renamed",
toast_updated: "✏️ Proxy data updated",
confirm_switch: "Switch to profile {0}?",
confirm_del_prof: "Delete profile {0}? This action is irreversible.",
confirm_del_bk: "Delete backup {0}?",
confirm_clean: "Keep only the last {0} backups?",
confirm_restore: "Restore {0}? Current config will be overwritten.",
confirm_del_proxy: "Delete?",
confirm_replace: "Replace data for proxy '{0}'?",
prompt_enter_name: "Enter name!",
error_invalid_name: "Invalid name!",
error_exists: "Profile with this name already exists",
error_no_proxy_edit: "Select a proxy to edit",
error_empty_wg: "WireGuard configuration cannot be empty.",
modal_add_proxy: "Add Proxy",
modal_edit_proxy: "Edit Proxy",
lbl_vless_link: "VLESS Link:",
lbl_proxy_name: "Proxy Name (optional):",
lbl_wg_conf: "WireGuard Config:",
btn_add: "Add",
btn_save: "Save",
btn_cancel: "Cancel",
btn_restore: "Restore",
btn_close: "Close",
btn_update: "Update",
tab_vless: "VLESS",
tab_wg: "WireGuard",
lbl_select_edit: "Select proxy to replace:",
warn_edit: "⚠️ This proxy's data will be fully replaced!",
modal_new_prof: "New Profile",
lbl_prof_name: "Name (English, no spaces):",
lbl_content: "Content:",
btn_load_file: "📂 Upload File",
ph_paste_yaml: "Paste YAML config here...",
modal_groups: "Add to groups:",
btn_sel_all: "☑ Select All",
btn_sel_none: "☐ Select None",
modal_del_proxy: "Delete Proxy",
modal_ren_proxy: "Rename Proxy",
lbl_sel_ren: "Select proxy to rename:",
lbl_new_name: "New Name:",
ph_new_name: "Enter new name",
btn_rename: "Rename",
modal_console: "Console",
modal_view_bk: "View Backup",
log_loading: "⏳ Running xkeen -restart...",
last_load: "Loaded:",
last_saved: "Saved:"
}
};
function t(k, ...args) {
let s = TR[currLang][k] || k;
args.forEach((a, i) => s = s.replace('{'+i+'}', a));
return s;
}
function setLang(l) {
currLang = l;
localStorage.setItem(LANG_KEY, l);
document.getElementById('lang-sel').value = l;
document.querySelectorAll('[data-i18n]').forEach(e => {
let k = e.getAttribute('data-i18n');
if(TR[l][k]) e.innerText = TR[l][k];
});
document.querySelectorAll('[data-i18n-ph]').forEach(e => {
let k = e.getAttribute('data-i18n-ph');
if(TR[l][k]) e.placeholder = TR[l][k];
});
// Update dynamic parts
if(isEditMode) document.getElementById('proxyModalTitle').innerText = TR[l].modal_edit_proxy;
else document.getElementById('proxyModalTitle').innerText = TR[l].modal_add_proxy;
}
// Открываем панель через наш локальный прокси (безопасно для PNA/CORS) // Открываем панель через наш локальный прокси (безопасно для PNA/CORS)
function openPanel() { function openPanel() {
@ -708,7 +924,7 @@ function openPanel() {
ed.setValue(initialConfig); ed.clearSelection(); ed.setValue(initialConfig); ed.clearSelection();
document.getElementById('vlessLink').addEventListener('input', function() { document.getElementById('vlessLink').addEventListener('input', function() {
if(isEditMode) return; // В режиме редактирования имя берется из селекта if(isEditMode) return;
var link = this.value; var link = this.value;
if (link.startsWith("vless://") && link.includes("#")) { if (link.startsWith("vless://") && link.includes("#")) {
var name = link.split('#')[1]; var name = link.split('#')[1];
@ -728,11 +944,11 @@ document.getElementById('wgConfig').addEventListener('input', function() {
}); });
function closeM(i){document.getElementById(i).style.display='none'} function closeM(i){document.getElementById(i).style.display='none'}
function showToast(msg){ var t=document.getElementById('toast'); t.innerText=msg||'✅ Выполнено'; t.style.display='block'; setTimeout(()=>{t.style.display='none'}, 2000); } function showToast(msg){ var tBox=document.getElementById('toast'); tBox.innerText=msg||t('toast_saved'); tBox.style.display='block'; setTimeout(()=>{tBox.style.display='none'}, 2000); }
function switchProf() { function switchProf() {
var p = document.getElementById('prof-sel').value; var p = document.getElementById('prof-sel').value;
if(!confirm("Переключиться на профиль " + p + "?")) return; if(!confirm(t('confirm_switch', p))) return;
var params = new URLSearchParams(); params.append('act', 'switch_prof'); params.append('name', p); var params = new URLSearchParams(); params.append('act', 'switch_prof'); params.append('name', p);
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
if(d.error) alert(d.error); if(d.error) alert(d.error);
@ -752,22 +968,22 @@ function loadProfFile(input) {
function saveNewProf() { function saveNewProf() {
var n = document.getElementById('np-name').value.trim(); var n = document.getElementById('np-name').value.trim();
var c = document.getElementById('np-content').value; var c = document.getElementById('np-content').value;
if(!n) return alert("Введите имя!"); if(!n) return alert(t('prompt_enter_name'));
if(!n.match(/^[a-zA-Z0-9_-]+$/)) return alert("Недопустимое имя!"); if(!n.match(/^[a-zA-Z0-9_-]+$/)) return alert(t('error_invalid_name'));
var params = new URLSearchParams(); params.append('act', 'add_prof'); params.append('name', n); params.append('content', c); var params = new URLSearchParams(); params.append('act', 'add_prof'); params.append('name', n); params.append('content', c);
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
if(d.error) alert(d.error); if(d.error) alert(d.error);
else { showToast("✅ Профиль создан"); setTimeout(()=>{window.location.reload()}, 500); } else { showToast(t('toast_saved')); setTimeout(()=>{window.location.reload()}, 500); }
}); });
} }
function delProf() { function delProf() {
var p = document.getElementById('prof-sel').value; var p = document.getElementById('prof-sel').value;
if(!p) return; if(!p) return;
if(!confirm("Удалить профиль " + p + "? Это действие необратимо.")) return; if(!confirm(t('confirm_del_prof', p))) return;
var params = new URLSearchParams(); params.append('act', 'del_prof'); params.append('name', p); var params = new URLSearchParams(); params.append('act', 'del_prof'); params.append('name', p);
fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:params}).then(r=>r.json()).then(d=>{
if(d.error) alert(d.error); if(d.error) alert(d.error);
else { showToast("🗑 Удалено"); setTimeout(()=>{window.location.reload()}, 500); } else { showToast(t('toast_deleted')); setTimeout(()=>{window.location.reload()}, 500); }
}); });
} }
@ -790,7 +1006,7 @@ function downloadProf() {
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
showToast('📄 Загрузка началась'); showToast('💾');
} }
}); });
} }
@ -829,6 +1045,9 @@ function setTheme(t) {
var savedTheme = localStorage.getItem(THM_KEY) || 'dark'; var savedTheme = localStorage.getItem(THM_KEY) || 'dark';
setTheme(savedTheme); setTheme(savedTheme);
var savedLang = localStorage.getItem(LANG_KEY) || 'ru';
setLang(savedLang);
var bkInp = document.getElementById('bk-lim'); var bkInp = document.getElementById('bk-lim');
if(localStorage.getItem(LIM_KEY)) bkInp.value = localStorage.getItem(LIM_KEY); if(localStorage.getItem(LIM_KEY)) bkInp.value = localStorage.getItem(LIM_KEY);
bkInp.addEventListener('change', function(){ localStorage.setItem(LIM_KEY, this.value); }); bkInp.addEventListener('change', function(){ localStorage.setItem(LIM_KEY, this.value); });
@ -838,12 +1057,12 @@ function save(mode){
var p=new URLSearchParams(); p.append('act', mode); p.append('content', c); var p=new URLSearchParams(); p.append('act', mode); p.append('content', c);
if(mode==='restart') { if(mode==='restart') {
document.getElementById('m-con').style.display='flex'; document.getElementById('m-con').style.display='flex';
document.getElementById('cons').innerHTML='<div style="padding:20px;text-align:center">⏳ Выполнение xkeen -restart...</div>'; document.getElementById('cons').innerHTML='<div style="padding:20px;text-align:center">' + t('log_loading') + '</div>';
} }
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
if(mode==='save'){ if(mode==='save'){
showToast("✅ Сохранено"); showToast(t('toast_saved'));
document.getElementById('last-load').innerText = "Saved: " + d.time; document.getElementById('last-load').innerText = t('last_saved') + " " + d.time;
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups; if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
} else { } else {
var consoleDiv = document.getElementById('cons'); var consoleDiv = document.getElementById('cons');
@ -855,19 +1074,19 @@ function save(mode){
function cleanBackups(){ function cleanBackups(){
var lim = document.getElementById('bk-lim').value; var lim = document.getElementById('bk-lim').value;
if(!confirm('Оставить только ' + lim + ' последних бэкапов?')) return; if(!confirm(t('confirm_clean', lim))) return;
var p=new URLSearchParams(); p.append('act', 'clean_backups'); p.append('limit', lim); var p=new URLSearchParams(); p.append('act', 'clean_backups'); p.append('limit', lim);
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
showToast("🧹 Очищено"); showToast(t('toast_cleaned'));
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups; if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
}); });
} }
function delBackup(fname){ function delBackup(fname){
if(!confirm('Удалить бэкап ' + fname + '?')) return; if(!confirm(t('confirm_del_bk', fname))) return;
var p=new URLSearchParams(); p.append('act', 'del_backup'); p.append('f', fname); var p=new URLSearchParams(); p.append('act', 'del_backup'); p.append('f', fname);
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{ fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{
showToast("🗑 Удалено"); showToast(t('toast_deleted'));
if(d.backups) document.getElementById('bk-list').innerHTML = d.backups; if(d.backups) document.getElementById('bk-list').innerHTML = d.backups;
}); });
} }
@ -888,7 +1107,7 @@ function viewBackup(fname) {
} }
function restoreBackup(fname){ function restoreBackup(fname){
if(!confirm('Восстановить ' + fname + '? Текущий конфиг будет перезаписан.')) return; if(!confirm(t('confirm_restore', fname))) return;
var p=new URLSearchParams(); p.append('act', 'rest'); p.append('f', fname); var p=new URLSearchParams(); p.append('act', 'rest'); p.append('f', fname);
fetch('/',{method:'POST',body:p}).then(r=>r.text()).then(()=>{ fetch('/',{method:'POST',body:p}).then(r=>r.text()).then(()=>{
window.location.reload(); window.location.reload();
@ -911,7 +1130,7 @@ function getProxiesList() {
function openAddProxyModal() { function openAddProxyModal() {
isEditMode = false; isEditMode = false;
document.getElementById('proxyModalTitle').innerText = "Добавить прокси"; document.getElementById('proxyModalTitle').innerText = t('modal_add_proxy');
document.getElementById('edit-proxy-container').style.display = 'none'; document.getElementById('edit-proxy-container').style.display = 'none';
document.getElementById('vless-name-block').style.display = 'block'; document.getElementById('vless-name-block').style.display = 'block';
document.getElementById('wg-name-block').style.display = 'block'; document.getElementById('wg-name-block').style.display = 'block';
@ -927,7 +1146,7 @@ function openAddProxyModal() {
function openEditProxyModal() { function openEditProxyModal() {
isEditMode = true; isEditMode = true;
document.getElementById('proxyModalTitle').innerText = "Изменить прокси"; document.getElementById('proxyModalTitle').innerText = t('modal_edit_proxy');
document.getElementById('edit-proxy-container').style.display = 'block'; document.getElementById('edit-proxy-container').style.display = 'block';
document.getElementById('vless-name-block').style.display = 'none'; document.getElementById('vless-name-block').style.display = 'none';
document.getElementById('wg-name-block').style.display = 'none'; document.getElementById('wg-name-block').style.display = 'none';
@ -938,7 +1157,7 @@ function openEditProxyModal() {
sel.innerHTML = ''; sel.innerHTML = '';
if(prs.length === 0) { if(prs.length === 0) {
var o = document.createElement('option'); var o = document.createElement('option');
o.text = "Нет прокси для редактирования"; o.text = "---";
sel.add(o); sel.add(o);
sel.disabled = true; sel.disabled = true;
} else { } else {
@ -990,12 +1209,12 @@ function addWireguard() {
if(isEditMode) { if(isEditMode) {
name = document.getElementById('edit-proxy-sel').value; name = document.getElementById('edit-proxy-sel').value;
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert("Выберите прокси для редактирования"); if(!name || document.getElementById('edit-proxy-sel').disabled) return alert(t('error_no_proxy_edit'));
} else { } else {
name = document.getElementById('wgProxyName').value.trim(); name = document.getElementById('wgProxyName').value.trim();
} }
if (!conf) return alert("Конфигурация WireGuard не может быть пустой."); if (!conf) return alert(t('error_empty_wg'));
var p = new URLSearchParams(); var p = new URLSearchParams();
p.append('act', 'add_wireguard'); p.append('act', 'add_wireguard');
@ -1025,7 +1244,7 @@ function parseVless(){
if(isEditMode) { if(isEditMode) {
name = document.getElementById('edit-proxy-sel').value; name = document.getElementById('edit-proxy-sel').value;
if(!name || document.getElementById('edit-proxy-sel').disabled) return alert("Выберите прокси для редактирования"); if(!name || document.getElementById('edit-proxy-sel').disabled) return alert(t('error_no_proxy_edit'));
} else { } else {
name = document.getElementById('vlessProxyName').value.trim(); name = document.getElementById('vlessProxyName').value.trim();
} }
@ -1055,7 +1274,7 @@ function parseVless(){
} }
function replaceProxyData(targetName, newYaml) { function replaceProxyData(targetName, newYaml) {
if(!confirm("Заменить данные прокси '" + targetName + "'?")) return; if(!confirm(t('confirm_replace', targetName))) return;
var content = ed.getValue(); var content = ed.getValue();
var p = new URLSearchParams(); var p = new URLSearchParams();
p.append('act', 'replace_proxy'); p.append('act', 'replace_proxy');
@ -1072,7 +1291,7 @@ function replaceProxyData(targetName, newYaml) {
ed.setValue(d.new_content); ed.setValue(d.new_content);
ed.clearSelection(); ed.clearSelection();
closeM('addProxyModal'); closeM('addProxyModal');
showToast("✏️ Данные прокси обновлены"); showToast(t('toast_updated'));
} }
}); });
} }
@ -1094,7 +1313,7 @@ function applyVless(){
document.querySelectorAll('#g-cnt input:checked').forEach(c=>sels.push(c.value)); document.querySelectorAll('#g-cnt input:checked').forEach(c=>sels.push(c.value));
localStorage.setItem(GRP_KEY, JSON.stringify(sels)); localStorage.setItem(GRP_KEY, JSON.stringify(sels));
var p=new URLSearchParams(); p.append('act','apply_insert'); p.append('content',txt); p.append('proxy_name',pData.name); p.append('proxy_yaml',pData.yaml); p.append('targets',JSON.stringify(sels)); var p=new URLSearchParams(); p.append('act','apply_insert'); p.append('content',txt); p.append('proxy_name',pData.name); p.append('proxy_yaml',pData.yaml); p.append('targets',JSON.stringify(sels));
fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{if(d.error)alert(d.error);else{ed.setValue(d.new_content);ed.clearSelection();showToast("✅ Добавлено")}}); fetch('/',{method:'POST',body:p}).then(r=>r.json()).then(d=>{if(d.error)alert(d.error);else{ed.setValue(d.new_content);ed.clearSelection();showToast(t('toast_added'))}});
} }
function showDel(){ function showDel(){
var prs = getProxiesList(); var prs = getProxiesList();
@ -1103,7 +1322,7 @@ function showDel(){
document.getElementById('m-del').style.display='flex'; document.getElementById('m-del').style.display='flex';
} }
function doDel(){ function doDel(){
var nm=document.getElementById('sel-del').value;if(!nm)return;if(!confirm('Удалить?'))return;closeM('m-del'); var nm=document.getElementById('sel-del').value;if(!nm)return;if(!confirm(t('confirm_del_proxy')))return;closeM('m-del');
var ls=ed.getValue().split(/\\r?\\n/); var nls=[], inP=false, delB=false, bInd=-1; var ls=ed.getValue().split(/\\r?\\n/); var nls=[], inP=false, delB=false, bInd=-1;
for(var l of ls){ for(var l of ls){
if(l.match(/^proxies:/)){inP=true;nls.push(l);continue} if(inP && l.match(/^[a-zA-Z]/) && !l.match(/^proxies:/)){inP=false;delB=false} if(l.match(/^proxies:/)){inP=true;nls.push(l);continue} if(inP && l.match(/^[a-zA-Z]/) && !l.match(/^proxies:/)){inP=false;delB=false}
@ -1179,7 +1398,7 @@ function doRename() {
ed.setValue(d.new_content); ed.setValue(d.new_content);
ed.clearSelection(); ed.clearSelection();
closeM('m-ren'); closeM('m-ren');
showToast("✏️ Прокси переименован!"); showToast(t('toast_renamed'));
} }
}) })
.catch(e => alert("Сетевая ошибка: " + e)); .catch(e => alert("Сетевая ошибка: " + e));