refactor: удаление неиспользуемого автоподбора Zapret2 и чистка файлов локализации

This commit is contained in:
Dushnilin
2026-07-30 16:42:30 +03:00
parent f3d8c5a264
commit b057e0a48d
28 changed files with 3319 additions and 3331 deletions
+3 -19
View File
@@ -90,11 +90,6 @@ async function generatePo() {
const calls = JSON.parse(callsRaw);
const oldTranslations = parsePo(oldPoRaw);
const header = getHeader(lang);
const tsKeys = new Set(calls.map(({ key }) => key));
// TypeScript-sourced strings (always present)
const body = calls
.map(({ key }) => {
const msgid = key;
@@ -107,23 +102,12 @@ async function generatePo() {
})
.join('\n');
// Extra strings from old PO (static JS files: settings.js, section.js, etc.)
// Preserved so that locales:actualize doesn't wipe them.
const extraBody = [...oldTranslations.entries()]
.filter(([msgid]) => msgid !== '' && !tsKeys.has(msgid))
.map(([msgid, msgstr]) => [
`msgid "${escapePoString(msgid)}"`,
`msgstr "${escapePoString(msgstr)}"`,
''
].join('\n'))
.join('\n');
const finalPo = header.join('\n') + '\n' + body + (extraBody ? '\n' + extraBody : '');
const header = getHeader(lang);
const finalPo = header.join('\n') + '\n' + body;
await fs.writeFile(poPath, finalPo, 'utf8');
const translated = [...oldTranslations.values()].filter(v => v !== '').length;
const total = tsKeys.size + (oldTranslations.size - 1); // -1 for header entry
console.log(`✅ Файл ${poPath} успешно сгенерирован. Переведено ${[...oldTranslations.keys()].length}/${calls.length}`);
console.log(`✅ Файл ${poPath} успешно сгенерирован. Переведено ${translated}/${calls.length}`);
}
generatePo().catch((err) => {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+94 -362
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-28 21:47+0300\n"
"PO-Revision-Date: 2026-07-28 21:47+0300\n"
"POT-Creation-Date: 2026-07-30 16:42+0300\n"
"PO-Revision-Date: 2026-07-30 16:42+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -20,8 +20,8 @@ msgstr ""
msgid "-- Select --"
msgstr "-- Выбрать --"
msgid ".lst списки"
msgstr ""
msgid ".lst Lists"
msgstr ".lst списки"
msgid "+ Add JSON outbound"
msgstr "+ Добавить JSON outbound"
@@ -54,7 +54,7 @@ msgid "✔ Running"
msgstr "✔ Работает"
msgid "✘ Stopped"
msgstr "✘ Остановлен"
msgstr "✘ Остановлено"
msgid "Выберите файл с расширением .conf"
msgstr "Выберите файл с расширением .conf"
@@ -62,12 +62,6 @@ msgstr "Выберите файл с расширением .conf"
msgid "Готово"
msgstr "Готово"
msgid "Действие секции"
msgstr ""
msgid "Домены (Domains)"
msgstr ""
msgid "Загружено!"
msgstr "Загружено!"
@@ -77,21 +71,12 @@ msgstr "Загрузить .conf"
msgid "Загрузить конфиг .conf"
msgstr "Загрузить конфиг .conf"
msgid "Закрыть"
msgstr ""
msgid "Импортировать настройки AmneziaWG из файла .conf"
msgstr "Импортировать настройки AmneziaWG из файла .conf"
msgid "Конфиг AmneziaWG успешно загружен!"
msgstr "Конфиг AmneziaWG успешно загружен!"
msgid "Наборы правил (.srs / .json)"
msgstr ""
msgid "Не заданы"
msgstr ""
msgid "Не удалось разобрать файл:"
msgstr ""
@@ -101,21 +86,6 @@ msgstr ""
msgid "Ошибка чтения"
msgstr "Ошибка чтения"
msgid "Порты"
msgstr ""
msgid "Правила"
msgstr ""
msgid "Содержимое правил секции"
msgstr ""
msgid "Списки сообщества (Community Lists)"
msgstr ""
msgid "Устройства-источники"
msgstr ""
msgid "Accept external proxy connections and route them with sing-box."
msgstr "Принимает внешние прокси-подключения и маршрутизирует их через sing-box."
@@ -371,9 +341,15 @@ msgstr "Стратегия ByeDPI"
msgid "ByeDPI strategy cannot be empty"
msgstr "Стратегия ByeDPI не может быть пустой"
msgid "Bypasses UDP traffic for selected game consoles (PS5/Xbox) to achieve NAT Type 1 / 2 (Full Cone NAT) for P2P matchmaking, while keeping TCP (PSN/Auth) routed through the proxy."
msgstr "Направляет UDP трафик выбранных консолей (PS5/Xbox) в обход прокси напрямую (Full Cone NAT) для идеального P2P матчмейкинга, оставляя TCP трафик (PSN/Auth) под защитой прокси."
msgid "CA Certificate"
msgstr "CA сертификат"
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя."
msgid "Cache File Path"
msgstr "Путь к файлу кэша"
@@ -423,7 +399,7 @@ msgid "Checking..."
msgstr ""
msgid "Checking…"
msgstr "Проверка…"
msgstr ""
msgid "checks"
msgstr "проверки"
@@ -503,6 +479,9 @@ msgstr "Токен доступа к API Cloudflare WARP"
msgid "Comma-separated list of Telegram user IDs authorized to control the bot."
msgstr "Список ID пользователей Telegram через запятую, которым разрешено управлять ботом."
msgid "Community Lists"
msgstr "Списки сообщества (Community Lists)"
msgid "Component update available"
msgstr "Доступно обновление компонента"
@@ -756,7 +735,7 @@ msgid "Domain test sections"
msgstr "Секции тестирования доменов"
msgid "Domains"
msgstr "Домены"
msgstr "Домены (Domains)"
msgid "Domains automatically detected by Smart Detect or added via Telegram bot. You can manually edit or remove them here."
msgstr "Домены, автоматически обнаруженные Smart Detect или добавленные через Telegram-бота. Вы можете вручную изменить или удалить их здесь."
@@ -845,12 +824,6 @@ msgstr "Включить быстрый DNS-кэш"
msgid "Enable list updates"
msgstr "Включить обновление списков"
msgid "Pre-download all preset lists"
msgstr "Скачивать все предустановленные списки"
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя."
msgid "Enable Mixed Proxy"
msgstr "Включить смешанный прокси"
@@ -1070,6 +1043,12 @@ msgstr "Proxy protocol для фронтинга"
msgid "Fuzzing is not supported here because Tachyon needs deterministic runtime validation."
msgstr "Фаззинг здесь не поддерживается, так как Tachyon требует детерминированной валидации во время выполнения."
msgid "Game Console IPs"
msgstr "IP-адреса игровых консолей"
msgid "Game Console Optimizer (NAT Type 1)"
msgstr "Оптимизатор игровых консолей (NAT Type 1)"
msgid "Generate a random 16-character hexadecimal HWID"
msgstr "Сгенерировать случайный 16-символьный шестнадцатеричный HWID"
@@ -1118,7 +1097,6 @@ msgstr "gRPC-сервис"
msgid "Handshake timeout"
msgstr "Таймаут рукопожатия"
msgid "Hide cascade connection nodes"
msgstr "Скрыть узлы каскадных подключений"
@@ -1239,6 +1217,26 @@ msgstr "Включить серверы"
msgid "Information"
msgstr "Информация"
msgid "Init Packet Magic Header (S1)"
msgstr "Magic Header: Инициализация (S1)"
msgid "Init Packet Magic Header (S3)"
msgstr "Magic Header: Инициализация (S3)"
msgid "Init Packet Payload (I1)"
msgstr "Payload: Инициализация (I1)"
msgid "Init Packet Payload (I2)"
msgstr "Payload: Инициализация (I2)"
msgid "Init Packet Payload (I3)"
msgstr "Payload: Инициализация (I3)"
msgid "Init Packet Payload (I4)"
msgstr "Payload: Инициализация (I4)"
msgid "Init Packet Payload (I5)"
msgstr "Payload: Инициализация (I5)"
msgid "Install"
msgstr "Установить"
@@ -1339,11 +1337,8 @@ msgstr "Неверный формат JSON"
msgid "Invalid line: \"%s\". Use: domain ip or ip domain"
msgstr "Неверная строка: \"%s\". Формат: домен ip или ip домен"
msgid "Invalid NFQWS2 strategy"
msgstr "Некорректная стратегия NFQWS2"
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
msgstr "Неверный формат пути. Должен начинаться с \"/\" и содержать допустимые символы"
msgid "Invalid port number. Must be between 1 and 65535"
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
@@ -1364,7 +1359,7 @@ msgid "Invalid Shadowsocks URL: missing credentials"
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
msgstr "Недопустимый Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
msgid "Invalid Shadowsocks URL: missing port"
msgstr "Неверный URL Shadowsocks: отсутствует порт"
@@ -1429,8 +1424,8 @@ msgstr "Неверный URL VLESS: ошибка разбора"
msgid "Invalid VMess URL: parsing failed"
msgstr "Некорректный VMess URL: ошибка разбора"
msgid "IP-адреса и подсети (IP CIDR)"
msgstr ""
msgid "IP Addresses (IP CIDR)"
msgstr "IP-адреса и подсети (IP CIDR)"
msgid "IPs"
msgstr "IP-адреса"
@@ -1468,6 +1463,14 @@ msgstr "JSON outbound должен содержать непустое поле
msgid "JSON outbound settings"
msgstr "Настройки JSON outbound"
msgid "Junk Max Size (Jmax)"
msgstr "Макс. размер Junk (Jmax)"
msgid "Junk Min Size (Jmin)"
msgstr "Мин. размер Junk (Jmin)"
msgid "Junk Packet Count (Jc)"
msgstr "Количество Junk-пакетов (Jc)"
msgid "Kazakhstan (kz)"
msgstr ""
@@ -1745,6 +1748,9 @@ msgstr "Не отвечает"
msgid "Not running"
msgstr "Не запущено"
msgid "Not set"
msgstr "Не заданы"
msgid "Notify on Certificate Warnings"
msgstr "Уведомлять о предупреждениях сертификатов"
@@ -1772,7 +1778,6 @@ msgstr "Количество последовательных успешных
msgid "Obfuscation"
msgstr "Обфускация"
msgid "Obfuscation password"
msgstr "Пароль обфускации"
@@ -1816,7 +1821,7 @@ msgid "Operation timed out"
msgstr "Время ожидания истекло"
msgid "Option \"%s\" contains an invalid input value."
msgstr "Опция \"%s\" содержит некорректное значение."
msgstr "Параметр \"%s\" содержит недопустимое значение."
msgid "Optional — leave empty for default"
msgstr "Опционально — оставьте пустым для значения по умолчанию"
@@ -1929,6 +1934,9 @@ msgstr "Начало диапазона портов должно быть ме
msgid "Ports"
msgstr "Порты"
msgid "Pre-download all preset lists"
msgstr "Скачивать все предустановленные списки"
msgid "Pre-shared key"
msgstr "Предварительно распределённый ключ"
@@ -2058,6 +2066,12 @@ msgstr "Выбор ресурсов по IP или CIDR внутри nfqws не
msgid "Resource selection by IP or CIDR inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE."
msgstr "Выбор ресурсов по IP или CIDR внутри nfqws2 не поддерживается здесь; sing-box выбирает ресурсы до NFQUEUE."
msgid "Response Packet Magic Header (S2)"
msgstr "Magic Header: Ответ (S2)"
msgid "Response Packet Magic Header (S4)"
msgstr "Magic Header: Ответ (S4)"
msgid "Restart Tachyon"
msgstr "Перезапустить Tachyon"
@@ -2097,6 +2111,9 @@ msgstr "Настройки набора правил"
msgid "Rule sets"
msgstr "Наборы правил"
msgid "Rule Sets (.srs / .json)"
msgstr "Наборы правил (.srs / .json)"
msgid "Rules"
msgstr "Правила"
@@ -2151,9 +2168,15 @@ msgstr "Секретный ключ для аутентификации удал
msgid "Section"
msgstr "Секция"
msgid "Section action"
msgstr "Действие секции"
msgid "Section name"
msgstr "Название секции"
msgid "Section Rules Content"
msgstr "Содержимое правил секции"
msgid "Section test order (checked = active, drag rows with △▽):"
msgstr "Порядок тестирования (отмечены активные, сортировка △▽):"
@@ -2208,6 +2231,9 @@ msgstr "Выберите или введите User-Agent"
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
msgid "Select or enter the IP addresses of your game consoles."
msgstr "Выберите или введите IP-адреса ваших игровых приставок."
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
@@ -2310,7 +2336,6 @@ msgstr "Информация о сервисах"
msgid "Settings"
msgstr "Настройки"
msgid "Show metadata on dashboard"
msgstr "Показывать метаданные на дашборде"
@@ -2401,6 +2426,9 @@ msgstr "Сортирует серверы в этой секции по наим
msgid "Source"
msgstr "Источник"
msgid "Source Devices"
msgstr "Устройства-источники"
msgid "Source Network Interface"
msgstr "Сетевой интерфейс источника"
@@ -2776,6 +2804,18 @@ msgstr "Не удалось валидировать стратегию NFQWS2
msgid "Unavailability timeout"
msgstr "Таймаут недоступности"
msgid "Underload Packet Magic Header (H1)"
msgstr "Magic Header: Underload (H1)"
msgid "Underload Packet Magic Header (H2)"
msgstr "Magic Header: Underload (H2)"
msgid "Underload Packet Magic Header (H3)"
msgstr "Magic Header: Underload (H3)"
msgid "Underload Packet Magic Header (H4)"
msgstr "Magic Header: Underload (H4)"
msgid "Unexpected ByeDPI strategy argument: %s"
msgstr "Неожиданный аргумент стратегии ByeDPI: %s"
@@ -3045,311 +3085,3 @@ msgstr "sing-box outbound для Zapret2 настроен"
msgid "Zapret2 sing-box outbound is not configured"
msgstr "sing-box outbound для Zapret2 не настроен"
msgid "checked"
msgstr "проверено"
msgid "Enter a domain, IP address, or port to test routing. The tracer will show which section intercepts the traffic and which rule matches."
msgstr "Введите домен, IP-адрес или порт для проверки маршрутизации. Трассировщик покажет, в какую секцию попадет трафик и какое именно правило сработает."
msgid "Enter a valid DNS server address"
msgstr "Введите корректный адрес DNS сервера"
msgid "Invalid line: \"%s\". Use: domain ip"
msgstr "Неверный формат строки: \"%s\". Используйте: домен ip"
msgid "One record per line: <code>example.com 192.168.1.100</code>. Lines starting with <code>#</code> are ignored."
msgstr "По одной записи на строку: <code>example.com 192.168.1.100</code>. Строки, начинающиеся с <code>#</code>, игнорируются."
msgid "Matching Rule"
msgstr "Сработавшее правило"
msgid "No active routing section intercepts this traffic. Traffic will bypass the custom routing logic and be handled by the default route (Direct/Default)."
msgstr "Этот трафик не перехватывается ни одной активной секцией маршрутизации. Он пойдет в обход кастомных правил и будет обработан маршрутом по умолчанию (Напрямую/Дефолт)."
msgid "out of"
msgstr "из"
msgid "Routing Section"
msgstr "Секция маршрутизации"
msgid "Add URLs or local paths to .lst lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к .lst спискам. Поддерживаются только доменные правила."
msgid "Regional options cannot be used together"
msgstr "Региональные опции не могут использоваться одновременно"
msgid "Russia inside restrictions"
msgstr "Ограничения для России внутри"
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие выборы были удалены."
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
msgstr "Предупреждение: «Россия внутри» можно использовать только с %s. %s уже находятся в «России внутри» и были удалены из выбора."
msgid "Error saving"
msgstr "Ошибка сохранения"
msgid "Saved"
msgstr "Сохранено"
msgid "Checking:"
msgstr "Проверка:"
msgid "Loading:"
msgstr "Загрузка:"
msgid "Starting:"
msgstr "Запуск:"
msgid "Stopping:"
msgstr "Остановка:"
msgid "AnyTLS"
msgstr "AnyTLS"
msgid "Snell"
msgstr "Snell"
msgid "Mieru"
msgstr "Mieru"
msgid "Sudoku"
msgstr "Sudoku"
msgid "MASQUE"
msgstr "MASQUE"
msgid "OpenVPN"
msgstr "OpenVPN"
msgid "Skip TLS certificate verification"
msgstr "Пропустить проверку TLS сертификата"
msgid "Generating..."
msgstr "Генерация..."
msgid "Failed to generate WARP config: "
msgstr "Не удалось сгенерировать конфиг WARP: "
msgid "Failed to parse WARP config: "
msgstr "Не удалось разобрать конфиг WARP: "
msgid "Error running generator: "
msgstr "Ошибка при запуске генератора: "
msgid "Не удалось разобрать файл: "
msgstr "Не удалось разобрать файл: "
msgid "Not tested"
msgstr "Не проверено"
msgid "Test connection"
msgstr "Проверить подключение"
msgid "Destination IP or CIDR"
msgstr "IP или CIDR назначения"
msgid "Forced destination routing"
msgstr "Принудительная маршрутизация по IP (Dest)"
msgid "All traffic to these destination IPs will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик к этим IP назначения будет направлен через секцию безусловно, игнорируя все остальные условия."
msgid "Total lines: "
msgstr "Всего строк: "
msgid "Parsed records: "
msgstr "Распознано записей: "
msgid "Skipped loopback: "
msgstr "Пропущено loopback: "
msgid "Skipped comments/invalid: "
msgstr "Пропущено комментариев/некорректных: "
msgid "New domains: "
msgstr "Новых доменов: "
msgid "Updated/duplicates: "
msgstr "Обновлено/дубликатов: "
msgid "GeoIP Country Routing"
msgstr "Маршрутизация по странам (GeoIP)"
msgid "Select target country or region for this section (e.g. Non-RU for all foreign traffic)."
msgstr "Выберите целевую страну или регион для этой секции (например, Non-RU для всего зарубежного трафика)."
msgid "🌐 All Traffic (Default)"
msgstr "🌐 Весь трафик (По умолчанию)"
msgid "🇪🇺/🇺🇸 Non-RU (All Foreign Traffic)"
msgstr "🇪🇺/🇺🇸 Зарубежный трафик (Non-RU)"
msgid "🇷🇺 RU Only (Russia)"
msgstr "🇷🇺 Только РФ (Россия)"
msgid "🇺🇸 US Only (United States)"
msgstr "🇺🇸 Только США (United States)"
msgid "🇨🇳 CN Only (China)"
msgstr "🇨🇳 Только Китай (China)"
msgid "zapret2 Strategy Constructor"
msgstr "Конструктор формулы zapret2"
msgid "Build formula"
msgstr "Собрать формулу"
msgid "Method:"
msgstr "Метод:"
msgid "Position:"
msgstr "Позиция:"
msgid "Fooling:"
msgstr "Fooling:"
msgid "Testing desync strategies..."
msgstr "Тестирование стратегий обхода..."
msgid "No working desync strategy found for this domain."
msgstr "Рабочая стратегия обхода для этого домена не найдена."
msgid "Section domains:"
msgstr "Домены секции:"
msgid "Testing desync strategy:"
msgstr "Тестирование стратегии:"
msgid "No working desync strategy found for section domains."
msgstr "Рабочая стратегия обхода для доменов секции не найдена."
msgid "Optimal strategy found and applied: "
msgstr "Оптимальная стратегия найдена и применена: "
msgid "DPI Blocked"
msgstr "Заблокировано ТСПУ"
msgid "Applied"
msgstr "Применено"
msgid "Apply"
msgstr "Применить"
msgid "Checking\u2026"
msgstr ""
msgid "Init Packet Magic Header (S1)"
msgstr "Magic Header: Инициализация (S1)"
msgid "Init Packet Magic Header (S3)"
msgstr "Magic Header: Инициализация (S3)"
msgid "Init Packet Payload (I1)"
msgstr "Payload: Инициализация (I1)"
msgid "Init Packet Payload (I2)"
msgstr "Payload: Инициализация (I2)"
msgid "Init Packet Payload (I3)"
msgstr "Payload: Инициализация (I3)"
msgid "Init Packet Payload (I4)"
msgstr "Payload: Инициализация (I4)"
msgid "Init Packet Payload (I5)"
msgstr "Payload: Инициализация (I5)"
msgid "Invalid Shadowsocks URL: missing method and password separator ":""
msgstr "Недопустимый Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
msgid "Invalid line: "%s". Use: domain ip or ip domain"
msgstr "Неверная строка: \"%s\". Формат: домен ip или ip домен"
msgid "Invalid path format. Path must start with "/" and contain valid characters"
msgstr "Неверный формат пути. Должен начинаться с \"/\" и содержать допустимые символы"
msgid "Junk Max Size (Jmax)"
msgstr "Макс. размер Junk (Jmax)"
msgid "Junk Min Size (Jmin)"
msgstr "Мин. размер Junk (Jmin)"
msgid "Junk Packet Count (Jc)"
msgstr "Количество Junk-пакетов (Jc)"
msgid "Option "%s" contains an invalid input value."
msgstr "Опция \"%s\" содержит недопустимое значение."
msgid "Preset"
msgstr "Пресет"
msgid "Response Packet Magic Header (S2)"
msgstr "Magic Header: Ответ (S2)"
msgid "Response Packet Magic Header (S4)"
msgstr "Magic Header: Ответ (S4)"
msgid "Starting\u2026"
msgstr "Запуск\u2026"
msgid "Status / RTT"
msgstr "Статус / Пинг"
msgid "Stopping\u2026"
msgstr "Остановка\u2026"
msgid "Underload Packet Magic Header (H1)"
msgstr "Magic Header: Underload (H1)"
msgid "Underload Packet Magic Header (H2)"
msgstr "Magic Header: Underload (H2)"
msgid "Underload Packet Magic Header (H3)"
msgstr "Magic Header: Underload (H3)"
msgid "Underload Packet Magic Header (H4)"
msgstr "Magic Header: Underload (H4)"
msgid "\u2714 Running"
msgstr "\u2714 Работает"
msgid "\u2718 Stopped"
msgstr "\u2718 Остановлено"
msgid "Close"
msgstr "Закрыть"
msgid "Not set"
msgstr "Не заданы"
msgid "Section action"
msgstr "Действие секции"
msgid "Community Lists"
msgstr "Списки сообщества (Community Lists)"
msgid "Domains"
msgstr "Домены (Domains)"
msgid "IP Addresses (IP CIDR)"
msgstr "IP-адреса и подсети (IP CIDR)"
msgid "Rule Sets (.srs / .json)"
msgstr "Наборы правил (.srs / .json)"
msgid ".lst Lists"
msgstr ".lst списки"
msgid "Source Devices"
msgstr "Устройства-источники"
msgid "Ports"
msgstr "Порты"
msgid "Section Rules Content"
msgstr "Содержимое правил секции"
@@ -1,14 +1,14 @@
declare const rpc: any;
const callHostHints = rpc.declare({
object: "luci-rpc",
method: "getHostHints",
expect: { "": {} },
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} },
});
export async function fetchHostnames(): Promise<Map<string, string>> {
const hostnames = new Map<string, string>();
try {
const hints = await callHostHints();
if (hints && typeof hints === 'object') {
@@ -34,11 +34,12 @@ export async function fetchServicesInfo() {
return uiState;
}
const [tachyonResult, singboxResult, watchdogResult] = await Promise.allSettled([
TachyonShellMethods.getStatus(),
TachyonShellMethods.getSingBoxStatus(),
TachyonShellMethods.getWatchdogStatus(),
]);
const [tachyonResult, singboxResult, watchdogResult] =
await Promise.allSettled([
TachyonShellMethods.getStatus(),
TachyonShellMethods.getSingBoxStatus(),
TachyonShellMethods.getWatchdogStatus(),
]);
if (requestId !== latestServicesInfoRequestId) {
return;
@@ -46,7 +47,10 @@ export async function fetchServicesInfo() {
const tachyon = getSettledMethodResponse('getStatus', tachyonResult);
const singbox = getSettledMethodResponse('getSingBoxStatus', singboxResult);
const watchdog = getSettledMethodResponse('getWatchdogStatus', watchdogResult);
const watchdog = getSettledMethodResponse(
'getWatchdogStatus',
watchdogResult,
);
const previousData = store.get().servicesInfoWidget.data;
store.set({
@@ -73,4 +77,3 @@ export async function fetchServicesInfo() {
return undefined;
}
@@ -8,7 +8,10 @@ type SingBoxVariantFields = {
};
export function isExtendedSingBoxVersion(version?: string) {
return String(version || '').includes('extended') || String(version || '').includes('-lx');
return (
String(version || '').includes('extended') ||
String(version || '').includes('-lx')
);
}
export function isLxSingBoxVersion(version?: string) {
@@ -54,10 +57,7 @@ export function formatSingBoxVersion(value: SingBoxVariantFields) {
normalizedValue.sing_box_compressed
) {
variant = _('compressed');
} else if (
normalizedValue.sing_box_extended &&
normalizedValue.sing_box_lx
) {
} else if (normalizedValue.sing_box_extended && normalizedValue.sing_box_lx) {
variant = _('lx');
} else if (normalizedValue.sing_box_tiny) {
variant = _('tiny');
@@ -365,7 +365,10 @@ function getJsonOutbounds(section: Tachyon.ConfigSection) {
function isConnectionAction(action?: string) {
return Boolean(
action && ['connection', 'proxy', 'outbound', 'vpn', 'awg', 'warp'].includes(action),
action &&
['connection', 'proxy', 'outbound', 'vpn', 'awg', 'warp'].includes(
action,
),
);
}
@@ -1431,7 +1434,11 @@ export async function getDashboardSections(
};
}
if (sectionAction === 'vpn' || sectionAction === 'awg' || sectionAction === 'warp') {
if (
sectionAction === 'vpn' ||
sectionAction === 'awg' ||
sectionAction === 'warp'
) {
const outboundTag = getOutboundTagBySection(sectionName);
const outbound = proxies.find((proxy) => proxy.code === outboundTag);
@@ -1447,9 +1454,13 @@ export async function getDashboardSections(
displayName:
section.interface ||
outbound?.value?.name ||
(sectionAction === 'awg' ? 'AmneziaWG' : sectionAction.toUpperCase()),
(sectionAction === 'awg'
? 'AmneziaWG'
: sectionAction.toUpperCase()),
latency: outbound?.value?.history?.length
? (outbound.value.history[0].delay > 0 ? outbound.value.history[0].delay : -1)
? outbound.value.history[0].delay > 0
? outbound.value.history[0].delay
: -1
: 0,
type: outbound?.value?.type || '',
selected: true,
@@ -817,4 +817,3 @@ export const TachyonShellMethods = {
} as Tachyon.MethodResponse<void>;
},
};
@@ -30,7 +30,10 @@ class Logger {
break;
default:
// Hide spammy debug logs from console unless explicitly enabled
if (typeof localStorage !== 'undefined' && localStorage.getItem('tachyon_debug') === 'true') {
if (
typeof localStorage !== 'undefined' &&
localStorage.getItem('tachyon_debug') === 'true'
) {
console.log(message);
}
}
@@ -33,7 +33,7 @@ import { fetchHostnames } from '../../fetchers/fetchHostnames';
const DASHBOARD_EXPANDED_SECTIONS_KEY = 'tachyon_dashboard_expanded_sections';
const expandedSections = new Set<string>(
JSON.parse(localStorage.getItem(DASHBOARD_EXPANDED_SECTIONS_KEY) || '[]')
JSON.parse(localStorage.getItem(DASHBOARD_EXPANDED_SECTIONS_KEY) || '[]'),
);
function toggleSectionExpanded(sectionCode: string) {
@@ -42,7 +42,10 @@ function toggleSectionExpanded(sectionCode: string) {
} else {
expandedSections.add(sectionCode);
}
localStorage.setItem(DASHBOARD_EXPANDED_SECTIONS_KEY, JSON.stringify(Array.from(expandedSections)));
localStorage.setItem(
DASHBOARD_EXPANDED_SECTIONS_KEY,
JSON.stringify(Array.from(expandedSections)),
);
void renderSectionsWidget();
void renderConnectionsWidget();
}
@@ -639,7 +642,7 @@ function startDashboardDataUpdates() {
sectionsRefreshTimer = setInterval(() => {
void fetchDashboardSections();
}, SECTIONS_REFRESH_INTERVAL_MS);
void fetchConnections();
connectionsRefreshTimer = setInterval(() => {
void fetchConnections();
@@ -744,15 +747,18 @@ async function handleTestLatency(
if (latencyType === 'proxy') {
// Test proxy latency immediately
const parsedTag = tag.startsWith('[') ? JSON.parse(tag)[0] : tag;
const response = await TachyonShellMethods.getClashApiProxyLatency(parsedTag, timeout);
if (response.success && response.data) {
customProxyLatencies.set(tag, response.data.delay || -1);
} else {
customProxyLatencies.set(tag, -1);
}
setLatencyFetching(sectionName, false);
completed = true;
void fetchDashboardSections({ force: true });
const response = await TachyonShellMethods.getClashApiProxyLatency(
parsedTag,
timeout,
);
if (response.success && response.data) {
customProxyLatencies.set(tag, response.data.delay || -1);
} else {
customProxyLatencies.set(tag, -1);
}
setLatencyFetching(sectionName, false);
completed = true;
void fetchDashboardSections({ force: true });
} else {
const startResponse = await TachyonShellMethods.latencyTestStart(
latencyType,
@@ -1394,8 +1400,8 @@ function updateLatencyProgressInline(
section.action || '',
);
const text = isConnectionNode
? _('Checking Connection...')
const text = isConnectionNode
? _('Checking Connection...')
: getLatencyTestLabel(
sectionsWidget.latencyProgressSections[section.sectionName],
);
@@ -1562,9 +1568,14 @@ async function fetchConnections() {
try {
const [res, hostnames] = await Promise.all([
TachyonShellMethods.getClashApiConnections(),
fetchHostnames()
fetchHostnames(),
]);
if (res.success && res.data && typeof res.data === 'object' && Array.isArray((res.data as any).connections)) {
if (
res.success &&
res.data &&
typeof res.data === 'object' &&
Array.isArray((res.data as any).connections)
) {
const connectionsList = (res.data as any).connections;
const map = new Map<string, IConnection>();
for (const conn of connectionsList) {
@@ -1582,13 +1593,15 @@ async function fetchConnections() {
map.set(ip, { ip, count: 1, upload: up, download: down, name });
}
}
currentConnections = Array.from(map.values()).sort((a, b) => (b.download + b.upload) - (a.download + a.upload));
currentConnections = Array.from(map.values()).sort(
(a, b) => b.download + b.upload - (a.download + a.upload),
);
connectionsLoading = false;
connectionsFailed = false;
} else {
connectionsFailed = true;
}
} catch(e) {
} catch (e) {
connectionsFailed = true;
}
renderConnectionsWidget();
@@ -1597,12 +1610,14 @@ async function fetchConnections() {
function renderConnectionsWidget() {
const container = document.getElementById('dashboard-connections-grid');
if (!container) return;
container.replaceChildren(renderConnections(
currentConnections,
!expandedSections.has('active_clients'),
() => toggleSectionExpanded('active_clients')
));
container.replaceChildren(
renderConnections(
currentConnections,
!expandedSections.has('active_clients'),
() => toggleSectionExpanded('active_clients'),
),
);
}
async function renderBandwidthWidget() {
@@ -1740,7 +1755,6 @@ async function onPageMount() {
void renderServicesInfoWidget();
syncDashboardServiceAvailability();
if (hasRuntimeSnapshot) {
void refreshRuntimeUiState({ force: true });
}
@@ -46,7 +46,7 @@ function rerender() {
export async function loadAdvancedSettingsState() {
const sections = await getConfigSections();
const settingsSec = sections.find(s => s['.type'] === 'settings');
const settingsSec = sections.find((s) => s['.type'] === 'settings');
const smartDetect = settingsSec?.smart_detect === '1';
const raw = settingsSec?.smart_detect_sections;
@@ -57,9 +57,14 @@ export async function loadAdvancedSettingsState() {
: [];
const ruleSections = sections.filter(
s => s['.type'] === 'section' && s.enabled !== '0' && (s.action as string) !== 'bypass' && (s.action as string) !== 'block' && (s.action as string) !== 'dns',
(s) =>
s['.type'] === 'section' &&
s.enabled !== '0' &&
(s.action as string) !== 'bypass' &&
(s.action as string) !== 'block' &&
(s.action as string) !== 'dns',
);
const allSectionNames = ruleSections.map(s => s['.name'] as string);
const allSectionNames = ruleSections.map((s) => s['.name'] as string);
const deviceIpsPerSection: Record<string, string[]> = {};
for (const s of ruleSections) {
@@ -73,17 +78,18 @@ export async function loadAdvancedSettingsState() {
}
const wdRes = await TachyonShellMethods.getWatchdogStatus();
const watchdogRunning: boolean =
wdRes.success
? Boolean((wdRes as { data: { running: boolean } }).data.running)
: false;
const watchdogRunning: boolean = wdRes.success
? Boolean((wdRes as { data: { running: boolean } }).data.running)
: false;
_state = {
..._state,
watchdogRunning,
smartDetectEnabled: smartDetect,
smartDetectSections:
smartDetectSections.length > 0 ? smartDetectSections : allSectionNames.slice(0, 1),
smartDetectSections.length > 0
? smartDetectSections
: allSectionNames.slice(0, 1),
allSectionNames,
deviceIpsPerSection,
dnsTurboCache: settingsSec?.dns_turbo_cache === '1',
@@ -103,8 +109,9 @@ async function toggleWatchdog() {
await TachyonShellMethods.watchdogStart();
}
const wdRes = await TachyonShellMethods.getWatchdogStatus();
const running =
wdRes.success ? (wdRes as { data: { running: boolean } }).data.running : _state.watchdogRunning;
const running = wdRes.success
? (wdRes as { data: { running: boolean } }).data.running
: _state.watchdogRunning;
_state = { ..._state, watchdogRunning: running, watchdogLoading: false };
rerender();
}
@@ -142,7 +149,7 @@ async function saveDeviceIps(sectionName: string, ipsText: string) {
try {
const ips = ipsText
.split('\n')
.map(l => l.trim())
.map((l) => l.trim())
.filter(Boolean);
await TachyonShellMethods.uciRunCommand([
'delete',
@@ -158,7 +165,11 @@ async function saveDeviceIps(sectionName: string, ipsText: string) {
_state.deviceIpsPerSection[sectionName] = ips;
showToast(_('Device IPs saved'), 'success');
// Reload tachyon async so UI stays responsive
void TachyonShellMethods.uciRunCommand(['-q', 'commit', TACHYON_UCI_PACKAGE]);
void TachyonShellMethods.uciRunCommand([
'-q',
'commit',
TACHYON_UCI_PACKAGE,
]);
} catch {
showToast(_('Failed to save device IPs'), 'error');
}
@@ -205,7 +216,7 @@ function toggleSectionInList(sectionName: string, checked: boolean) {
if (checked && !arr.includes(sectionName)) {
arr.push(sectionName);
} else if (!checked) {
arr = arr.filter(s => s !== sectionName);
arr = arr.filter((s) => s !== sectionName);
}
_state = { ..._state, smartDetectSections: arr };
rerender();
@@ -254,9 +265,13 @@ function renderDnsTurboCacheSection(state: AdvancedSettingsState) {
E('span', { class: 'tachyon_adv__section-icon' }, '⚡'),
E('h3', { class: 'tachyon_adv__section-title' }, _('DNS Turbo Cache')),
]),
E('p', { class: 'tachyon_adv__hint' }, _(
'Keeps FakeIP cache persistent across reboots and pre-resolves popular blocked domains on startup so first-visit latency is 0\u00a0ms.',
)),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Keeps FakeIP cache persistent across reboots and pre-resolves popular blocked domains on startup so first-visit latency is 0\u00a0ms.',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
@@ -270,13 +285,18 @@ function renderDnsTurboCacheSection(state: AdvancedSettingsState) {
E('span', {}, _('Enable DNS Turbo Cache')),
]),
]),
saving ? E('span', { class: 'tachyon_adv__hint' }, _('Saving…')) : E('span', {}),
saving
? E('span', { class: 'tachyon_adv__hint' }, _('Saving…'))
: E('span', {}),
]);
}
function renderSmartDetectSection(state: AdvancedSettingsState) {
const { smartDetectEnabled, smartDetectSections, allSectionNames, saving } = state;
const unselected = allSectionNames.filter(s => !smartDetectSections.includes(s));
const { smartDetectEnabled, smartDetectSections, allSectionNames, saving } =
state;
const unselected = allSectionNames.filter(
(s) => !smartDetectSections.includes(s),
);
const rows: Element[] = [];
@@ -288,43 +308,72 @@ function renderSmartDetectSection(state: AdvancedSettingsState) {
type: 'checkbox',
checked: true,
onchange: (e: Event) =>
toggleSectionInList(secName, (e.target as HTMLInputElement).checked),
toggleSectionInList(
secName,
(e.target as HTMLInputElement).checked,
),
}),
E('span', { class: 'tachyon_adv__priority-name' }, `${idx + 1}. ${secName}`),
E(
'span',
{ class: 'tachyon_adv__priority-name' },
`${idx + 1}. ${secName}`,
),
]),
E('div', { class: 'tachyon_adv__arrows' }, [
E('button', {
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move up'),
disabled: idx === 0,
onclick: () => moveSectionUp(idx),
}, '△'),
E('button', {
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move down'),
disabled: idx === smartDetectSections.length - 1,
onclick: () => moveSectionDown(idx),
}, '▽'),
E(
'button',
{
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move up'),
disabled: idx === 0,
onclick: () => moveSectionUp(idx),
},
'△',
),
E(
'button',
{
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move down'),
disabled: idx === smartDetectSections.length - 1,
onclick: () => moveSectionDown(idx),
},
'▽',
),
]),
]),
);
});
unselected.forEach(secName => {
unselected.forEach((secName) => {
rows.push(
E('div', { class: 'tachyon_adv__priority-row tachyon_adv__priority-row--off' }, [
E('label', { class: 'tachyon_adv__priority-label' }, [
E('input', {
type: 'checkbox',
checked: false,
onchange: (e: Event) =>
toggleSectionInList(secName, (e.target as HTMLInputElement).checked),
}),
E('span', { class: 'tachyon_adv__priority-name tachyon_adv__priority-name--off' }, secName),
]),
]),
E(
'div',
{ class: 'tachyon_adv__priority-row tachyon_adv__priority-row--off' },
[
E('label', { class: 'tachyon_adv__priority-label' }, [
E('input', {
type: 'checkbox',
checked: false,
onchange: (e: Event) =>
toggleSectionInList(
secName,
(e.target as HTMLInputElement).checked,
),
}),
E(
'span',
{
class:
'tachyon_adv__priority-name tachyon_adv__priority-name--off',
},
secName,
),
]),
],
),
);
});
@@ -333,16 +382,23 @@ function renderSmartDetectSection(state: AdvancedSettingsState) {
E('span', { class: 'tachyon_adv__section-icon' }, '🔍'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Smart Detect')),
]),
E('p', { class: 'tachyon_adv__hint' }, _(
'Auto-detects blocked domains from logs and adds them to the first section where they work via proxy.',
)),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Auto-detects blocked domains from logs and adds them to the first section where they work via proxy.',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
type: 'checkbox',
checked: smartDetectEnabled,
onchange: (e: Event) => {
_state = { ..._state, smartDetectEnabled: (e.target as HTMLInputElement).checked };
_state = {
..._state,
smartDetectEnabled: (e.target as HTMLInputElement).checked,
};
rerender();
},
}),
@@ -351,18 +407,24 @@ function renderSmartDetectSection(state: AdvancedSettingsState) {
]),
smartDetectEnabled && rows.length > 0
? E('div', { class: 'tachyon_adv__priority-list' }, [
E('p', { class: 'tachyon_adv__sub-hint' }, _(
'Section test order (checked = active, drag rows with △▽):',
)),
E(
'p',
{ class: 'tachyon_adv__sub-hint' },
_('Section test order (checked = active, drag rows with △▽):'),
),
...rows,
])
: E('span', {}),
E('button', {
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => void saveSmartDetect(),
}, saving ? _('Saving…') : _('Save Smart Detect Settings')),
E(
'button',
{
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => void saveSmartDetect(),
},
saving ? _('Saving…') : _('Save Smart Detect Settings'),
),
]);
}
@@ -373,34 +435,56 @@ function renderDeviceRoutingSection(state: AdvancedSettingsState) {
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🖥'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Per-Device Routing')),
E(
'h3',
{ class: 'tachyon_adv__section-title' },
_('Per-Device Routing'),
),
]),
E('p', { class: 'tachyon_adv__hint' }, _('No active routing sections found.')),
E(
'p',
{ class: 'tachyon_adv__hint' },
_('No active routing sections found.'),
),
]);
}
const editors = allSectionNames.map(secName => {
const editors = allSectionNames.map((secName) => {
const currentIps = (deviceIpsPerSection[secName] || []).join('\n');
const taId = `tachyon-device-ips-${secName}`;
return E('div', { class: 'tachyon_adv__device-block' }, [
E('div', { class: 'tachyon_adv__device-name' }, secName),
E('label', { class: 'tachyon_adv__device-label' }, _('Device IPs (one per line):')),
E('textarea', {
id: taId,
class: 'cbi-input-textarea tachyon_adv__device-ta',
rows: 3,
placeholder: '192.168.1.100\n192.168.1.105',
}, currentIps),
E('button', {
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => {
const ta = document.getElementById(taId) as HTMLTextAreaElement | null;
void saveDeviceIps(secName, ta ? ta.value : '');
E(
'label',
{ class: 'tachyon_adv__device-label' },
_('Device IPs (one per line):'),
),
E(
'textarea',
{
id: taId,
class: 'cbi-input-textarea tachyon_adv__device-ta',
rows: 3,
placeholder: '192.168.1.100\n192.168.1.105',
},
}, saving ? _('Saving…') : _('Save')),
currentIps,
),
E(
'button',
{
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => {
const ta = document.getElementById(
taId,
) as HTMLTextAreaElement | null;
void saveDeviceIps(secName, ta ? ta.value : '');
},
},
saving ? _('Saving…') : _('Save'),
),
]);
});
@@ -409,9 +493,13 @@ function renderDeviceRoutingSection(state: AdvancedSettingsState) {
E('span', { class: 'tachyon_adv__section-icon' }, '🖥'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Per-Device Routing')),
]),
E('p', { class: 'tachyon_adv__hint' }, _(
'Devices listed here are always routed through the assigned section, regardless of global rules.',
)),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Devices listed here are always routed through the assigned section, regardless of global rules.',
),
),
...editors,
]);
}
@@ -435,10 +523,16 @@ function renderAdvancedSettingsBody(state: AdvancedSettingsState) {
export function renderAdvancedSettingsPanel() {
return E('div', { id: 'tachyon-advanced-settings', class: 'tachyon_adv' }, [
E('details', { class: 'tachyon_adv__details' }, [
E('summary', { class: 'tachyon_adv__summary' }, _('⚙ Advanced Settings')),
E('div', { id: 'tachyon-advanced-settings-inner', class: 'tachyon_adv__inner' }, [
renderAdvancedSettingsBody(_state),
]),
E(
'summary',
{ class: 'tachyon_adv__summary' },
_('⚙ Advanced Settings'),
),
E(
'div',
{ id: 'tachyon-advanced-settings-inner', class: 'tachyon_adv__inner' },
[renderAdvancedSettingsBody(_state)],
),
]),
]);
}
@@ -9,68 +9,134 @@ export interface IConnection {
name?: string;
}
export function renderConnections(connections: IConnection[], isCollapsed: boolean, onToggleCollapse: () => void) {
export function renderConnections(
connections: IConnection[],
isCollapsed: boolean,
onToggleCollapse: () => void,
) {
if (connections.length === 0) {
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
E('div', {
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: pointer; user-select: none;',
click: onToggleCollapse
}, [
E('div', { class: 'tachyon_dashboard-page__outbound-section__title-section__title', style: 'display: flex; align-items: center; gap: 8px;' }, [
svgEl('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`
}, [
svgEl('polyline', { points: '6 9 12 15 18 9' })
]),
_('Active Clients')
]),
]),
isCollapsed ? '' : E('div', { class: 'tachyon_dashboard-page__outbound-section centered', style: 'height: 60px;' }, _('No active clients')),
E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: pointer; user-select: none;',
click: onToggleCollapse,
},
[
E(
'div',
{
class:
'tachyon_dashboard-page__outbound-section__title-section__title',
style: 'display: flex; align-items: center; gap: 8px;',
},
[
svgEl(
'svg',
{
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`,
},
[svgEl('polyline', { points: '6 9 12 15 18 9' })],
),
_('Active Clients'),
],
),
],
),
isCollapsed
? ''
: E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section centered',
style: 'height: 60px;',
},
_('No active clients'),
),
]);
}
const rows = connections.map(c => {
return E('div', { class: 'tachyon_dashboard-page__widgets-section__item__row', style: 'padding: 8px 0; border-bottom: 1px solid rgba(128, 128, 128, 0.1); display: flex; justify-content: space-between;' }, [
E('div', {}, [
E('b', {}, c.name ? `${c.name} (${c.ip})` : c.ip),
E('span', { style: 'opacity: 0.7; font-size: 13px; margin-left: 8px;' }, `(${c.count} conns)`)
]),
E('div', { style: 'font-size: 13px;' }, `▲ ${prettyBytes(c.upload)} | ▼ ${prettyBytes(c.download)}`)
]);
const rows = connections.map((c) => {
return E(
'div',
{
class: 'tachyon_dashboard-page__widgets-section__item__row',
style:
'padding: 8px 0; border-bottom: 1px solid rgba(128, 128, 128, 0.1); display: flex; justify-content: space-between;',
},
[
E('div', {}, [
E('b', {}, c.name ? `${c.name} (${c.ip})` : c.ip),
E(
'span',
{ style: 'opacity: 0.7; font-size: 13px; margin-left: 8px;' },
`(${c.count} conns)`,
),
]),
E(
'div',
{ style: 'font-size: 13px;' },
`▲ ${prettyBytes(c.upload)} | ▼ ${prettyBytes(c.download)}`,
),
],
);
});
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
E('div', {
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: pointer; user-select: none;',
click: onToggleCollapse
}, [
E('div', { class: 'tachyon_dashboard-page__outbound-section__title-section__title', style: 'display: flex; align-items: center; gap: 8px;' }, [
svgEl('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`
}, [
svgEl('polyline', { points: '6 9 12 15 18 9' })
]),
_('Active Clients')
]),
]),
isCollapsed ? '' : E('div', { class: 'tachyon_dashboard-page__outbound-grid', style: 'padding: 12px; display: block;' }, rows),
E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section__title-section',
style: 'cursor: pointer; user-select: none;',
click: onToggleCollapse,
},
[
E(
'div',
{
class:
'tachyon_dashboard-page__outbound-section__title-section__title',
style: 'display: flex; align-items: center; gap: 8px;',
},
[
svgEl(
'svg',
{
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`,
},
[svgEl('polyline', { points: '6 9 12 15 18 9' })],
),
_('Active Clients'),
],
),
],
),
isCollapsed
? ''
: E(
'div',
{
class: 'tachyon_dashboard-page__outbound-grid',
style: 'padding: 12px; display: block;',
},
rows,
),
]);
}
@@ -9,11 +9,7 @@ export function renderFlagEmojis(value: string): (HTMLElement | string)[] {
.filter(Boolean)
.map((part) =>
EXACT_FLAG_EMOJI_PATTERN.test(part)
? E(
'span',
{ class: 'tachyon_dashboard-page__flag-emoji' },
part,
)
? E('span', { class: 'tachyon_dashboard-page__flag-emoji' }, part)
: part,
);
}
@@ -267,10 +267,11 @@ function renderDefaultState({
isCollapsed,
onToggleCollapse,
}: IRenderSectionsProps) {
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(section.action || '');
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(
section.action || '',
);
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(
section.latencyTestCodes?.length
@@ -317,8 +318,10 @@ function renderDefaultState({
const connectionStatusText = latencyFetching
? `● ${_('Checking...')}`
: outbound.latency === -1 || !outbound.runtimeAvailable
? `● N/A`
: (outbound.latency && outbound.latency > 0 ? `● ${outbound.latency}ms` : `● N/A`);
? `● N/A`
: outbound.latency && outbound.latency > 0
? `● ${outbound.latency}ms`
: `● N/A`;
const canCopyLink =
Boolean(outbound.canCopyLink) || isCopyableProxyLink(outbound.link);
@@ -352,14 +355,43 @@ function renderDefaultState({
'div',
{
class: className,
style: 'display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;'
style:
'display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;',
},
[
E('div', { style: 'display: flex; align-items: center; gap: 12px; min-width: 0;' }, [
E('b', { style: 'overflow-wrap: anywhere; word-break: break-all; min-width: 0;' }, renderFlagEmojis(outbound.displayName)),
E('span', { style: 'opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;' }, [outbound.type].filter(Boolean)),
E('div', { class: getLatencyClass(), style: 'white-space: nowrap; flex-shrink: 0;' }, connectionStatusText),
]),
E(
'div',
{
style:
'display: flex; align-items: center; gap: 12px; min-width: 0;',
},
[
E(
'b',
{
style:
'overflow-wrap: anywhere; word-break: break-all; min-width: 0;',
},
renderFlagEmojis(outbound.displayName),
),
E(
'span',
{
style:
'opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;',
},
[outbound.type].filter(Boolean),
),
E(
'div',
{
class: getLatencyClass(),
style: 'white-space: nowrap; flex-shrink: 0;',
},
connectionStatusText,
),
],
),
E(
'button',
{
@@ -377,11 +409,21 @@ function renderDefaultState({
latencyFetching
? [
renderLoaderCircleIcon24(),
E('span', { class: 'dashboard-sections-grid-item-test-latency__label' }, _('Checking...')),
E(
'span',
{
class: 'dashboard-sections-grid-item-test-latency__label',
},
_('Checking...'),
),
]
: E('span', { class: 'dashboard-sections-grid-item-test-latency__label' }, _('Check Connection')),
)
]
: E(
'span',
{ class: 'dashboard-sections-grid-item-test-latency__label' },
_('Check Connection'),
),
),
],
);
}
@@ -493,7 +535,9 @@ function renderDefaultState({
{ class: getLatencyClass() },
isConnectionNode
? connectionStatusText
: (outbound.latency ? `${outbound.latency}ms` : 'N/A'),
: outbound.latency
? `${outbound.latency}ms`
: 'N/A',
),
],
),
@@ -502,11 +546,23 @@ function renderDefaultState({
}
if (isConnectionNode) {
return E('div', { class: 'tachyon_dashboard-page__outbound-section', style: 'border: none; padding: 0;' }, [
E('div', { style: 'display: flex; flex-direction: column; gap: 8px; padding: 0;' }, [
...section.outbounds.map((outbound) => renderOutbound(outbound)),
])
]);
return E(
'div',
{
class: 'tachyon_dashboard-page__outbound-section',
style: 'border: none; padding: 0;',
},
[
E(
'div',
{
style:
'display: flex; flex-direction: column; gap: 8px; padding: 0;',
},
[...section.outbounds.map((outbound) => renderOutbound(outbound))],
),
],
);
}
const metadataNodes = (section.subscriptionMetadata || [])
@@ -522,13 +578,13 @@ function renderDefaultState({
// Title with test latency
E(
'div',
{
{
class: 'tachyon_dashboard-page__outbound-section__title-section',
click: (e: MouseEvent) => {
if (e.target && (e.target as Element).closest('button')) return;
onToggleCollapse?.();
},
style: 'cursor: pointer; user-select: none;'
style: 'cursor: pointer; user-select: none;',
},
[
E(
@@ -536,57 +592,97 @@ function renderDefaultState({
{
class:
'tachyon_dashboard-page__outbound-section__title-section__title',
style: 'display: flex; align-items: center; gap: 8px;'
style: 'display: flex; align-items: center; gap: 8px;',
},
[
svgEl('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`
}, [
svgEl('polyline', { points: '6 9 12 15 18 9' })
]),
svgEl(
'svg',
{
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? '-90deg' : '0deg'})`,
},
[svgEl('polyline', { points: '6 9 12 15 18 9' })],
),
E('span', {}, section.displayName),
isCollapsed ? (() => {
const selectedOutbound = section.outbounds.find(o => o.selected);
if (!selectedOutbound) return '';
isCollapsed
? (() => {
const selectedOutbound = section.outbounds.find(
(o) => o.selected,
);
if (!selectedOutbound) return '';
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(section.action || '');
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(
section.action || '',
);
function getLatencyColor() {
if (isConnectionNode) {
if (latencyFetching) return 'var(--warn-color-medium, orange)';
if (selectedOutbound!.latency === -1) return 'var(--error-color-medium, red)';
return selectedOutbound!.runtimeAvailable ? 'var(--success-color-medium, green)' : 'var(--error-color-medium, red)';
function getLatencyColor() {
if (isConnectionNode) {
if (latencyFetching)
return 'var(--warn-color-medium, orange)';
if (selectedOutbound!.latency === -1)
return 'var(--error-color-medium, red)';
return selectedOutbound!.runtimeAvailable
? 'var(--success-color-medium, green)'
: 'var(--error-color-medium, red)';
}
if (!selectedOutbound!.latency)
return 'var(--primary-color-low, lightgray)';
if (selectedOutbound!.latency < 800)
return 'var(--success-color-medium, green)';
if (selectedOutbound!.latency < 1500)
return 'var(--warn-color-medium, orange)';
return 'var(--error-color-medium, red)';
}
if (!selectedOutbound!.latency) return 'var(--primary-color-low, lightgray)';
if (selectedOutbound!.latency < 800) return 'var(--success-color-medium, green)';
if (selectedOutbound!.latency < 1500) return 'var(--warn-color-medium, orange)';
return 'var(--error-color-medium, red)';
}
let latencyText = '';
if (isConnectionNode) {
latencyText = latencyFetching
? _('Checking...')
: selectedOutbound.latency === -1 ||
!selectedOutbound.runtimeAvailable
? _('Not connected')
: selectedOutbound.latency &&
selectedOutbound.latency > 0
? `${selectedOutbound.latency}ms`
: _('Connected');
} else {
latencyText = selectedOutbound.latency
? `${selectedOutbound.latency}ms`
: '';
}
let latencyText = '';
if (isConnectionNode) {
latencyText = latencyFetching ? _('Checking...') : (selectedOutbound.latency === -1 || !selectedOutbound.runtimeAvailable ? _('Not connected') : (selectedOutbound.latency && selectedOutbound.latency > 0 ? `${selectedOutbound.latency}ms` : _('Connected')));
} else {
latencyText = selectedOutbound.latency ? `${selectedOutbound.latency}ms` : '';
}
return E('span', {
style: 'font-size: 13px; font-weight: normal; margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;'
}, [
E('span', { style: 'opacity: 0.7;' }, selectedOutbound.displayName),
latencyText ? E('span', { style: `color: ${getLatencyColor()};` }, latencyText) : ''
]);
})() : ''
]
return E(
'span',
{
style:
'font-size: 13px; font-weight: normal; margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;',
},
[
E(
'span',
{ style: 'opacity: 0.7;' },
selectedOutbound.displayName,
),
latencyText
? E(
'span',
{ style: `color: ${getLatencyColor()};` },
latencyText,
)
: '',
],
);
})()
: '',
],
),
E(
'div',
@@ -622,7 +718,9 @@ function renderDefaultState({
class:
'dashboard-sections-grid-item-test-latency__label',
},
isConnectionNode ? _('Checking...') : getLatencyTestLabel(latencyProgress),
isConnectionNode
? _('Checking...')
: getLatencyTestLabel(latencyProgress),
),
]
: E(
@@ -630,17 +728,21 @@ function renderDefaultState({
{
class: 'dashboard-sections-grid-item-test-latency__label',
},
isConnectionNode ? _('Check Connection') : _('Test latency'),
isConnectionNode
? _('Check Connection')
: _('Test latency'),
),
),
],
),
],
),
!isCollapsed ? E('div', { class: 'tachyon_dashboard-page__outbound-grid' }, [
...metadataNodes,
...section.outbounds.map((outbound) => renderOutbound(outbound)),
]) : '',
!isCollapsed
? E('div', { class: 'tachyon_dashboard-page__outbound-grid' }, [
...metadataNodes,
...section.outbounds.map((outbound) => renderOutbound(outbound)),
])
: '',
]);
}
@@ -90,9 +90,7 @@ export function render() {
selectorSwitchingTag: undefined,
}),
),
]),
],
);
}
@@ -1,4 +1,8 @@
import { onMount, preserveScrollForPage, executeShellCommand } from '../../../helpers';
import {
onMount,
preserveScrollForPage,
executeShellCommand,
} from '../../../helpers';
import { showToast } from '../../../helpers/showToast';
import { runDnsCheck } from './checks/runDnsCheck';
import { runSingBoxCheck } from './checks/runSingBoxCheck';
@@ -803,22 +807,32 @@ async function handleGenerateBugReport() {
setDiagnosticActionLoading('generateBugReport', true);
try {
const configResult = await fs.read('/etc/config/tachyon').catch(() => '');
const logsResult = await executeShellCommand({ command: '/sbin/logread', args: ['-e', 'tachyon', '-l', '1000'] });
const singboxLogsResult = await executeShellCommand({ command: '/sbin/logread', args: ['-e', 'sing-box', '-l', '1000'] });
const logsResult = await executeShellCommand({
command: '/sbin/logread',
args: ['-e', 'tachyon', '-l', '1000'],
});
const singboxLogsResult = await executeShellCommand({
command: '/sbin/logread',
args: ['-e', 'sing-box', '-l', '1000'],
});
const rawReport = [
'--- TACHYON CONFIG ---',
configResult || 'Failed to fetch config',
'',
'--- TACHYON LOGS ---',
logsResult.code === 0 ? logsResult.stdout : 'Failed to fetch tachyon logs',
logsResult.code === 0
? logsResult.stdout
: 'Failed to fetch tachyon logs',
'',
'--- SING-BOX LOGS ---',
singboxLogsResult.code === 0 ? singboxLogsResult.stdout : 'Failed to fetch sing-box logs',
singboxLogsResult.code === 0
? singboxLogsResult.stdout
: 'Failed to fetch sing-box logs',
].join('\n');
const maskedReport = maskGlobalCheckText(rawReport);
// Download as a text file instead of using clipboard, because navigator.clipboard
// is undefined in insecure contexts (HTTP) which is common for router web interfaces.
const blob = new Blob([maskedReport], { type: 'text/plain;charset=utf-8' });
@@ -831,7 +845,7 @@ async function handleGenerateBugReport() {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(_('Bug report downloaded'), 'success');
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleGenerateBugReport - e', e);
@@ -95,8 +95,8 @@ describe('diagnostic masking', () => {
expect(masked.split('\n')).toHaveLength(raw.split('\n').length);
expect(masked).not.toContain('vless://secret');
expect(masked).toContain('config interface \'lan\'');
expect(masked).toContain('config interface \'wan\'');
expect(masked).toContain("config interface 'lan'");
expect(masked).toContain("config interface 'wan'");
expect(masked).not.toContain('192.168.1.1');
expect(masked).not.toContain('provider-password');
expect(masked).toContain("option proxy_string '*******'");
@@ -480,7 +480,9 @@ function getVisibleConnections(): MonitoredConnection[] {
return getConnectionsForActiveTab().filter((connection) => {
const sourceIp = getConnectionSourceIp(connection);
const isMatchingDevice = selectedDeviceFilter === ALL_FILTER_VALUE || sourceIp === selectedDeviceFilter;
const isMatchingDevice =
selectedDeviceFilter === ALL_FILTER_VALUE ||
sourceIp === selectedDeviceFilter;
let isMatchingRoute = selectedRouteFilter === ALL_FILTER_VALUE;
if (!isMatchingRoute && getRoute(connection) === selectedRouteFilter) {
@@ -612,7 +614,7 @@ function renderRouteFilterOptions() {
});
const routes = Array.from(uniqueRoutes).sort();
if (
selectedRouteFilter !== ALL_FILTER_VALUE &&
!routes.includes(selectedRouteFilter)
@@ -620,16 +622,13 @@ function renderRouteFilterOptions() {
selectedRouteFilter = ALL_FILTER_VALUE;
}
const signature = [
selectedRouteFilter,
...routes,
].join('|');
const signature = [selectedRouteFilter, ...routes].join('|');
if (signature === lastRouteFilterSignature) {
select.value = selectedRouteFilter;
return;
}
lastRouteFilterSignature = signature;
const options = [
@@ -832,13 +832,14 @@ function getComponentCards(): ComponentCard[] {
!systemInfo.sing_box_extended &&
!systemInfo.sing_box_tiny;
const singBoxExtended =
Boolean(systemInfo.sing_box_extended) && !systemInfo.sing_box_compressed && !systemInfo.sing_box_lx;
Boolean(systemInfo.sing_box_extended) &&
!systemInfo.sing_box_compressed &&
!systemInfo.sing_box_lx;
const singBoxExtendedCompressed =
Boolean(systemInfo.sing_box_extended) &&
Boolean(systemInfo.sing_box_compressed);
const singBoxLx =
Boolean(systemInfo.sing_box_extended) &&
Boolean(systemInfo.sing_box_lx);
Boolean(systemInfo.sing_box_extended) && Boolean(systemInfo.sing_box_lx);
const singBoxTiny = Boolean(systemInfo.sing_box_tiny);
const tachyonActions = getInstalledUpdateActions(
-1
View File
@@ -365,7 +365,6 @@ export namespace Tachyon {
dns_turbo_cache?: '0' | '1';
}
export interface MethodSuccessResponse<T> {
success: true;
data: T;
@@ -862,11 +862,7 @@ var FLAG_EMOJI_PATTERN = /([\u{1f1e6}-\u{1f1ff}]{2}|\u{1f3f4}[\u{e0061}-\u{e007a
var EXACT_FLAG_EMOJI_PATTERN = /^([\u{1f1e6}-\u{1f1ff}]{2}|\u{1f3f4}[\u{e0061}-\u{e007a}]+\u{e007f})$/u;
function renderFlagEmojis(value) {
return value.split(FLAG_EMOJI_PATTERN).filter(Boolean).map(
(part) => EXACT_FLAG_EMOJI_PATTERN.test(part) ? E(
"span",
{ class: "tachyon_dashboard-page__flag-emoji" },
part
) : part
(part) => EXACT_FLAG_EMOJI_PATTERN.test(part) ? E("span", { class: "tachyon_dashboard-page__flag-emoji" }, part) : part
);
}
@@ -1957,7 +1953,9 @@ function renderDefaultState({
isCollapsed,
onToggleCollapse
}) {
const isConnectionNode = ["vpn", "awg", "warp"].includes(section.action || "");
const isConnectionNode = ["vpn", "awg", "warp"].includes(
section.action || ""
);
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(
@@ -2012,11 +2010,36 @@ function renderDefaultState({
style: "display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;"
},
[
E("div", { style: "display: flex; align-items: center; gap: 12px; min-width: 0;" }, [
E("b", { style: "overflow-wrap: anywhere; word-break: break-all; min-width: 0;" }, renderFlagEmojis(outbound.displayName)),
E("span", { style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;" }, [outbound.type].filter(Boolean)),
E("div", { class: getLatencyClass(), style: "white-space: nowrap; flex-shrink: 0;" }, connectionStatusText)
]),
E(
"div",
{
style: "display: flex; align-items: center; gap: 12px; min-width: 0;"
},
[
E(
"b",
{
style: "overflow-wrap: anywhere; word-break: break-all; min-width: 0;"
},
renderFlagEmojis(outbound.displayName)
),
E(
"span",
{
style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;"
},
[outbound.type].filter(Boolean)
),
E(
"div",
{
class: getLatencyClass(),
style: "white-space: nowrap; flex-shrink: 0;"
},
connectionStatusText
)
]
),
E(
"button",
{
@@ -2033,8 +2056,18 @@ function renderDefaultState({
},
latencyFetching ? [
renderLoaderCircleIcon24(),
E("span", { class: "dashboard-sections-grid-item-test-latency__label" }, _("Checking..."))
] : E("span", { class: "dashboard-sections-grid-item-test-latency__label" }, _("Check Connection"))
E(
"span",
{
class: "dashboard-sections-grid-item-test-latency__label"
},
_("Checking...")
)
] : E(
"span",
{ class: "dashboard-sections-grid-item-test-latency__label" },
_("Check Connection")
)
)
]
);
@@ -2139,11 +2172,22 @@ function renderDefaultState({
);
}
if (isConnectionNode) {
return E("div", { class: "tachyon_dashboard-page__outbound-section", style: "border: none; padding: 0;" }, [
E("div", { style: "display: flex; flex-direction: column; gap: 8px; padding: 0;" }, [
...section.outbounds.map((outbound) => renderOutbound(outbound))
])
]);
return E(
"div",
{
class: "tachyon_dashboard-page__outbound-section",
style: "border: none; padding: 0;"
},
[
E(
"div",
{
style: "display: flex; flex-direction: column; gap: 8px; padding: 0;"
},
[...section.outbounds.map((outbound) => renderOutbound(outbound))]
)
]
);
}
const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean);
const subscriptionUpdateAction = renderSubscriptionUpdateAction(
@@ -2171,33 +2215,44 @@ function renderDefaultState({
style: "display: flex; align-items: center; gap: 8px;"
},
[
svgEl("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
}, [
svgEl("polyline", { points: "6 9 12 15 18 9" })
]),
svgEl(
"svg",
{
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
},
[svgEl("polyline", { points: "6 9 12 15 18 9" })]
),
E("span", {}, section.displayName),
isCollapsed ? (() => {
const selectedOutbound = section.outbounds.find((o) => o.selected);
const selectedOutbound = section.outbounds.find(
(o) => o.selected
);
if (!selectedOutbound) return "";
const isConnectionNode2 = ["vpn", "awg", "warp"].includes(section.action || "");
const isConnectionNode2 = ["vpn", "awg", "warp"].includes(
section.action || ""
);
function getLatencyColor() {
if (isConnectionNode2) {
if (latencyFetching) return "var(--warn-color-medium, orange)";
if (selectedOutbound.latency === -1) return "var(--error-color-medium, red)";
if (latencyFetching)
return "var(--warn-color-medium, orange)";
if (selectedOutbound.latency === -1)
return "var(--error-color-medium, red)";
return selectedOutbound.runtimeAvailable ? "var(--success-color-medium, green)" : "var(--error-color-medium, red)";
}
if (!selectedOutbound.latency) return "var(--primary-color-low, lightgray)";
if (selectedOutbound.latency < 800) return "var(--success-color-medium, green)";
if (selectedOutbound.latency < 1500) return "var(--warn-color-medium, orange)";
if (!selectedOutbound.latency)
return "var(--primary-color-low, lightgray)";
if (selectedOutbound.latency < 800)
return "var(--success-color-medium, green)";
if (selectedOutbound.latency < 1500)
return "var(--warn-color-medium, orange)";
return "var(--error-color-medium, red)";
}
let latencyText = "";
@@ -2206,12 +2261,24 @@ function renderDefaultState({
} else {
latencyText = selectedOutbound.latency ? `${selectedOutbound.latency}ms` : "";
}
return E("span", {
style: "font-size: 13px; font-weight: normal; margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;"
}, [
E("span", { style: "opacity: 0.7;" }, selectedOutbound.displayName),
latencyText ? E("span", { style: `color: ${getLatencyColor()};` }, latencyText) : ""
]);
return E(
"span",
{
style: "font-size: 13px; font-weight: normal; margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;"
},
[
E(
"span",
{ style: "opacity: 0.7;" },
selectedOutbound.displayName
),
latencyText ? E(
"span",
{ style: `color: ${getLatencyColor()};` },
latencyText
) : ""
]
);
})() : ""
]
),
@@ -2345,64 +2412,119 @@ function renderWidget(props) {
function renderConnections(connections, isCollapsed, onToggleCollapse) {
if (connections.length === 0) {
return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [
E("div", {
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: pointer; user-select: none;",
click: onToggleCollapse
}, [
E("div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [
svgEl("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
}, [
svgEl("polyline", { points: "6 9 12 15 18 9" })
]),
_("Active Clients")
])
]),
isCollapsed ? "" : E("div", { class: "tachyon_dashboard-page__outbound-section centered", style: "height: 60px;" }, _("No active clients"))
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: pointer; user-select: none;",
click: onToggleCollapse
},
[
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section__title",
style: "display: flex; align-items: center; gap: 8px;"
},
[
svgEl(
"svg",
{
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
},
[svgEl("polyline", { points: "6 9 12 15 18 9" })]
),
_("Active Clients")
]
)
]
),
isCollapsed ? "" : E(
"div",
{
class: "tachyon_dashboard-page__outbound-section centered",
style: "height: 60px;"
},
_("No active clients")
)
]);
}
const rows = connections.map((c) => {
return E("div", { class: "tachyon_dashboard-page__widgets-section__item__row", style: "padding: 8px 0; border-bottom: 1px solid rgba(128, 128, 128, 0.1); display: flex; justify-content: space-between;" }, [
E("div", {}, [
E("b", {}, c.name ? `${c.name} (${c.ip})` : c.ip),
E("span", { style: "opacity: 0.7; font-size: 13px; margin-left: 8px;" }, `(${c.count} conns)`)
]),
E("div", { style: "font-size: 13px;" }, `\u25B2 ${prettyBytes(c.upload)} | \u25BC ${prettyBytes(c.download)}`)
]);
return E(
"div",
{
class: "tachyon_dashboard-page__widgets-section__item__row",
style: "padding: 8px 0; border-bottom: 1px solid rgba(128, 128, 128, 0.1); display: flex; justify-content: space-between;"
},
[
E("div", {}, [
E("b", {}, c.name ? `${c.name} (${c.ip})` : c.ip),
E(
"span",
{ style: "opacity: 0.7; font-size: 13px; margin-left: 8px;" },
`(${c.count} conns)`
)
]),
E(
"div",
{ style: "font-size: 13px;" },
`\u25B2 ${prettyBytes(c.upload)} | \u25BC ${prettyBytes(c.download)}`
)
]
);
});
return E("div", { class: "tachyon_dashboard-page__outbound-section" }, [
E("div", {
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: pointer; user-select: none;",
click: onToggleCollapse
}, [
E("div", { class: "tachyon_dashboard-page__outbound-section__title-section__title", style: "display: flex; align-items: center; gap: 8px;" }, [
svgEl("svg", {
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
}, [
svgEl("polyline", { points: "6 9 12 15 18 9" })
]),
_("Active Clients")
])
]),
isCollapsed ? "" : E("div", { class: "tachyon_dashboard-page__outbound-grid", style: "padding: 12px; display: block;" }, rows)
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section",
style: "cursor: pointer; user-select: none;",
click: onToggleCollapse
},
[
E(
"div",
{
class: "tachyon_dashboard-page__outbound-section__title-section__title",
style: "display: flex; align-items: center; gap: 8px;"
},
[
svgEl(
"svg",
{
width: "16",
height: "16",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: `transition: transform 0.2s; transform: rotate(${isCollapsed ? "-90deg" : "0deg"})`
},
[svgEl("polyline", { points: "6 9 12 15 18 9" })]
),
_("Active Clients")
]
)
]
),
isCollapsed ? "" : E(
"div",
{
class: "tachyon_dashboard-page__outbound-grid",
style: "padding: 12px; display: block;"
},
rows
)
]);
}
@@ -3489,7 +3611,9 @@ function getJsonOutbounds(section) {
}
function isConnectionAction(action) {
return Boolean(
action && ["connection", "proxy", "outbound", "vpn", "awg", "warp"].includes(action)
action && ["connection", "proxy", "outbound", "vpn", "awg", "warp"].includes(
action
)
);
}
function hasSubscriptionSources(section) {
@@ -5709,7 +5833,10 @@ async function fetchServicesInfo() {
}
const tachyon = getSettledMethodResponse("getStatus", tachyonResult);
const singbox = getSettledMethodResponse("getSingBoxStatus", singboxResult);
const watchdog = getSettledMethodResponse("getWatchdogStatus", watchdogResult);
const watchdog = getSettledMethodResponse(
"getWatchdogStatus",
watchdogResult
);
const previousData = store.get().servicesInfoWidget.data;
store.set({
servicesInfoWidget: {
@@ -5798,7 +5925,10 @@ function toggleSectionExpanded(sectionCode) {
} else {
expandedSections.add(sectionCode);
}
localStorage.setItem(DASHBOARD_EXPANDED_SECTIONS_KEY, JSON.stringify(Array.from(expandedSections)));
localStorage.setItem(
DASHBOARD_EXPANDED_SECTIONS_KEY,
JSON.stringify(Array.from(expandedSections))
);
void renderSectionsWidget();
void renderConnectionsWidget();
}
@@ -6309,7 +6439,10 @@ async function handleTestLatency(latencyType, sectionName, tag, timeout) {
try {
if (latencyType === "proxy") {
const parsedTag = tag.startsWith("[") ? JSON.parse(tag)[0] : tag;
const response = await TachyonShellMethods.getClashApiProxyLatency(parsedTag, timeout);
const response = await TachyonShellMethods.getClashApiProxyLatency(
parsedTag,
timeout
);
if (response.success && response.data) {
customProxyLatencies.set(tag, response.data.delay || -1);
} else {
@@ -6975,7 +7108,9 @@ async function fetchConnections() {
map.set(ip, { ip, count: 1, upload: up, download: down, name });
}
}
currentConnections = Array.from(map.values()).sort((a, b) => b.download + b.upload - (a.download + a.upload));
currentConnections = Array.from(map.values()).sort(
(a, b) => b.download + b.upload - (a.download + a.upload)
);
connectionsLoading = false;
connectionsFailed = false;
} else {
@@ -6989,11 +7124,13 @@ async function fetchConnections() {
function renderConnectionsWidget() {
const container = document.getElementById("dashboard-connections-grid");
if (!container) return;
container.replaceChildren(renderConnections(
currentConnections,
!expandedSections.has("active_clients"),
() => toggleSectionExpanded("active_clients")
));
container.replaceChildren(
renderConnections(
currentConnections,
!expandedSections.has("active_clients"),
() => toggleSectionExpanded("active_clients")
)
);
}
async function renderBandwidthWidget() {
renderStoreWidget(
@@ -7295,7 +7432,6 @@ var styles = `
justify-content: space-between;
gap: 8px 10px;
min-width: 0;
flex-wrap: wrap;
}
.tachyon_dashboard-page__outbound-section__title-section__title {
@@ -7310,8 +7446,7 @@ var styles = `
align-items: center;
justify-content: flex-end;
gap: 6px;
flex: 1 1 auto;
flex-wrap: wrap;
flex: 0 0 auto;
}
.tachyon_dashboard-page .btn.tachyon_dashboard-page__outbound-section__subscription-update {
@@ -7389,13 +7524,14 @@ var styles = `
}
.tachyon_dashboard-page__subscription-meta__title {
flex: 1 1 auto;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-all;
flex: 0 1 auto;
width: max-content;
max-width: min(28ch, 30%);
min-width: min-content;
color: var(--text-color-high);
font-weight: 700;
line-height: 1.25;
overflow-wrap: anywhere;
}
.tachyon_dashboard-page__subscription-meta__facts {
@@ -10586,8 +10722,14 @@ async function handleGenerateBugReport() {
setDiagnosticActionLoading("generateBugReport", true);
try {
const configResult = await fs.read("/etc/config/tachyon").catch(() => "");
const logsResult = await executeShellCommand({ command: "/sbin/logread", args: ["-e", "tachyon", "-l", "1000"] });
const singboxLogsResult = await executeShellCommand({ command: "/sbin/logread", args: ["-e", "sing-box", "-l", "1000"] });
const logsResult = await executeShellCommand({
command: "/sbin/logread",
args: ["-e", "tachyon", "-l", "1000"]
});
const singboxLogsResult = await executeShellCommand({
command: "/sbin/logread",
args: ["-e", "sing-box", "-l", "1000"]
});
const rawReport = [
"--- TACHYON CONFIG ---",
configResult || "Failed to fetch config",
@@ -11773,10 +11915,7 @@ function renderRouteFilterOptions() {
if (selectedRouteFilter !== ALL_FILTER_VALUE && !routes.includes(selectedRouteFilter)) {
selectedRouteFilter = ALL_FILTER_VALUE;
}
const signature = [
selectedRouteFilter,
...routes
].join("|");
const signature = [selectedRouteFilter, ...routes].join("|");
if (signature === lastRouteFilterSignature) {
select.value = selectedRouteFilter;
return;
@@ -7344,145 +7344,7 @@ function createSectionContent(section) {
return analysis.valid ? true : analysis.message;
};
/* zapret2 Strategy Auto-Tuner Widget (NATIVE LUCI CBI STYLE) */
o = section.taboption("settings", form.DummyValue, "_zapret2_tune_widget", "");
o.modalonly = true;
o.depends("action", "zapret2");
o.renderWidget = function (section_id, option_index, cfgvalue) {
const setNfqws2OptTextarea = (val) => {
const el = document.querySelector(`textarea[name="cbid.${UCI_PACKAGE}.${section_id}.nfqws2_opt"]`) ||
document.querySelector(`[data-name="nfqws2_opt"] textarea`) ||
document.querySelector(`.cbi-option-nfqws2_opt textarea`);
if (el) {
el.value = val;
el.dispatchEvent(new Event("change", { bubbles: true }));
}
uci.set(UCI_PACKAGE, section_id, "nfqws2_opt", val);
};
const startBtn = E("button", {
class: "btn cbi-button cbi-button-action",
type: "button"
}, _("Auto-Tune Strategy"));
const statusText = E("div", { class: "cbi-value-description", style: "margin-top: 8px; font-weight: bold;" }, "");
const resultsContainer = E("div", { style: "margin-top: 8px;" });
const card = E("div", { class: "cbi-value" }, [
E("label", { class: "cbi-value-title" }, _("Auto-Tune Strategy")),
E("div", { class: "cbi-value-field" }, [
startBtn,
statusText,
resultsContainer
])
]);
startBtn.addEventListener("click", (e) => {
e.preventDefault();
const selectedMode = "deep";
startBtn.disabled = true;
statusText.textContent = _("Initializing Auto-Tune...");
resultsContainer.innerHTML = "";
fs.exec("/usr/bin/tachyon", ["zapret2_tune_async", section_id, "", selectedMode])
.then((res) => {
let data = null;
try { data = JSON.parse(res.stdout || "{}"); } catch (err) {}
if (!data || !data.success || !data.job_id) {
startBtn.disabled = false;
statusText.textContent = _("Failed to start Auto-Tune job.");
return;
}
const jobId = data.job_id;
const pollTimer = setInterval(() => {
fs.exec("/usr/bin/tachyon", ["zapret2_tune_status", jobId])
.then((stRes) => {
let stData = null;
try { stData = JSON.parse(stRes.stdout || "{}"); } catch (err) {}
if (!stData) return;
if (stData.running) {
const prog = stData.progress || {};
const completed = prog.completed || 0;
const total = prog.total || 7;
const domainText = prog.current_domain ? ` [${prog.current_domain}]` : "";
statusText.textContent = _("Testing desync strategy:") + ` ${prog.current_item || ""}${domainText}... [${completed}/${total}]`;
return;
}
// Tuning finished!
clearInterval(pollTimer);
startBtn.disabled = false;
if (!stData.success || !stData.winning_strategy) {
statusText.textContent = (stData && stData.message) ? stData.message : _("No working desync strategy found for section domains.");
} else {
statusText.textContent = _("Optimal strategy found and applied: ") + `${stData.winning_preset_name || ""} (${stData.best_rtt_ms || 0} ms)`;
}
const applyButtons = [];
const rows = (stData.results || []).map((r) => {
const isWinner = (r.opt === stData.winning_strategy);
const applyBtn = E("button", {
class: isWinner ? "btn cbi-button cbi-button-action" : "btn cbi-button cbi-button-neutral",
type: "button"
}, isWinner ? _("Applied") : _("Apply"));
applyButtons.push(applyBtn);
applyBtn.addEventListener("click", (evt) => {
evt.preventDefault();
setNfqws2OptTextarea(r.opt);
// Reset all buttons
applyButtons.forEach(btn => {
btn.className = "btn cbi-button cbi-button-neutral";
btn.textContent = _("Apply");
});
// Set current button to applied
applyBtn.className = "btn cbi-button cbi-button-action";
applyBtn.textContent = _("Applied");
});
return E("tr", { class: "tr" }, [
E("td", { class: "td" }, r.name),
E("td", { class: "td" }, r.success ? `${r.rtt_ms} ms (${r.message})` : _("DPI Blocked")),
E("td", { class: "td right" }, applyBtn)
]);
});
const table = E("table", { class: "table cbi-section-table" }, [
E("thead", {}, [
E("tr", { class: "tr" }, [
E("th", { class: "th" }, _("Preset")),
E("th", { class: "th" }, _("Status / RTT")),
E("th", { class: "th right" }, _("Action"))
])
]),
E("tbody", {}, rows)
]);
resultsContainer.innerHTML = "";
resultsContainer.appendChild(table);
if (stData.winning_strategy) {
setNfqws2OptTextarea(stData.winning_strategy);
}
})
.catch(() => {});
}, 1000);
})
.catch((err) => {
startBtn.disabled = false;
statusText.textContent = _("Error running tuner: ") + err;
});
});
return card;
};
o = section.taboption(
"settings",
@@ -6,6 +6,7 @@
"require ui";
"require tools.widgets as widgets";
"require view.tachyon.main as main";
"require view.tachyon.local_devices as local_devices";
const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE;
@@ -1317,6 +1318,31 @@ function createSettingsContent(section, capabilities) {
o.default = "0";
o.rmempty = false;
o = section.option(
form.Flag,
"game_console_optimizer",
_("Game Console Optimizer (NAT Type 1)"),
_("Bypasses UDP traffic for selected game consoles (PS5/Xbox) to achieve NAT Type 1 / 2 (Full Cone NAT) for P2P matchmaking, while keeping TCP (PSN/Auth) routed through the proxy."),
);
o.default = "0";
o.rmempty = false;
const gameConsoleIpsOpt = section.option(
form.DynamicList,
"game_console_ips",
_("Game Console IPs"),
_("Select or enter the IP addresses of your game consoles."),
);
gameConsoleIpsOpt.depends("game_console_optimizer", "1");
gameConsoleIpsOpt.datatype = "ipaddr";
gameConsoleIpsOpt.renderWidget = function (section_id, option_index, cfgvalue) {
return local_devices.createLocalDeviceDynamicListWidget(
this,
section_id,
cfgvalue,
);
};
o = section.option(
form.Flag,
"list_update_enabled",
+94 -362
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-28 21:47+0300\n"
"PO-Revision-Date: 2026-07-28 21:47+0300\n"
"POT-Creation-Date: 2026-07-30 16:42+0300\n"
"PO-Revision-Date: 2026-07-30 16:42+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -20,8 +20,8 @@ msgstr ""
msgid "-- Select --"
msgstr "-- Выбрать --"
msgid ".lst списки"
msgstr ""
msgid ".lst Lists"
msgstr ".lst списки"
msgid "+ Add JSON outbound"
msgstr "+ Добавить JSON outbound"
@@ -54,7 +54,7 @@ msgid "✔ Running"
msgstr "✔ Работает"
msgid "✘ Stopped"
msgstr "✘ Остановлен"
msgstr "✘ Остановлено"
msgid "Выберите файл с расширением .conf"
msgstr "Выберите файл с расширением .conf"
@@ -62,12 +62,6 @@ msgstr "Выберите файл с расширением .conf"
msgid "Готово"
msgstr "Готово"
msgid "Действие секции"
msgstr ""
msgid "Домены (Domains)"
msgstr ""
msgid "Загружено!"
msgstr "Загружено!"
@@ -77,21 +71,12 @@ msgstr "Загрузить .conf"
msgid "Загрузить конфиг .conf"
msgstr "Загрузить конфиг .conf"
msgid "Закрыть"
msgstr ""
msgid "Импортировать настройки AmneziaWG из файла .conf"
msgstr "Импортировать настройки AmneziaWG из файла .conf"
msgid "Конфиг AmneziaWG успешно загружен!"
msgstr "Конфиг AmneziaWG успешно загружен!"
msgid "Наборы правил (.srs / .json)"
msgstr ""
msgid "Не заданы"
msgstr ""
msgid "Не удалось разобрать файл:"
msgstr ""
@@ -101,21 +86,6 @@ msgstr ""
msgid "Ошибка чтения"
msgstr "Ошибка чтения"
msgid "Порты"
msgstr ""
msgid "Правила"
msgstr ""
msgid "Содержимое правил секции"
msgstr ""
msgid "Списки сообщества (Community Lists)"
msgstr ""
msgid "Устройства-источники"
msgstr ""
msgid "Accept external proxy connections and route them with sing-box."
msgstr "Принимает внешние прокси-подключения и маршрутизирует их через sing-box."
@@ -371,9 +341,15 @@ msgstr "Стратегия ByeDPI"
msgid "ByeDPI strategy cannot be empty"
msgstr "Стратегия ByeDPI не может быть пустой"
msgid "Bypasses UDP traffic for selected game consoles (PS5/Xbox) to achieve NAT Type 1 / 2 (Full Cone NAT) for P2P matchmaking, while keeping TCP (PSN/Auth) routed through the proxy."
msgstr "Направляет UDP трафик выбранных консолей (PS5/Xbox) в обход прокси напрямую (Full Cone NAT) для идеального P2P матчмейкинга, оставляя TCP трафик (PSN/Auth) под защитой прокси."
msgid "CA Certificate"
msgstr "CA сертификат"
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя."
msgid "Cache File Path"
msgstr "Путь к файлу кэша"
@@ -423,7 +399,7 @@ msgid "Checking..."
msgstr ""
msgid "Checking…"
msgstr "Проверка…"
msgstr ""
msgid "checks"
msgstr "проверки"
@@ -503,6 +479,9 @@ msgstr "Токен доступа к API Cloudflare WARP"
msgid "Comma-separated list of Telegram user IDs authorized to control the bot."
msgstr "Список ID пользователей Telegram через запятую, которым разрешено управлять ботом."
msgid "Community Lists"
msgstr "Списки сообщества (Community Lists)"
msgid "Component update available"
msgstr "Доступно обновление компонента"
@@ -756,7 +735,7 @@ msgid "Domain test sections"
msgstr "Секции тестирования доменов"
msgid "Domains"
msgstr "Домены"
msgstr "Домены (Domains)"
msgid "Domains automatically detected by Smart Detect or added via Telegram bot. You can manually edit or remove them here."
msgstr "Домены, автоматически обнаруженные Smart Detect или добавленные через Telegram-бота. Вы можете вручную изменить или удалить их здесь."
@@ -845,12 +824,6 @@ msgstr "Включить быстрый DNS-кэш"
msgid "Enable list updates"
msgstr "Включить обновление списков"
msgid "Pre-download all preset lists"
msgstr "Скачивать все предустановленные списки"
msgid "Cache all built-in lists and databases even if they are not currently selected in any section. Recommended if you have plenty of storage space or USB drive."
msgstr "Кешировать все встроенные списки и базы (даже если они не выбраны ни в одной секции). Рекомендуется при наличии свободного места или USB-накопителя."
msgid "Enable Mixed Proxy"
msgstr "Включить смешанный прокси"
@@ -1070,6 +1043,12 @@ msgstr "Proxy protocol для фронтинга"
msgid "Fuzzing is not supported here because Tachyon needs deterministic runtime validation."
msgstr "Фаззинг здесь не поддерживается, так как Tachyon требует детерминированной валидации во время выполнения."
msgid "Game Console IPs"
msgstr "IP-адреса игровых консолей"
msgid "Game Console Optimizer (NAT Type 1)"
msgstr "Оптимизатор игровых консолей (NAT Type 1)"
msgid "Generate a random 16-character hexadecimal HWID"
msgstr "Сгенерировать случайный 16-символьный шестнадцатеричный HWID"
@@ -1118,7 +1097,6 @@ msgstr "gRPC-сервис"
msgid "Handshake timeout"
msgstr "Таймаут рукопожатия"
msgid "Hide cascade connection nodes"
msgstr "Скрыть узлы каскадных подключений"
@@ -1239,6 +1217,26 @@ msgstr "Включить серверы"
msgid "Information"
msgstr "Информация"
msgid "Init Packet Magic Header (S1)"
msgstr "Magic Header: Инициализация (S1)"
msgid "Init Packet Magic Header (S3)"
msgstr "Magic Header: Инициализация (S3)"
msgid "Init Packet Payload (I1)"
msgstr "Payload: Инициализация (I1)"
msgid "Init Packet Payload (I2)"
msgstr "Payload: Инициализация (I2)"
msgid "Init Packet Payload (I3)"
msgstr "Payload: Инициализация (I3)"
msgid "Init Packet Payload (I4)"
msgstr "Payload: Инициализация (I4)"
msgid "Init Packet Payload (I5)"
msgstr "Payload: Инициализация (I5)"
msgid "Install"
msgstr "Установить"
@@ -1339,11 +1337,8 @@ msgstr "Неверный формат JSON"
msgid "Invalid line: \"%s\". Use: domain ip or ip domain"
msgstr "Неверная строка: \"%s\". Формат: домен ip или ip домен"
msgid "Invalid NFQWS2 strategy"
msgstr "Некорректная стратегия NFQWS2"
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать допустимые символы"
msgstr "Неверный формат пути. Должен начинаться с \"/\" и содержать допустимые символы"
msgid "Invalid port number. Must be between 1 and 65535"
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
@@ -1364,7 +1359,7 @@ msgid "Invalid Shadowsocks URL: missing credentials"
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
msgstr "Недопустимый Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
msgid "Invalid Shadowsocks URL: missing port"
msgstr "Неверный URL Shadowsocks: отсутствует порт"
@@ -1429,8 +1424,8 @@ msgstr "Неверный URL VLESS: ошибка разбора"
msgid "Invalid VMess URL: parsing failed"
msgstr "Некорректный VMess URL: ошибка разбора"
msgid "IP-адреса и подсети (IP CIDR)"
msgstr ""
msgid "IP Addresses (IP CIDR)"
msgstr "IP-адреса и подсети (IP CIDR)"
msgid "IPs"
msgstr "IP-адреса"
@@ -1468,6 +1463,14 @@ msgstr "JSON outbound должен содержать непустое поле
msgid "JSON outbound settings"
msgstr "Настройки JSON outbound"
msgid "Junk Max Size (Jmax)"
msgstr "Макс. размер Junk (Jmax)"
msgid "Junk Min Size (Jmin)"
msgstr "Мин. размер Junk (Jmin)"
msgid "Junk Packet Count (Jc)"
msgstr "Количество Junk-пакетов (Jc)"
msgid "Kazakhstan (kz)"
msgstr ""
@@ -1745,6 +1748,9 @@ msgstr "Не отвечает"
msgid "Not running"
msgstr "Не запущено"
msgid "Not set"
msgstr "Не заданы"
msgid "Notify on Certificate Warnings"
msgstr "Уведомлять о предупреждениях сертификатов"
@@ -1772,7 +1778,6 @@ msgstr "Количество последовательных успешных
msgid "Obfuscation"
msgstr "Обфускация"
msgid "Obfuscation password"
msgstr "Пароль обфускации"
@@ -1816,7 +1821,7 @@ msgid "Operation timed out"
msgstr "Время ожидания истекло"
msgid "Option \"%s\" contains an invalid input value."
msgstr "Опция \"%s\" содержит некорректное значение."
msgstr "Параметр \"%s\" содержит недопустимое значение."
msgid "Optional — leave empty for default"
msgstr "Опционально — оставьте пустым для значения по умолчанию"
@@ -1929,6 +1934,9 @@ msgstr "Начало диапазона портов должно быть ме
msgid "Ports"
msgstr "Порты"
msgid "Pre-download all preset lists"
msgstr "Скачивать все предустановленные списки"
msgid "Pre-shared key"
msgstr "Предварительно распределённый ключ"
@@ -2058,6 +2066,12 @@ msgstr "Выбор ресурсов по IP или CIDR внутри nfqws не
msgid "Resource selection by IP or CIDR inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE."
msgstr "Выбор ресурсов по IP или CIDR внутри nfqws2 не поддерживается здесь; sing-box выбирает ресурсы до NFQUEUE."
msgid "Response Packet Magic Header (S2)"
msgstr "Magic Header: Ответ (S2)"
msgid "Response Packet Magic Header (S4)"
msgstr "Magic Header: Ответ (S4)"
msgid "Restart Tachyon"
msgstr "Перезапустить Tachyon"
@@ -2097,6 +2111,9 @@ msgstr "Настройки набора правил"
msgid "Rule sets"
msgstr "Наборы правил"
msgid "Rule Sets (.srs / .json)"
msgstr "Наборы правил (.srs / .json)"
msgid "Rules"
msgstr "Правила"
@@ -2151,9 +2168,15 @@ msgstr "Секретный ключ для аутентификации удал
msgid "Section"
msgstr "Секция"
msgid "Section action"
msgstr "Действие секции"
msgid "Section name"
msgstr "Название секции"
msgid "Section Rules Content"
msgstr "Содержимое правил секции"
msgid "Section test order (checked = active, drag rows with △▽):"
msgstr "Порядок тестирования (отмечены активные, сортировка △▽):"
@@ -2208,6 +2231,9 @@ msgstr "Выберите или введите User-Agent"
msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing"
msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
msgid "Select or enter the IP addresses of your game consoles."
msgstr "Выберите или введите IP-адреса ваших игровых приставок."
msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing"
msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете"
@@ -2310,7 +2336,6 @@ msgstr "Информация о сервисах"
msgid "Settings"
msgstr "Настройки"
msgid "Show metadata on dashboard"
msgstr "Показывать метаданные на дашборде"
@@ -2401,6 +2426,9 @@ msgstr "Сортирует серверы в этой секции по наим
msgid "Source"
msgstr "Источник"
msgid "Source Devices"
msgstr "Устройства-источники"
msgid "Source Network Interface"
msgstr "Сетевой интерфейс источника"
@@ -2776,6 +2804,18 @@ msgstr "Не удалось валидировать стратегию NFQWS2
msgid "Unavailability timeout"
msgstr "Таймаут недоступности"
msgid "Underload Packet Magic Header (H1)"
msgstr "Magic Header: Underload (H1)"
msgid "Underload Packet Magic Header (H2)"
msgstr "Magic Header: Underload (H2)"
msgid "Underload Packet Magic Header (H3)"
msgstr "Magic Header: Underload (H3)"
msgid "Underload Packet Magic Header (H4)"
msgstr "Magic Header: Underload (H4)"
msgid "Unexpected ByeDPI strategy argument: %s"
msgstr "Неожиданный аргумент стратегии ByeDPI: %s"
@@ -3045,311 +3085,3 @@ msgstr "sing-box outbound для Zapret2 настроен"
msgid "Zapret2 sing-box outbound is not configured"
msgstr "sing-box outbound для Zapret2 не настроен"
msgid "checked"
msgstr "проверено"
msgid "Enter a domain, IP address, or port to test routing. The tracer will show which section intercepts the traffic and which rule matches."
msgstr "Введите домен, IP-адрес или порт для проверки маршрутизации. Трассировщик покажет, в какую секцию попадет трафик и какое именно правило сработает."
msgid "Enter a valid DNS server address"
msgstr "Введите корректный адрес DNS сервера"
msgid "Invalid line: \"%s\". Use: domain ip"
msgstr "Неверный формат строки: \"%s\". Используйте: домен ip"
msgid "One record per line: <code>example.com 192.168.1.100</code>. Lines starting with <code>#</code> are ignored."
msgstr "По одной записи на строку: <code>example.com 192.168.1.100</code>. Строки, начинающиеся с <code>#</code>, игнорируются."
msgid "Matching Rule"
msgstr "Сработавшее правило"
msgid "No active routing section intercepts this traffic. Traffic will bypass the custom routing logic and be handled by the default route (Direct/Default)."
msgstr "Этот трафик не перехватывается ни одной активной секцией маршрутизации. Он пойдет в обход кастомных правил и будет обработан маршрутом по умолчанию (Напрямую/Дефолт)."
msgid "out of"
msgstr "из"
msgid "Routing Section"
msgstr "Секция маршрутизации"
msgid "Add URLs or local paths to .lst lists. Only domain rules are supported."
msgstr "Добавьте URL или локальные пути к .lst спискам. Поддерживаются только доменные правила."
msgid "Regional options cannot be used together"
msgstr "Региональные опции не могут использоваться одновременно"
msgid "Russia inside restrictions"
msgstr "Ограничения для России внутри"
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие выборы были удалены."
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
msgstr "Предупреждение: «Россия внутри» можно использовать только с %s. %s уже находятся в «России внутри» и были удалены из выбора."
msgid "Error saving"
msgstr "Ошибка сохранения"
msgid "Saved"
msgstr "Сохранено"
msgid "Checking:"
msgstr "Проверка:"
msgid "Loading:"
msgstr "Загрузка:"
msgid "Starting:"
msgstr "Запуск:"
msgid "Stopping:"
msgstr "Остановка:"
msgid "AnyTLS"
msgstr "AnyTLS"
msgid "Snell"
msgstr "Snell"
msgid "Mieru"
msgstr "Mieru"
msgid "Sudoku"
msgstr "Sudoku"
msgid "MASQUE"
msgstr "MASQUE"
msgid "OpenVPN"
msgstr "OpenVPN"
msgid "Skip TLS certificate verification"
msgstr "Пропустить проверку TLS сертификата"
msgid "Generating..."
msgstr "Генерация..."
msgid "Failed to generate WARP config: "
msgstr "Не удалось сгенерировать конфиг WARP: "
msgid "Failed to parse WARP config: "
msgstr "Не удалось разобрать конфиг WARP: "
msgid "Error running generator: "
msgstr "Ошибка при запуске генератора: "
msgid "Не удалось разобрать файл: "
msgstr "Не удалось разобрать файл: "
msgid "Not tested"
msgstr "Не проверено"
msgid "Test connection"
msgstr "Проверить подключение"
msgid "Destination IP or CIDR"
msgstr "IP или CIDR назначения"
msgid "Forced destination routing"
msgstr "Принудительная маршрутизация по IP (Dest)"
msgid "All traffic to these destination IPs will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик к этим IP назначения будет направлен через секцию безусловно, игнорируя все остальные условия."
msgid "Total lines: "
msgstr "Всего строк: "
msgid "Parsed records: "
msgstr "Распознано записей: "
msgid "Skipped loopback: "
msgstr "Пропущено loopback: "
msgid "Skipped comments/invalid: "
msgstr "Пропущено комментариев/некорректных: "
msgid "New domains: "
msgstr "Новых доменов: "
msgid "Updated/duplicates: "
msgstr "Обновлено/дубликатов: "
msgid "GeoIP Country Routing"
msgstr "Маршрутизация по странам (GeoIP)"
msgid "Select target country or region for this section (e.g. Non-RU for all foreign traffic)."
msgstr "Выберите целевую страну или регион для этой секции (например, Non-RU для всего зарубежного трафика)."
msgid "🌐 All Traffic (Default)"
msgstr "🌐 Весь трафик (По умолчанию)"
msgid "🇪🇺/🇺🇸 Non-RU (All Foreign Traffic)"
msgstr "🇪🇺/🇺🇸 Зарубежный трафик (Non-RU)"
msgid "🇷🇺 RU Only (Russia)"
msgstr "🇷🇺 Только РФ (Россия)"
msgid "🇺🇸 US Only (United States)"
msgstr "🇺🇸 Только США (United States)"
msgid "🇨🇳 CN Only (China)"
msgstr "🇨🇳 Только Китай (China)"
msgid "zapret2 Strategy Constructor"
msgstr "Конструктор формулы zapret2"
msgid "Build formula"
msgstr "Собрать формулу"
msgid "Method:"
msgstr "Метод:"
msgid "Position:"
msgstr "Позиция:"
msgid "Fooling:"
msgstr "Fooling:"
msgid "Testing desync strategies..."
msgstr "Тестирование стратегий обхода..."
msgid "No working desync strategy found for this domain."
msgstr "Рабочая стратегия обхода для этого домена не найдена."
msgid "Section domains:"
msgstr "Домены секции:"
msgid "Testing desync strategy:"
msgstr "Тестирование стратегии:"
msgid "No working desync strategy found for section domains."
msgstr "Рабочая стратегия обхода для доменов секции не найдена."
msgid "Optimal strategy found and applied: "
msgstr "Оптимальная стратегия найдена и применена: "
msgid "DPI Blocked"
msgstr "Заблокировано ТСПУ"
msgid "Applied"
msgstr "Применено"
msgid "Apply"
msgstr "Применить"
msgid "Checking\u2026"
msgstr ""
msgid "Init Packet Magic Header (S1)"
msgstr "Magic Header: Инициализация (S1)"
msgid "Init Packet Magic Header (S3)"
msgstr "Magic Header: Инициализация (S3)"
msgid "Init Packet Payload (I1)"
msgstr "Payload: Инициализация (I1)"
msgid "Init Packet Payload (I2)"
msgstr "Payload: Инициализация (I2)"
msgid "Init Packet Payload (I3)"
msgstr "Payload: Инициализация (I3)"
msgid "Init Packet Payload (I4)"
msgstr "Payload: Инициализация (I4)"
msgid "Init Packet Payload (I5)"
msgstr "Payload: Инициализация (I5)"
msgid "Invalid Shadowsocks URL: missing method and password separator ":""
msgstr "Недопустимый Shadowsocks URL: отсутствует разделитель метода и пароля \":\""
msgid "Invalid line: "%s". Use: domain ip or ip domain"
msgstr "Неверная строка: \"%s\". Формат: домен ip или ip домен"
msgid "Invalid path format. Path must start with "/" and contain valid characters"
msgstr "Неверный формат пути. Должен начинаться с \"/\" и содержать допустимые символы"
msgid "Junk Max Size (Jmax)"
msgstr "Макс. размер Junk (Jmax)"
msgid "Junk Min Size (Jmin)"
msgstr "Мин. размер Junk (Jmin)"
msgid "Junk Packet Count (Jc)"
msgstr "Количество Junk-пакетов (Jc)"
msgid "Option "%s" contains an invalid input value."
msgstr "Опция \"%s\" содержит недопустимое значение."
msgid "Preset"
msgstr "Пресет"
msgid "Response Packet Magic Header (S2)"
msgstr "Magic Header: Ответ (S2)"
msgid "Response Packet Magic Header (S4)"
msgstr "Magic Header: Ответ (S4)"
msgid "Starting\u2026"
msgstr "Запуск\u2026"
msgid "Status / RTT"
msgstr "Статус / Пинг"
msgid "Stopping\u2026"
msgstr "Остановка\u2026"
msgid "Underload Packet Magic Header (H1)"
msgstr "Magic Header: Underload (H1)"
msgid "Underload Packet Magic Header (H2)"
msgstr "Magic Header: Underload (H2)"
msgid "Underload Packet Magic Header (H3)"
msgstr "Magic Header: Underload (H3)"
msgid "Underload Packet Magic Header (H4)"
msgstr "Magic Header: Underload (H4)"
msgid "\u2714 Running"
msgstr "\u2714 Работает"
msgid "\u2718 Stopped"
msgstr "\u2718 Остановлено"
msgid "Close"
msgstr "Закрыть"
msgid "Not set"
msgstr "Не заданы"
msgid "Section action"
msgstr "Действие секции"
msgid "Community Lists"
msgstr "Списки сообщества (Community Lists)"
msgid "Domains"
msgstr "Домены (Domains)"
msgid "IP Addresses (IP CIDR)"
msgstr "IP-адреса и подсети (IP CIDR)"
msgid "Rule Sets (.srs / .json)"
msgstr "Наборы правил (.srs / .json)"
msgid ".lst Lists"
msgstr ".lst списки"
msgid "Source Devices"
msgstr "Устройства-источники"
msgid "Ports"
msgstr "Порты"
msgid "Section Rules Content"
msgstr "Содержимое правил секции"
File diff suppressed because it is too large Load Diff
+2
View File
@@ -39,6 +39,8 @@ config settings 'settings'
option exclude_ntp '0'
option shutdown_correctly '0'
option smart_detect '0'
option game_console_optimizer '0'
list game_console_ips ''
# smart_detect: auto-detects blocked domains from logs and adds them to the first
# section in smart_detect_sections that works via proxy.
# list smart_detect_sections 'zapret-section'
+28 -2
View File
@@ -869,8 +869,32 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
if (!nft_add_rule(table, "mangle", [ "ct", "status", "dnat", "return" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "daddr", "@" + as_string(localv4_set), "return" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "daddr", "@" + as_string(localv6_set), "return" ]) ||
!nft_add_rule(table, "mangle", [ "jump", "priority_rules" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "daddr", "@" + as_string(localv6_set), "return" ]))
return false;
let console_ips_list = list_option(uci_settings(), "game_console_ips");
if (bool_option(uci_settings(), "game_console_optimizer", false) && length(console_ips_list) > 0) {
let v4_ips = [];
let v6_ips = [];
for (let ip in console_ips_list) {
if (core_ip.ip_family(ip) == 4) push(v4_ips, ip);
else if (core_ip.ip_family(ip) == 6) push(v6_ips, ip);
}
if (length(v4_ips) > 0) {
if (!nft_create_ipv4_set(table, "tachyon_consoles") ||
!nft_add_set_elements(table, "tachyon_consoles", join(",", v4_ips)) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "saddr", "@tachyon_consoles", "meta", "l4proto", "udp", "counter", "return" ]))
return false;
}
if (length(v6_ips) > 0) {
if (!nft_create_ipv6_set(table, "tachyon_consoles6") ||
!nft_add_set_elements(table, "tachyon_consoles6", join(",", v6_ips)) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "saddr", "@tachyon_consoles6", "meta", "l4proto", "udp", "counter", "return" ]))
return false;
}
}
if (!nft_add_rule(table, "mangle", [ "jump", "priority_rules" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "daddr", "@" + as_string(common_set), "meta", "l4proto", "tcp", "meta", "mark", "set", fakeip_mark, "counter" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip", "daddr", "@" + as_string(common_set), "meta", "l4proto", "udp", "meta", "mark", "set", fakeip_mark, "counter" ]) ||
!nft_add_rule(table, "mangle", [ "iifname", "@" + as_string(interface_set), "ip6", "daddr", "@" + as_string(common6_set), "meta", "l4proto", "tcp", "meta", "mark", "set", fakeip_mark, "counter" ]) ||
@@ -1589,6 +1613,8 @@ function nft_runtime_signature_from_settings_and_sections(settings, sections) {
body = signature_add_value(body, "settings.source_network_interfaces", option(settings, "source_network_interfaces", "br-lan"));
body = signature_add_value(body, "settings.exclude_ntp", bool_option(settings, "exclude_ntp", false) ? "1" : "0");
body = signature_add_value(body, "settings.game_console_optimizer", option(settings, "game_console_optimizer", "0"));
body = signature_add_value(body, "settings.game_console_ips", option(settings, "game_console_ips", ""));
for (let section in sections)
body = nft_rule_signature_body(body, object_or_empty(section));