feat(updates): live operation log with page lock in progress modal
The progress modal faked its steps on a timer because the worker only wrote to syslog. Now every updates_log line is appended to <job>.log (UPDATES_JOB_LOG), the running state advertises log_path, the log survives job completion until the TTL sweep, and the new component_action_log CLI mode serves it from a byte offset. The UI polls it into a monospace panel with auto-scroll, exposes a Copy log button, and the modal cannot be closed while the job runs (only the disabled progress button is shown).
This commit is contained in:
@@ -76,6 +76,10 @@ const dictionary = {
|
||||
"Reloading page in": "Перезагрузка страницы через",
|
||||
"Please wait, operation is running...": "Пожалуйста, подождите, выполняется операция...",
|
||||
"Operation failed": "Операция не выполнена",
|
||||
"Operation log": "Лог операции",
|
||||
"Copy log": "Копировать лог",
|
||||
"Operation log is not available": "Лог операции недоступен",
|
||||
"Failed to read operation log": "Не удалось прочитать лог операции",
|
||||
"Direct": "Напрямую",
|
||||
|
||||
// === Новые UI-строки (settings.js, section.js) ===
|
||||
|
||||
+497
-420
File diff suppressed because it is too large
Load Diff
+417
-371
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-13 20:17+0300\n"
|
||||
"PO-Revision-Date: 2026-08-13 20:17+0300\n"
|
||||
"POT-Creation-Date: 2026-08-14 13:16+0300\n"
|
||||
"PO-Revision-Date: 2026-08-14 13:16+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -527,6 +527,9 @@ msgstr "Условия"
|
||||
msgid "Config File Path"
|
||||
msgstr "Путь к файлу конфигурации"
|
||||
|
||||
msgid "Config Snapshots"
|
||||
msgstr "Снапшоты конфигурации"
|
||||
|
||||
msgid "Configure User-Agent in the item settings"
|
||||
msgstr "Настройте User-Agent в параметрах элемента"
|
||||
|
||||
@@ -560,6 +563,9 @@ msgstr "Скопировано"
|
||||
msgid "Copy"
|
||||
msgstr "Копировать"
|
||||
|
||||
msgid "Copy log"
|
||||
msgstr "Копировать лог"
|
||||
|
||||
msgid "Copy proxy link"
|
||||
msgstr "Скопировать ссылку прокси"
|
||||
|
||||
@@ -614,6 +620,15 @@ msgstr "Задержка в миллисекундах перед перезаг
|
||||
msgid "Delay value cannot be empty"
|
||||
msgstr "Значение задержки не может быть пустым"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
msgid "Delete Snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Delete the snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Detect server country"
|
||||
msgstr "Определять страну сервера"
|
||||
|
||||
@@ -854,6 +869,9 @@ msgstr "напр. google.com, 1.1.1.1, 443"
|
||||
msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "напр. SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Each entry must be \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp), e.g. tcp:6881,udp:6881-6889"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "Включить"
|
||||
|
||||
@@ -1031,6 +1049,9 @@ msgstr "Не удалось закрыть подключения"
|
||||
msgid "Failed to copy!"
|
||||
msgstr "Не удалось скопировать!"
|
||||
|
||||
msgid "Failed to delete snapshot"
|
||||
msgstr "Не удалось удалить снапшот"
|
||||
|
||||
msgid "Failed to execute"
|
||||
msgstr "Не удалось выполнить"
|
||||
|
||||
@@ -1040,18 +1061,27 @@ msgstr "Не удалось создать отчёт об ошибке"
|
||||
msgid "Failed to generate WARP config:"
|
||||
msgstr "Не удалось сгенерировать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
msgid "Failed to parse file:"
|
||||
msgstr "Ошибка разбора файла:"
|
||||
|
||||
msgid "Failed to parse WARP config:"
|
||||
msgstr "Не удалось разобрать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to read operation log"
|
||||
msgstr "Не удалось прочитать лог операции"
|
||||
|
||||
msgid "Failed to receive response from AI service."
|
||||
msgstr "Не удалось получить ответ от ИИ-сервиса."
|
||||
|
||||
msgid "Failed to reset settings"
|
||||
msgstr "Не удалось сбросить настройки"
|
||||
|
||||
msgid "Failed to restore snapshot"
|
||||
msgstr "Не удалось восстановить снапшот"
|
||||
|
||||
msgid "Failed to save AI Watchdog settings"
|
||||
msgstr "Ошибка сохранения настроек AI Watchdog"
|
||||
|
||||
@@ -1064,6 +1094,9 @@ msgstr "Не удалось сохранить DNS Turbo Cache"
|
||||
msgid "Failed to save Smart Detect settings"
|
||||
msgstr "Ошибка сохранения настроек Smart Detect"
|
||||
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to start bot"
|
||||
msgstr "Не удалось запустить бота"
|
||||
|
||||
@@ -1181,6 +1214,9 @@ msgstr "GET-эндпоинты открыты в локальной сети. PO
|
||||
msgid "Get global check"
|
||||
msgstr "Получить глобальную проверку"
|
||||
|
||||
msgid "Give the snapshot a name. It is saved with the current date and can be restored from this list later."
|
||||
msgstr "Дайте снапшоту имя. Он сохраняется с текущей датой, и его можно будет восстановить из этого списка позже."
|
||||
|
||||
msgid "Global check"
|
||||
msgstr "Глобальная проверка"
|
||||
|
||||
@@ -1895,6 +1931,9 @@ msgstr "Нет правил, использующих Zapret"
|
||||
msgid "No rules use Zapret2"
|
||||
msgstr "Нет правил, использующих Zapret2"
|
||||
|
||||
msgid "No snapshots yet"
|
||||
msgstr "Снапшотов пока нет"
|
||||
|
||||
msgid "Node list is empty"
|
||||
msgstr "Список узлов пуст"
|
||||
|
||||
@@ -2003,6 +2042,12 @@ msgstr "Операция не выполнена"
|
||||
msgid "Operation in progress..."
|
||||
msgstr "Выполняется операция..."
|
||||
|
||||
msgid "Operation log"
|
||||
msgstr "Лог операции"
|
||||
|
||||
msgid "Operation log is not available"
|
||||
msgstr "Лог операции недоступен"
|
||||
|
||||
msgid "Operation timed out"
|
||||
msgstr "Время ожидания истекло"
|
||||
|
||||
@@ -2042,14 +2087,11 @@ msgstr "Переопределить стандартные заголовки
|
||||
msgid "Overwrite existing records (if unchecked, merge with existing records)"
|
||||
msgstr "Перезаписать существующие записи (если снято — объединить с существующими)"
|
||||
|
||||
msgid "P2P Leak Protection"
|
||||
msgstr "Защита от утечек P2P"
|
||||
|
||||
msgid "P2P Client Ports"
|
||||
msgstr "Порты P2P-клиента"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
msgid "P2P Leak Protection"
|
||||
msgstr "Защита от утечек P2P"
|
||||
|
||||
msgid "Parsed records:"
|
||||
msgstr "Обработанные записи:"
|
||||
@@ -2111,6 +2153,9 @@ msgstr "Постоянное умное определение"
|
||||
msgid "Pin on dashboard"
|
||||
msgstr "Закрепить на дашборде"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
|
||||
msgid "Please wait, operation is running..."
|
||||
msgstr "Пожалуйста, подождите, выполняется операция..."
|
||||
|
||||
@@ -2135,9 +2180,15 @@ msgstr "Порт должен быть от 1 до 65535"
|
||||
msgid "Port range start must be less than or equal to end"
|
||||
msgstr "Начало диапазона портов должно быть меньше или равно концу"
|
||||
|
||||
msgid "Port range start must not exceed its end"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ports"
|
||||
msgstr "Порты"
|
||||
|
||||
msgid "Ports must be between 1 and 65535"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pre-download all preset lists"
|
||||
msgstr "Предзагрузить все предустановленные списки"
|
||||
|
||||
@@ -2315,6 +2366,12 @@ msgstr "Перезапустить Tachyon"
|
||||
msgid "Restarts"
|
||||
msgstr "Перезапуски"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restore Snapshot"
|
||||
msgstr "Восстановить снапшот"
|
||||
|
||||
msgid "Restores the Tachyon factory defaults: all rules, subscriptions, custom DNS servers and other options will be lost."
|
||||
msgstr "Восстанавливает заводские настройки Tachyon: все правила, подписки, пользовательские DNS-серверы и другие параметры будут удалены."
|
||||
|
||||
@@ -2390,12 +2447,18 @@ msgstr "Россия (ru)"
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
msgid "Save a snapshot of the working config and restore it later if something breaks."
|
||||
msgstr "Сохранить снапшот рабочей конфигурации, чтобы восстановить её позже, если что-то сломается."
|
||||
|
||||
msgid "Save AI Watchdog Settings"
|
||||
msgstr "Сохранить настройки AI Watchdog"
|
||||
|
||||
msgid "Save Smart Detect Settings"
|
||||
msgstr "Сохранить настройки Smart Detect"
|
||||
|
||||
msgid "Save Snapshot"
|
||||
msgstr "Сохранить снапшот"
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr "Сохранение…"
|
||||
|
||||
@@ -2651,6 +2714,12 @@ msgstr "Smart Detect"
|
||||
msgid "Smart Detect settings saved"
|
||||
msgstr "Настройки Smart Detect сохранены"
|
||||
|
||||
msgid "Snapshot name"
|
||||
msgstr "Имя снапшота"
|
||||
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
msgid "Snell Version"
|
||||
msgstr "Версия Snell"
|
||||
|
||||
@@ -2819,6 +2888,9 @@ msgstr "Служба Tachyon остановлена. Запустите серв
|
||||
msgid "Tachyon service is stopped. Start the service to display the dashboard."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения дашборда."
|
||||
|
||||
msgid "Tachyon update did not complete within the timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tachyon will not modify your DHCP configuration"
|
||||
msgstr "Tachyon не будет изменять вашу конфигурацию DHCP"
|
||||
|
||||
@@ -2930,6 +3002,9 @@ msgstr "Этот параметр требует значение."
|
||||
msgid "This will erase all Tachyon settings and restore the factory defaults. The service will restart. This action cannot be undone."
|
||||
msgstr "Все настройки Tachyon будут удалены и восстановлены заводские значения. Сервис будет перезапущен. Это действие нельзя отменить."
|
||||
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Время"
|
||||
|
||||
@@ -3095,6 +3170,9 @@ msgstr "Неуказанный IP-адрес не разрешён"
|
||||
msgid "Unstable"
|
||||
msgstr "Нестабильно"
|
||||
|
||||
msgid "Up to 10 snapshots are kept; the oldest ones are removed automatically."
|
||||
msgstr "Хранится до 10 снапшотов; самые старые удаляются автоматически."
|
||||
|
||||
msgid "Update"
|
||||
msgstr "Обновить"
|
||||
|
||||
@@ -3310,453 +3388,3 @@ msgstr "sing-box outbound для Zapret2 настроен"
|
||||
|
||||
msgid "Zapret2 sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret2 не настроен"
|
||||
|
||||
msgid "Service Health Check"
|
||||
msgstr "Проверка доступности сервисов"
|
||||
|
||||
msgid "Total Targets"
|
||||
msgstr "Всего целей"
|
||||
|
||||
msgid "Passed"
|
||||
msgstr "Успешно"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "Ошибки"
|
||||
|
||||
msgid "Avg Latency"
|
||||
msgstr "Ср. задержка"
|
||||
|
||||
msgid "Search domain..."
|
||||
msgstr "Поиск домена..."
|
||||
|
||||
msgid "Section / Target"
|
||||
msgstr "Секция / Цель"
|
||||
|
||||
msgid "Outbound Route"
|
||||
msgstr "Маршрут"
|
||||
|
||||
msgid "Resolved IP / DNS"
|
||||
msgstr "IP / DNS"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Check domain or IP (e.g. example.com)..."
|
||||
msgstr "Проверить домен или IP (напр. example.com)..."
|
||||
|
||||
msgid "Check Custom"
|
||||
msgstr "Проверить свой"
|
||||
|
||||
msgid "Check All"
|
||||
msgstr "Проверить все"
|
||||
|
||||
msgid "Check Again"
|
||||
msgstr "Проверить снова"
|
||||
|
||||
msgid "Testing..."
|
||||
msgstr "Тестирование..."
|
||||
|
||||
msgid "Fetching service targets..."
|
||||
msgstr "Получение списка сервисов..."
|
||||
|
||||
msgid "Failed to run service check."
|
||||
msgstr "Не удалось выполнить проверку сервисов."
|
||||
|
||||
msgid "Failed to parse target list."
|
||||
msgstr "Ошибка обработки списка целей."
|
||||
|
||||
msgid "No targets found in section configs."
|
||||
msgstr "Цели в конфигурациях секций не найдены."
|
||||
|
||||
msgid "Check YouTube availability"
|
||||
msgstr "Проверить доступность YouTube"
|
||||
|
||||
msgid "Check Mode:"
|
||||
msgstr "Режим проверки:"
|
||||
|
||||
msgid "Active Sections"
|
||||
msgstr "Активные секции"
|
||||
|
||||
msgid "All Profiles"
|
||||
msgstr "Все сервисы и профили"
|
||||
|
||||
msgid "Sections"
|
||||
msgstr "Секции"
|
||||
|
||||
msgid "Servers"
|
||||
msgstr "Серверы"
|
||||
|
||||
msgid "Monitoring"
|
||||
msgstr "Мониторинг"
|
||||
|
||||
msgid "Components"
|
||||
msgstr "Компоненты"
|
||||
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетащите строки для изменения приоритета. Правило вверху проверяется первым."
|
||||
|
||||
msgid "Rules"
|
||||
msgstr "Правила"
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Редактировать"
|
||||
|
||||
msgid "Connection"
|
||||
msgstr "Соединение"
|
||||
|
||||
msgid "Add a section"
|
||||
msgstr "Добавить секцию"
|
||||
|
||||
msgid "Add a server inbound"
|
||||
msgstr "Добавить серверный инбаунд"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
msgid "Section"
|
||||
msgstr "Секция"
|
||||
|
||||
msgid "Server"
|
||||
msgstr "Сервер"
|
||||
|
||||
msgid "Installing"
|
||||
msgstr "Установка"
|
||||
|
||||
msgid "Initializing operation..."
|
||||
msgstr "Инициализация операции..."
|
||||
|
||||
msgid "Installing package & updating configurations..."
|
||||
msgstr "Установка пакета и обновление настроек..."
|
||||
|
||||
msgid "Installation completed successfully!"
|
||||
msgstr "Установка успешно завершена!"
|
||||
|
||||
msgid "Direct"
|
||||
msgstr "Напрямую"
|
||||
|
||||
msgid "Tachyon Settings"
|
||||
msgstr "Настройки Tachyon"
|
||||
|
||||
msgid "All Services"
|
||||
msgstr "Все сервисы"
|
||||
|
||||
msgid "Service Availability Check"
|
||||
msgstr "Проверка доступности сервисов"
|
||||
|
||||
msgid "Filter by service:"
|
||||
msgstr "Фильтр по сервису:"
|
||||
|
||||
msgid "Search domain or service..."
|
||||
msgstr "Поиск домена или сервиса..."
|
||||
|
||||
msgid "Service / Target"
|
||||
msgstr "Сервис / Цель"
|
||||
|
||||
msgid "Route Type"
|
||||
msgstr "Тип маршрута"
|
||||
|
||||
msgid "Active Routes"
|
||||
msgstr "Активные маршруты"
|
||||
|
||||
msgid "Custom Domains"
|
||||
msgstr "Свои домены"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "Недоступно"
|
||||
|
||||
msgid "Available"
|
||||
msgstr "Доступно"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Авто"
|
||||
|
||||
msgid "Proxy"
|
||||
msgstr "Прокси"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TLS"
|
||||
msgstr "TLS"
|
||||
|
||||
msgid "Method"
|
||||
msgstr "Метод"
|
||||
|
||||
msgid "ALPN"
|
||||
msgstr "ALPN"
|
||||
|
||||
msgid "VLESS flow"
|
||||
msgstr "VLESS flow"
|
||||
|
||||
msgid "FakeTLS host"
|
||||
msgstr "FakeTLS host"
|
||||
|
||||
msgid "Concurrency"
|
||||
msgstr "Параллелизм"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
msgid "Information"
|
||||
msgstr "Информация"
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr "Сохранение..."
|
||||
|
||||
msgid "Generating..."
|
||||
msgstr "Генерация..."
|
||||
|
||||
msgid "Starting..."
|
||||
msgstr "Запуск..."
|
||||
|
||||
msgid "Stopping..."
|
||||
msgstr "Остановка..."
|
||||
|
||||
msgid "None"
|
||||
msgstr "Нет"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
msgid "Configuration for Tachyon service"
|
||||
msgstr "Конфигурация сервиса Tachyon"
|
||||
|
||||
msgid "Only the specified servers"
|
||||
msgstr "Только указанные серверы"
|
||||
|
||||
msgid "Select all"
|
||||
msgstr "Выбрать все"
|
||||
|
||||
msgid "Routing mode"
|
||||
msgstr "Режим маршрутизации"
|
||||
|
||||
msgid "Routing section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Keeps FakeIP cache persistent across reboots and pre-resolves popular blocked domains on startup so first-visit latency is 0 ms."
|
||||
msgstr "Сохраняет кэш FakeIP между перезагрузками и предварительно разрешает популярные заблокированные домены при запуске, чтобы задержка первого обращения была 0 мс."
|
||||
|
||||
msgid "Configure User-Agent в the item settings"
|
||||
msgstr "Настройте User-Agent в параметрах элемента"
|
||||
|
||||
msgid "Client link"
|
||||
msgstr "Клиентская ссылка"
|
||||
|
||||
msgid "Client link QR code"
|
||||
msgstr "QR-код клиентской ссылки"
|
||||
|
||||
msgid "Client link is not available yet"
|
||||
msgstr "Клиентская ссылка пока недоступна"
|
||||
|
||||
msgid "Loading client link..."
|
||||
msgstr "Загрузка клиентской ссылки..."
|
||||
|
||||
msgid "Secret"
|
||||
msgstr "Секрет"
|
||||
|
||||
msgid "Public Key"
|
||||
msgstr "Публичный ключ"
|
||||
|
||||
msgid "Certificate path"
|
||||
msgstr "Путь к сертификату"
|
||||
|
||||
msgid "Key path"
|
||||
msgstr "Путь к ключу"
|
||||
|
||||
msgid "Specify a file path"
|
||||
msgstr "Укажите путь к файлу"
|
||||
|
||||
msgid "Specify different files"
|
||||
msgstr "Укажите разные файлы"
|
||||
|
||||
msgid "Selected section"
|
||||
msgstr "Выбранная секция"
|
||||
|
||||
msgid "Inbound JSON"
|
||||
msgstr "Inbound JSON"
|
||||
|
||||
msgid "Inbound JSON cannot be empty"
|
||||
msgstr "Inbound JSON не может быть пустым"
|
||||
|
||||
msgid "Inbound JSON must be an object with a type field"
|
||||
msgstr "Inbound JSON должен быть объектом с полем type"
|
||||
|
||||
msgid "Server information"
|
||||
msgstr "Информация о сервере"
|
||||
|
||||
msgid "Server name"
|
||||
msgstr "Имя сервера"
|
||||
|
||||
msgid "Server name / SNI"
|
||||
msgstr "Имя сервера / SNI"
|
||||
|
||||
msgid "Reality fake site"
|
||||
msgstr "Фейковый сайт Reality"
|
||||
|
||||
msgid "Reality fake site port"
|
||||
msgstr "Порт фейкового сайта Reality"
|
||||
|
||||
msgid "Reality max time difference"
|
||||
msgstr "Макс. разница времени Reality"
|
||||
|
||||
msgid "Reality private key"
|
||||
msgstr "Приватный ключ Reality"
|
||||
|
||||
msgid "Reality public key"
|
||||
msgstr "Публичный ключ Reality"
|
||||
|
||||
msgid "Reality short ID"
|
||||
msgstr "Короткий ID Reality"
|
||||
|
||||
msgid "Reality short ID must contain 1-8 hex digits"
|
||||
msgstr "Короткий ID Reality должен содержать 1-8 шестнадцатеричных цифр"
|
||||
|
||||
msgid "Fronting IP"
|
||||
msgstr "Fronting IP"
|
||||
|
||||
msgid "Fronting port"
|
||||
msgstr "Fronting порт"
|
||||
|
||||
msgid "Fronting proxy protocol"
|
||||
msgstr "Протокол Fronting прокси"
|
||||
|
||||
msgid "gRPC service"
|
||||
msgstr "Сервис gRPC"
|
||||
|
||||
msgid "Transport host"
|
||||
msgstr "Хост транспорта"
|
||||
|
||||
msgid "Transport path"
|
||||
msgstr "Путь транспорта"
|
||||
|
||||
msgid "Transport path must start with /"
|
||||
msgstr "Путь транспорта должен начинаться с /"
|
||||
|
||||
msgid "HTTP hosts"
|
||||
msgstr "HTTP хосты"
|
||||
|
||||
msgid "XHTTP mode"
|
||||
msgstr "Режим XHTTP"
|
||||
|
||||
msgid "Fallback on unknown DC"
|
||||
msgstr "Fallback при неизвестном DC"
|
||||
|
||||
msgid "Padding"
|
||||
msgstr "Padding"
|
||||
|
||||
msgid "Handshake timeout"
|
||||
msgstr "Таймаут рукопожатия"
|
||||
|
||||
msgid "Time skew tolerance"
|
||||
msgstr "Допуск отклонения времени"
|
||||
|
||||
msgid "Invalid NFQWS strategy"
|
||||
msgstr "Неверная стратегия NFQWS"
|
||||
|
||||
msgid "Invalid NFQWS2 strategy"
|
||||
msgstr "Неверная стратегия NFQWS2"
|
||||
|
||||
msgid "Installed build \\u2192 Available build"
|
||||
msgstr "Установленная сборка \\u2192 Доступная сборка"
|
||||
|
||||
msgid "Prevent multiple reload_firewall calls within 120 seconds to avoid connection drops."
|
||||
msgstr "Предотвращать множественные вызовы reload_firewall в течение 120 секунд для избежания разрыва соединений."
|
||||
|
||||
msgid "Obfuscation"
|
||||
msgstr "Обфускация"
|
||||
|
||||
msgid "Obfuscation password"
|
||||
msgstr "Пароль обфускации"
|
||||
|
||||
msgid "Auto update"
|
||||
msgstr "Автообновление"
|
||||
|
||||
msgid "Use CIDR prefixes like 192.168.1.0/24 or 2001:db8::/32"
|
||||
msgstr "Используйте CIDR-префиксы, например 192.168.1.0/24 или 2001:db8::/32"
|
||||
|
||||
msgid "Use an IP address"
|
||||
msgstr "Используйте IP-адрес"
|
||||
|
||||
msgid "Use an IP listen address"
|
||||
msgstr "Используйте IP-адрес прослушивания"
|
||||
|
||||
msgid "Use a domain name or IP address"
|
||||
msgstr "Используйте имя домена или IP-адрес"
|
||||
|
||||
msgid "Use an HTTP or HTTPS URL"
|
||||
msgstr "Используйте HTTP или HTTPS URL"
|
||||
|
||||
msgid "Use letters, digits, dots, underscores, or hyphens"
|
||||
msgstr "Используйте буквы, цифры, точки, подчёркивания или дефисы"
|
||||
|
||||
msgid "Use a non-negative integer"
|
||||
msgstr "Используйте неотрицательное целое число"
|
||||
|
||||
msgid "Use a non-zero secret"
|
||||
msgstr "Используйте ненулевой секрет"
|
||||
|
||||
msgid "Use 32 hex characters"
|
||||
msgstr "Используйте 32 шестнадцатеричных символа"
|
||||
|
||||
msgid "Use a duration like 30s, 5m, or 1h30m"
|
||||
msgstr "Используйте длительность, например 30s, 5m или 1h30m"
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидалось: %s"
|
||||
|
||||
msgid "Cannot save server"
|
||||
msgstr "Не удалось сохранить сервер"
|
||||
|
||||
msgid "QR code is too large"
|
||||
msgstr "QR-код слишком большой"
|
||||
|
||||
msgid "Save Snapshot"
|
||||
msgstr "Сохранить снапшот"
|
||||
|
||||
msgid "Snapshot name"
|
||||
msgstr "Имя снапшота"
|
||||
|
||||
msgid "Config Snapshots"
|
||||
msgstr "Снапшоты конфигурации"
|
||||
|
||||
msgid "Save a snapshot of the working config and restore it later if something breaks."
|
||||
msgstr "Сохранить снапшот рабочей конфигурации, чтобы восстановить её позже, если что-то сломается."
|
||||
|
||||
msgid "Give the snapshot a name. It is saved with the current date and can be restored from this list later."
|
||||
msgstr "Дайте снапшоту имя. Он сохраняется с текущей датой, и его можно будет восстановить из этого списка позже."
|
||||
|
||||
msgid "No snapshots yet"
|
||||
msgstr "Снапшотов пока нет"
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to delete snapshot"
|
||||
msgstr "Не удалось удалить снапшот"
|
||||
|
||||
msgid "Failed to restore snapshot"
|
||||
msgstr "Не удалось восстановить снапшот"
|
||||
|
||||
msgid "Restore Snapshot"
|
||||
msgstr "Восстановить снапшот"
|
||||
|
||||
msgid "Delete Snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Delete the snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Up to 10 snapshots are kept; the oldest ones are removed automatically."
|
||||
msgstr "Хранится до 10 снапшотов; самые старые удаляются автоматически."
|
||||
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
@@ -604,6 +604,34 @@ export const TachyonShellMethods = {
|
||||
data: parsedResponse,
|
||||
} as Tachyon.MethodSuccessResponse<Tachyon.ComponentActionResult>;
|
||||
},
|
||||
componentActionLog: async (jobId: string, offset = 0) => {
|
||||
const response = await executeShellCommand({
|
||||
command: '/usr/bin/tachyon',
|
||||
args: [
|
||||
Tachyon.AvailableMethods.COMPONENT_ACTION_LOG,
|
||||
jobId,
|
||||
String(Math.max(0, Math.floor(offset))),
|
||||
],
|
||||
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||
});
|
||||
const parsedResponse =
|
||||
parseJsonObjectOutput<Tachyon.ComponentActionLogResult>(response.stdout);
|
||||
|
||||
if ((response.code ?? 0) !== 0 || !parsedResponse) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
parsedResponse?.success === false
|
||||
? _('Operation log is not available')
|
||||
: response.stderr || _('Failed to read operation log'),
|
||||
} as Tachyon.MethodFailureResponse;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedResponse,
|
||||
} as Tachyon.MethodSuccessResponse<Tachyon.ComponentActionLogResult>;
|
||||
},
|
||||
componentUpdateCheckCache: async () =>
|
||||
callBaseMethod<Tachyon.ComponentUpdateCheckCache>(
|
||||
Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE,
|
||||
|
||||
@@ -668,6 +668,7 @@ async function followComponentActionState(
|
||||
targetVersion: state.latest_version,
|
||||
});
|
||||
}
|
||||
getActiveProgressModalController()?.startLogTracking(jobId);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -860,6 +861,7 @@ async function handleComponentAction(button: ComponentActionButton) {
|
||||
jobId = startResponse.data.job_id;
|
||||
setActiveProgressModalJobId(jobId);
|
||||
markUiActionOwned('component', jobId);
|
||||
modalController.startLogTracking(jobId);
|
||||
if (followedComponentJobs.has(jobId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { renderCheckIcon24, renderXIcon24 } from '../../../../icons';
|
||||
import { renderButton } from '../../../../partials';
|
||||
import { copyToClipboard } from '../../../../helpers/copyToClipboard';
|
||||
import { TachyonShellMethods } from '../../../methods';
|
||||
import { Tachyon } from '../../../types';
|
||||
|
||||
export interface UpdateProgressModalOptions {
|
||||
@@ -30,6 +32,9 @@ export interface UpdateProgressModalController {
|
||||
}) => void;
|
||||
completeSuccess: (message?: string, options?: CompleteSuccessOptions) => void;
|
||||
completeError: (errorMessage: string) => void;
|
||||
startLogTracking: (jobId: string) => void;
|
||||
stopLogTracking: () => void;
|
||||
getLogText: () => string;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
@@ -212,6 +217,29 @@ export function showUpdateProgressModal(
|
||||
steps[0] || _('Please wait, operation is running...'),
|
||||
);
|
||||
|
||||
const logPreEl = E('pre', {
|
||||
class: 'tachyon-update-modal__log',
|
||||
}) as HTMLPreElement;
|
||||
|
||||
const logPanelEl = E(
|
||||
'div',
|
||||
{
|
||||
class: 'tachyon-update-modal__log-panel',
|
||||
style: 'display: none;',
|
||||
},
|
||||
[
|
||||
E('div', { class: 'tachyon-update-modal__log-header' }, [
|
||||
E('b', {}, _('Operation log')),
|
||||
renderButton({
|
||||
classNames: ['cbi-button-action', 'tachyon-update-modal__log-copy'],
|
||||
text: _('Copy log'),
|
||||
onClick: copyLog,
|
||||
}),
|
||||
]),
|
||||
logPreEl,
|
||||
],
|
||||
);
|
||||
|
||||
const actionButtonContainer = E(
|
||||
'div',
|
||||
{ class: 'tachyon-update-modal__actions' },
|
||||
@@ -230,6 +258,7 @@ export function showUpdateProgressModal(
|
||||
progressBarTrackEl,
|
||||
stepListEl,
|
||||
statusMsgEl,
|
||||
logPanelEl,
|
||||
actionButtonContainer,
|
||||
]) as HTMLElement;
|
||||
|
||||
@@ -322,6 +351,94 @@ export function showUpdateProgressModal(
|
||||
}
|
||||
}
|
||||
|
||||
let logTrackingJobId: string | null = null;
|
||||
let logTrackingOffset = 0;
|
||||
let logFullText = '';
|
||||
let logPollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let logPollStopped = true;
|
||||
|
||||
function appendLogText(text: string) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
logFullText += text;
|
||||
logPreEl.textContent = logFullText;
|
||||
logPreEl.scrollTop = logPreEl.scrollHeight;
|
||||
}
|
||||
|
||||
async function pollLog() {
|
||||
if (logPollStopped || !logTrackingJobId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobId = logTrackingJobId;
|
||||
const offset = logTrackingOffset;
|
||||
|
||||
try {
|
||||
const response = await TachyonShellMethods.componentActionLog(
|
||||
jobId,
|
||||
offset,
|
||||
);
|
||||
if (logPollStopped || logTrackingJobId !== jobId) {
|
||||
return;
|
||||
}
|
||||
if (response.success && response.data) {
|
||||
logTrackingOffset = response.data.offset;
|
||||
appendLogText(response.data.log);
|
||||
}
|
||||
} catch (_error) {
|
||||
// A transient RPC failure must not stop the polling; the next tick retries.
|
||||
} finally {
|
||||
if (!logPollStopped && logTrackingJobId === jobId) {
|
||||
logPollTimer = setTimeout(pollLog, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopLogTracking() {
|
||||
logPollStopped = true;
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
logTrackingJobId = null;
|
||||
}
|
||||
|
||||
function finishLogTracking() {
|
||||
if (logPollStopped || !logTrackingJobId) {
|
||||
stopLogTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
const jobId = logTrackingJobId;
|
||||
const offset = logTrackingOffset;
|
||||
logPollStopped = true;
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
|
||||
void TachyonShellMethods.componentActionLog(jobId, offset)
|
||||
.then((response) => {
|
||||
if (response.success && response.data) {
|
||||
logTrackingOffset = response.data.offset;
|
||||
appendLogText(response.data.log);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
logTrackingJobId = null;
|
||||
});
|
||||
}
|
||||
|
||||
function copyLog() {
|
||||
if (!logFullText) {
|
||||
return;
|
||||
}
|
||||
|
||||
copyToClipboard(logFullText);
|
||||
}
|
||||
|
||||
const controller: UpdateProgressModalController = {
|
||||
updateStep: (stepIndex: number, statusText?: string) => {
|
||||
setStep(stepIndex, statusText);
|
||||
@@ -344,6 +461,7 @@ export function showUpdateProgressModal(
|
||||
completeSuccess: (message?: string, opts?: CompleteSuccessOptions) => {
|
||||
isCompleted = true;
|
||||
cleanupTimers();
|
||||
finishLogTracking();
|
||||
|
||||
progressBarFillEl.style.width = '100%';
|
||||
progressBarFillEl.classList.add(
|
||||
@@ -443,6 +561,7 @@ export function showUpdateProgressModal(
|
||||
completeError: (errorMessage: string) => {
|
||||
isCompleted = true;
|
||||
cleanupTimers();
|
||||
finishLogTracking();
|
||||
|
||||
progressBarFillEl.style.width = '100%';
|
||||
progressBarFillEl.classList.add(
|
||||
@@ -466,8 +585,29 @@ export function showUpdateProgressModal(
|
||||
|
||||
actionButtonContainer.replaceChildren(closeBtn);
|
||||
},
|
||||
startLogTracking: (jobId: string) => {
|
||||
if (!jobId || logTrackingJobId === jobId) {
|
||||
return;
|
||||
}
|
||||
|
||||
logTrackingJobId = jobId;
|
||||
logTrackingOffset = 0;
|
||||
logPollStopped = false;
|
||||
logPanelEl.style.display = '';
|
||||
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
void pollLog();
|
||||
},
|
||||
stopLogTracking: () => {
|
||||
stopLogTracking();
|
||||
},
|
||||
getLogText: () => logFullText,
|
||||
close: () => {
|
||||
cleanupTimers();
|
||||
stopLogTracking();
|
||||
if (activeModalController === controller) {
|
||||
activeModalController = null;
|
||||
activeModalJobId = null;
|
||||
|
||||
@@ -363,4 +363,39 @@ export const styles = `
|
||||
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-panel {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-copy {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log {
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color, #ddd);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
class MockMutationObserver {
|
||||
observe() {}
|
||||
@@ -72,6 +72,11 @@ const mockUi = {
|
||||
(globalThis as any)._ = (str: string) => str;
|
||||
|
||||
describe('renderUpdateProgressModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders install button and does not auto-close when onInstall is provided', async () => {
|
||||
vi.useFakeTimers();
|
||||
mockUi.showModal.mockReset();
|
||||
@@ -128,4 +133,88 @@ describe('renderUpdateProgressModal', () => {
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('polls the operation log while the job runs and stops after completion', async () => {
|
||||
vi.useFakeTimers();
|
||||
mockUi.showModal.mockReset();
|
||||
mockUi.hideModal.mockReset();
|
||||
|
||||
const { TachyonShellMethods } = await import('../../../methods');
|
||||
const { showUpdateProgressModal } = await import(
|
||||
'../partials/renderUpdateProgressModal'
|
||||
);
|
||||
|
||||
let nextOffset = 0;
|
||||
const logSpy = vi
|
||||
.spyOn(TachyonShellMethods, 'componentActionLog')
|
||||
.mockImplementation(async (_jobId: string, offset: number = 0) => {
|
||||
nextOffset = offset + 5;
|
||||
return {
|
||||
success: true,
|
||||
data: { success: true, log: 'chunk', offset: nextOffset },
|
||||
} as never;
|
||||
});
|
||||
|
||||
const controller = showUpdateProgressModal({
|
||||
component: 'sing_box',
|
||||
action: 'install',
|
||||
componentTitle: 'sing-box',
|
||||
});
|
||||
|
||||
// The first poll runs immediately, the following ones on the 1500ms timer.
|
||||
controller.startLogTracking('job-123');
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith('job-123', 0);
|
||||
expect(logSpy).toHaveBeenCalledWith('job-123', 5);
|
||||
expect(logSpy).toHaveBeenCalledWith('job-123', 10);
|
||||
expect(controller.getLogText()).toBe('chunkchunkchunk');
|
||||
|
||||
controller.completeSuccess('done');
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
// A final read drains the remaining log, then polling stops.
|
||||
expect(logSpy).toHaveBeenCalledWith('job-123', 15);
|
||||
expect(controller.getLogText()).toBe('chunkchunkchunkchunk');
|
||||
|
||||
const callsAfterCompletion = logSpy.mock.calls.length;
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(logSpy.mock.calls.length).toBe(callsAfterCompletion);
|
||||
|
||||
controller.close();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not poll the log when no job id was provided', async () => {
|
||||
vi.useFakeTimers();
|
||||
mockUi.showModal.mockReset();
|
||||
mockUi.hideModal.mockReset();
|
||||
|
||||
const { TachyonShellMethods } = await import('../../../methods');
|
||||
const { showUpdateProgressModal } = await import(
|
||||
'../partials/renderUpdateProgressModal'
|
||||
);
|
||||
|
||||
const logSpy = vi
|
||||
.spyOn(TachyonShellMethods, 'componentActionLog')
|
||||
.mockResolvedValue({
|
||||
success: true,
|
||||
data: { success: true, log: 'x', offset: 1 },
|
||||
} as never);
|
||||
|
||||
const controller = showUpdateProgressModal({
|
||||
component: 'sing_box',
|
||||
action: 'install',
|
||||
componentTitle: 'sing-box',
|
||||
});
|
||||
|
||||
controller.startLogTracking('');
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
expect(controller.getLogText()).toBe('');
|
||||
|
||||
controller.close();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,6 +90,7 @@ export namespace Tachyon {
|
||||
UI_ACTION_ACK = 'ui_action_ack',
|
||||
COMPONENT_ACTION_ASYNC = 'component_action_async',
|
||||
COMPONENT_ACTION_STATUS = 'component_action_status',
|
||||
COMPONENT_ACTION_LOG = 'component_action_log',
|
||||
COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache',
|
||||
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
|
||||
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
|
||||
@@ -648,6 +649,12 @@ export namespace Tachyon {
|
||||
results: ComponentActionResult[];
|
||||
}
|
||||
|
||||
export interface ComponentActionLogResult {
|
||||
success: boolean;
|
||||
log: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export type ComponentActionStartResult = UiActionStartResult;
|
||||
|
||||
export type SubscriptionUpdateStartResult = UiActionStartResult;
|
||||
|
||||
@@ -3053,6 +3053,7 @@ var Tachyon;
|
||||
AvailableMethods2["UI_ACTION_ACK"] = "ui_action_ack";
|
||||
AvailableMethods2["COMPONENT_ACTION_ASYNC"] = "component_action_async";
|
||||
AvailableMethods2["COMPONENT_ACTION_STATUS"] = "component_action_status";
|
||||
AvailableMethods2["COMPONENT_ACTION_LOG"] = "component_action_log";
|
||||
AvailableMethods2["COMPONENT_UPDATE_CHECK_CACHE"] = "component_update_check_cache";
|
||||
AvailableMethods2["SUBSCRIPTION_UPDATE_ASYNC"] = "subscription_update_async";
|
||||
AvailableMethods2["SUBSCRIPTION_UPDATE_STATUS"] = "subscription_update_status";
|
||||
@@ -3548,6 +3549,28 @@ var TachyonShellMethods = {
|
||||
data: parsedResponse
|
||||
};
|
||||
},
|
||||
componentActionLog: async (jobId, offset = 0) => {
|
||||
const response = await executeShellCommand({
|
||||
command: "/usr/bin/tachyon",
|
||||
args: [
|
||||
Tachyon.AvailableMethods.COMPONENT_ACTION_LOG,
|
||||
jobId,
|
||||
String(Math.max(0, Math.floor(offset)))
|
||||
],
|
||||
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
|
||||
});
|
||||
const parsedResponse = parseJsonObjectOutput(response.stdout);
|
||||
if ((response.code ?? 0) !== 0 || !parsedResponse) {
|
||||
return {
|
||||
success: false,
|
||||
error: parsedResponse?.success === false ? _("Operation log is not available") : response.stderr || _("Failed to read operation log")
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: parsedResponse
|
||||
};
|
||||
},
|
||||
componentUpdateCheckCache: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.COMPONENT_UPDATE_CHECK_CACHE
|
||||
),
|
||||
@@ -15383,6 +15406,27 @@ function showUpdateProgressModal(options) {
|
||||
{ class: "tachyon-update-modal__status-msg" },
|
||||
steps[0] || _("Please wait, operation is running...")
|
||||
);
|
||||
const logPreEl = E("pre", {
|
||||
class: "tachyon-update-modal__log"
|
||||
});
|
||||
const logPanelEl = E(
|
||||
"div",
|
||||
{
|
||||
class: "tachyon-update-modal__log-panel",
|
||||
style: "display: none;"
|
||||
},
|
||||
[
|
||||
E("div", { class: "tachyon-update-modal__log-header" }, [
|
||||
E("b", {}, _("Operation log")),
|
||||
renderButton({
|
||||
classNames: ["cbi-button-action", "tachyon-update-modal__log-copy"],
|
||||
text: _("Copy log"),
|
||||
onClick: copyLog
|
||||
})
|
||||
]),
|
||||
logPreEl
|
||||
]
|
||||
);
|
||||
const actionButtonContainer = E(
|
||||
"div",
|
||||
{ class: "tachyon-update-modal__actions" },
|
||||
@@ -15401,6 +15445,7 @@ function showUpdateProgressModal(options) {
|
||||
progressBarTrackEl,
|
||||
stepListEl,
|
||||
statusMsgEl,
|
||||
logPanelEl,
|
||||
actionButtonContainer
|
||||
]);
|
||||
const updateTimerDisplay = () => {
|
||||
@@ -15481,6 +15526,80 @@ function showUpdateProgressModal(options) {
|
||||
autoAdvanceTimer = null;
|
||||
}
|
||||
}
|
||||
let logTrackingJobId = null;
|
||||
let logTrackingOffset = 0;
|
||||
let logFullText = "";
|
||||
let logPollTimer = null;
|
||||
let logPollStopped = true;
|
||||
function appendLogText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
logFullText += text;
|
||||
logPreEl.textContent = logFullText;
|
||||
logPreEl.scrollTop = logPreEl.scrollHeight;
|
||||
}
|
||||
async function pollLog() {
|
||||
if (logPollStopped || !logTrackingJobId) {
|
||||
return;
|
||||
}
|
||||
const jobId = logTrackingJobId;
|
||||
const offset = logTrackingOffset;
|
||||
try {
|
||||
const response = await TachyonShellMethods.componentActionLog(
|
||||
jobId,
|
||||
offset
|
||||
);
|
||||
if (logPollStopped || logTrackingJobId !== jobId) {
|
||||
return;
|
||||
}
|
||||
if (response.success && response.data) {
|
||||
logTrackingOffset = response.data.offset;
|
||||
appendLogText(response.data.log);
|
||||
}
|
||||
} catch (_error) {
|
||||
} finally {
|
||||
if (!logPollStopped && logTrackingJobId === jobId) {
|
||||
logPollTimer = setTimeout(pollLog, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
function stopLogTracking() {
|
||||
logPollStopped = true;
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
logTrackingJobId = null;
|
||||
}
|
||||
function finishLogTracking() {
|
||||
if (logPollStopped || !logTrackingJobId) {
|
||||
stopLogTracking();
|
||||
return;
|
||||
}
|
||||
const jobId = logTrackingJobId;
|
||||
const offset = logTrackingOffset;
|
||||
logPollStopped = true;
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
void TachyonShellMethods.componentActionLog(jobId, offset).then((response) => {
|
||||
if (response.success && response.data) {
|
||||
logTrackingOffset = response.data.offset;
|
||||
appendLogText(response.data.log);
|
||||
}
|
||||
}).catch(() => {
|
||||
}).finally(() => {
|
||||
logTrackingJobId = null;
|
||||
});
|
||||
}
|
||||
function copyLog() {
|
||||
if (!logFullText) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(logFullText);
|
||||
}
|
||||
const controller = {
|
||||
updateStep: (stepIndex, statusText) => {
|
||||
setStep(stepIndex, statusText);
|
||||
@@ -15498,6 +15617,7 @@ function showUpdateProgressModal(options) {
|
||||
completeSuccess: (message, opts) => {
|
||||
isCompleted = true;
|
||||
cleanupTimers();
|
||||
finishLogTracking();
|
||||
progressBarFillEl.style.width = "100%";
|
||||
progressBarFillEl.classList.add(
|
||||
"tachyon-update-modal__progress-fill--success"
|
||||
@@ -15571,6 +15691,7 @@ function showUpdateProgressModal(options) {
|
||||
completeError: (errorMessage) => {
|
||||
isCompleted = true;
|
||||
cleanupTimers();
|
||||
finishLogTracking();
|
||||
progressBarFillEl.style.width = "100%";
|
||||
progressBarFillEl.classList.add(
|
||||
"tachyon-update-modal__progress-fill--error"
|
||||
@@ -15590,8 +15711,27 @@ function showUpdateProgressModal(options) {
|
||||
});
|
||||
actionButtonContainer.replaceChildren(closeBtn);
|
||||
},
|
||||
startLogTracking: (jobId) => {
|
||||
if (!jobId || logTrackingJobId === jobId) {
|
||||
return;
|
||||
}
|
||||
logTrackingJobId = jobId;
|
||||
logTrackingOffset = 0;
|
||||
logPollStopped = false;
|
||||
logPanelEl.style.display = "";
|
||||
if (logPollTimer) {
|
||||
clearTimeout(logPollTimer);
|
||||
logPollTimer = null;
|
||||
}
|
||||
void pollLog();
|
||||
},
|
||||
stopLogTracking: () => {
|
||||
stopLogTracking();
|
||||
},
|
||||
getLogText: () => logFullText,
|
||||
close: () => {
|
||||
cleanupTimers();
|
||||
stopLogTracking();
|
||||
if (activeModalController === controller) {
|
||||
activeModalController = null;
|
||||
activeModalJobId = null;
|
||||
@@ -16040,6 +16180,7 @@ async function followComponentActionState(state) {
|
||||
targetVersion: state.latest_version
|
||||
});
|
||||
}
|
||||
getActiveProgressModalController()?.startLogTracking(jobId);
|
||||
}
|
||||
try {
|
||||
const response = state.running ? await TachyonShellMethods.waitComponentActionJob(
|
||||
@@ -16195,6 +16336,7 @@ async function handleComponentAction(button) {
|
||||
jobId = startResponse.data.job_id;
|
||||
setActiveProgressModalJobId(jobId);
|
||||
markUiActionOwned("component", jobId);
|
||||
modalController.startLogTracking(jobId);
|
||||
if (followedComponentJobs.has(jobId)) {
|
||||
return;
|
||||
}
|
||||
@@ -17146,6 +17288,41 @@ var styles6 = `
|
||||
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-panel {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log-copy {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tachyon-update-modal__log {
|
||||
margin: 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color, #ddd);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
// src/tachyon/tabs/updates/index.ts
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-13 20:17+0300\n"
|
||||
"PO-Revision-Date: 2026-08-13 20:17+0300\n"
|
||||
"POT-Creation-Date: 2026-08-14 13:16+0300\n"
|
||||
"PO-Revision-Date: 2026-08-14 13:16+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -527,6 +527,9 @@ msgstr "Условия"
|
||||
msgid "Config File Path"
|
||||
msgstr "Путь к файлу конфигурации"
|
||||
|
||||
msgid "Config Snapshots"
|
||||
msgstr "Снапшоты конфигурации"
|
||||
|
||||
msgid "Configure User-Agent in the item settings"
|
||||
msgstr "Настройте User-Agent в параметрах элемента"
|
||||
|
||||
@@ -560,6 +563,9 @@ msgstr "Скопировано"
|
||||
msgid "Copy"
|
||||
msgstr "Копировать"
|
||||
|
||||
msgid "Copy log"
|
||||
msgstr "Копировать лог"
|
||||
|
||||
msgid "Copy proxy link"
|
||||
msgstr "Скопировать ссылку прокси"
|
||||
|
||||
@@ -614,6 +620,15 @@ msgstr "Задержка в миллисекундах перед перезаг
|
||||
msgid "Delay value cannot be empty"
|
||||
msgstr "Значение задержки не может быть пустым"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
msgid "Delete Snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Delete the snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Detect server country"
|
||||
msgstr "Определять страну сервера"
|
||||
|
||||
@@ -854,6 +869,9 @@ msgstr "напр. google.com, 1.1.1.1, 443"
|
||||
msgid "e.g. SHA256 — leave empty for default"
|
||||
msgstr "напр. SHA256 — оставьте пустым для значения по умолчанию"
|
||||
|
||||
msgid "Each entry must be \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp), e.g. tcp:6881,udp:6881-6889"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "Включить"
|
||||
|
||||
@@ -1031,6 +1049,9 @@ msgstr "Не удалось закрыть подключения"
|
||||
msgid "Failed to copy!"
|
||||
msgstr "Не удалось скопировать!"
|
||||
|
||||
msgid "Failed to delete snapshot"
|
||||
msgstr "Не удалось удалить снапшот"
|
||||
|
||||
msgid "Failed to execute"
|
||||
msgstr "Не удалось выполнить"
|
||||
|
||||
@@ -1040,18 +1061,27 @@ msgstr "Не удалось создать отчёт об ошибке"
|
||||
msgid "Failed to generate WARP config:"
|
||||
msgstr "Не удалось сгенерировать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
msgid "Failed to parse file:"
|
||||
msgstr "Ошибка разбора файла:"
|
||||
|
||||
msgid "Failed to parse WARP config:"
|
||||
msgstr "Не удалось разобрать конфигурацию WARP:"
|
||||
|
||||
msgid "Failed to read operation log"
|
||||
msgstr "Не удалось прочитать лог операции"
|
||||
|
||||
msgid "Failed to receive response from AI service."
|
||||
msgstr "Не удалось получить ответ от ИИ-сервиса."
|
||||
|
||||
msgid "Failed to reset settings"
|
||||
msgstr "Не удалось сбросить настройки"
|
||||
|
||||
msgid "Failed to restore snapshot"
|
||||
msgstr "Не удалось восстановить снапшот"
|
||||
|
||||
msgid "Failed to save AI Watchdog settings"
|
||||
msgstr "Ошибка сохранения настроек AI Watchdog"
|
||||
|
||||
@@ -1064,6 +1094,9 @@ msgstr "Не удалось сохранить DNS Turbo Cache"
|
||||
msgid "Failed to save Smart Detect settings"
|
||||
msgstr "Ошибка сохранения настроек Smart Detect"
|
||||
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to start bot"
|
||||
msgstr "Не удалось запустить бота"
|
||||
|
||||
@@ -1181,6 +1214,9 @@ msgstr "GET-эндпоинты открыты в локальной сети. PO
|
||||
msgid "Get global check"
|
||||
msgstr "Получить глобальную проверку"
|
||||
|
||||
msgid "Give the snapshot a name. It is saved with the current date and can be restored from this list later."
|
||||
msgstr "Дайте снапшоту имя. Он сохраняется с текущей датой, и его можно будет восстановить из этого списка позже."
|
||||
|
||||
msgid "Global check"
|
||||
msgstr "Глобальная проверка"
|
||||
|
||||
@@ -1895,6 +1931,9 @@ msgstr "Нет правил, использующих Zapret"
|
||||
msgid "No rules use Zapret2"
|
||||
msgstr "Нет правил, использующих Zapret2"
|
||||
|
||||
msgid "No snapshots yet"
|
||||
msgstr "Снапшотов пока нет"
|
||||
|
||||
msgid "Node list is empty"
|
||||
msgstr "Список узлов пуст"
|
||||
|
||||
@@ -2003,6 +2042,12 @@ msgstr "Операция не выполнена"
|
||||
msgid "Operation in progress..."
|
||||
msgstr "Выполняется операция..."
|
||||
|
||||
msgid "Operation log"
|
||||
msgstr "Лог операции"
|
||||
|
||||
msgid "Operation log is not available"
|
||||
msgstr "Лог операции недоступен"
|
||||
|
||||
msgid "Operation timed out"
|
||||
msgstr "Время ожидания истекло"
|
||||
|
||||
@@ -2042,14 +2087,11 @@ msgstr "Переопределить стандартные заголовки
|
||||
msgid "Overwrite existing records (if unchecked, merge with existing records)"
|
||||
msgstr "Перезаписать существующие записи (если снято — объединить с существующими)"
|
||||
|
||||
msgid "P2P Leak Protection"
|
||||
msgstr "Защита от утечек P2P"
|
||||
|
||||
msgid "P2P Client Ports"
|
||||
msgstr "Порты P2P-клиента"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
msgid "P2P Leak Protection"
|
||||
msgstr "Защита от утечек P2P"
|
||||
|
||||
msgid "Parsed records:"
|
||||
msgstr "Обработанные записи:"
|
||||
@@ -2111,6 +2153,9 @@ msgstr "Постоянное умное определение"
|
||||
msgid "Pin on dashboard"
|
||||
msgstr "Закрепить на дашборде"
|
||||
|
||||
msgid "Pin P2P isolation to the torrent client's own ports, comma separated. Each entry is \"proto\", \"proto:port\" or \"proto:port-port\" (tcp/udp). Without a port the whole protocol is routed direct."
|
||||
msgstr "Закрепить изоляцию P2P за портами торрент-клиента, через запятую. Каждая запись — \"протокол\", \"протокол:порт\" или \"протокол:порт-порт\" (tcp/udp). Без порта весь протокол уходит напрямую."
|
||||
|
||||
msgid "Please wait, operation is running..."
|
||||
msgstr "Пожалуйста, подождите, выполняется операция..."
|
||||
|
||||
@@ -2135,9 +2180,15 @@ msgstr "Порт должен быть от 1 до 65535"
|
||||
msgid "Port range start must be less than or equal to end"
|
||||
msgstr "Начало диапазона портов должно быть меньше или равно концу"
|
||||
|
||||
msgid "Port range start must not exceed its end"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ports"
|
||||
msgstr "Порты"
|
||||
|
||||
msgid "Ports must be between 1 and 65535"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pre-download all preset lists"
|
||||
msgstr "Предзагрузить все предустановленные списки"
|
||||
|
||||
@@ -2315,6 +2366,12 @@ msgstr "Перезапустить Tachyon"
|
||||
msgid "Restarts"
|
||||
msgstr "Перезапуски"
|
||||
|
||||
msgid "Restore"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restore Snapshot"
|
||||
msgstr "Восстановить снапшот"
|
||||
|
||||
msgid "Restores the Tachyon factory defaults: all rules, subscriptions, custom DNS servers and other options will be lost."
|
||||
msgstr "Восстанавливает заводские настройки Tachyon: все правила, подписки, пользовательские DNS-серверы и другие параметры будут удалены."
|
||||
|
||||
@@ -2390,12 +2447,18 @@ msgstr "Россия (ru)"
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
msgid "Save a snapshot of the working config and restore it later if something breaks."
|
||||
msgstr "Сохранить снапшот рабочей конфигурации, чтобы восстановить её позже, если что-то сломается."
|
||||
|
||||
msgid "Save AI Watchdog Settings"
|
||||
msgstr "Сохранить настройки AI Watchdog"
|
||||
|
||||
msgid "Save Smart Detect Settings"
|
||||
msgstr "Сохранить настройки Smart Detect"
|
||||
|
||||
msgid "Save Snapshot"
|
||||
msgstr "Сохранить снапшот"
|
||||
|
||||
msgid "Saving…"
|
||||
msgstr "Сохранение…"
|
||||
|
||||
@@ -2651,6 +2714,12 @@ msgstr "Smart Detect"
|
||||
msgid "Smart Detect settings saved"
|
||||
msgstr "Настройки Smart Detect сохранены"
|
||||
|
||||
msgid "Snapshot name"
|
||||
msgstr "Имя снапшота"
|
||||
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
msgid "Snell Version"
|
||||
msgstr "Версия Snell"
|
||||
|
||||
@@ -2819,6 +2888,9 @@ msgstr "Служба Tachyon остановлена. Запустите серв
|
||||
msgid "Tachyon service is stopped. Start the service to display the dashboard."
|
||||
msgstr "Служба Tachyon остановлена. Запустите сервис для отображения дашборда."
|
||||
|
||||
msgid "Tachyon update did not complete within the timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tachyon will not modify your DHCP configuration"
|
||||
msgstr "Tachyon не будет изменять вашу конфигурацию DHCP"
|
||||
|
||||
@@ -2930,6 +3002,9 @@ msgstr "Этот параметр требует значение."
|
||||
msgid "This will erase all Tachyon settings and restore the factory defaults. The service will restart. This action cannot be undone."
|
||||
msgstr "Все настройки Tachyon будут удалены и восстановлены заводские значения. Сервис будет перезапущен. Это действие нельзя отменить."
|
||||
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Время"
|
||||
|
||||
@@ -3095,6 +3170,9 @@ msgstr "Неуказанный IP-адрес не разрешён"
|
||||
msgid "Unstable"
|
||||
msgstr "Нестабильно"
|
||||
|
||||
msgid "Up to 10 snapshots are kept; the oldest ones are removed automatically."
|
||||
msgstr "Хранится до 10 снапшотов; самые старые удаляются автоматически."
|
||||
|
||||
msgid "Update"
|
||||
msgstr "Обновить"
|
||||
|
||||
@@ -3310,453 +3388,3 @@ msgstr "sing-box outbound для Zapret2 настроен"
|
||||
|
||||
msgid "Zapret2 sing-box outbound is not configured"
|
||||
msgstr "sing-box outbound для Zapret2 не настроен"
|
||||
|
||||
msgid "Service Health Check"
|
||||
msgstr "Проверка доступности сервисов"
|
||||
|
||||
msgid "Total Targets"
|
||||
msgstr "Всего целей"
|
||||
|
||||
msgid "Passed"
|
||||
msgstr "Успешно"
|
||||
|
||||
msgid "Failed"
|
||||
msgstr "Ошибки"
|
||||
|
||||
msgid "Avg Latency"
|
||||
msgstr "Ср. задержка"
|
||||
|
||||
msgid "Search domain..."
|
||||
msgstr "Поиск домена..."
|
||||
|
||||
msgid "Section / Target"
|
||||
msgstr "Секция / Цель"
|
||||
|
||||
msgid "Outbound Route"
|
||||
msgstr "Маршрут"
|
||||
|
||||
msgid "Resolved IP / DNS"
|
||||
msgstr "IP / DNS"
|
||||
|
||||
msgid "Verdict"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Check domain or IP (e.g. example.com)..."
|
||||
msgstr "Проверить домен или IP (напр. example.com)..."
|
||||
|
||||
msgid "Check Custom"
|
||||
msgstr "Проверить свой"
|
||||
|
||||
msgid "Check All"
|
||||
msgstr "Проверить все"
|
||||
|
||||
msgid "Check Again"
|
||||
msgstr "Проверить снова"
|
||||
|
||||
msgid "Testing..."
|
||||
msgstr "Тестирование..."
|
||||
|
||||
msgid "Fetching service targets..."
|
||||
msgstr "Получение списка сервисов..."
|
||||
|
||||
msgid "Failed to run service check."
|
||||
msgstr "Не удалось выполнить проверку сервисов."
|
||||
|
||||
msgid "Failed to parse target list."
|
||||
msgstr "Ошибка обработки списка целей."
|
||||
|
||||
msgid "No targets found in section configs."
|
||||
msgstr "Цели в конфигурациях секций не найдены."
|
||||
|
||||
msgid "Check YouTube availability"
|
||||
msgstr "Проверить доступность YouTube"
|
||||
|
||||
msgid "Check Mode:"
|
||||
msgstr "Режим проверки:"
|
||||
|
||||
msgid "Active Sections"
|
||||
msgstr "Активные секции"
|
||||
|
||||
msgid "All Profiles"
|
||||
msgstr "Все сервисы и профили"
|
||||
|
||||
msgid "Sections"
|
||||
msgstr "Секции"
|
||||
|
||||
msgid "Servers"
|
||||
msgstr "Серверы"
|
||||
|
||||
msgid "Monitoring"
|
||||
msgstr "Мониторинг"
|
||||
|
||||
msgid "Components"
|
||||
msgstr "Компоненты"
|
||||
|
||||
msgid "Drag rows to change priority. The rule at the top is checked first."
|
||||
msgstr "Перетащите строки для изменения приоритета. Правило вверху проверяется первым."
|
||||
|
||||
msgid "Rules"
|
||||
msgstr "Правила"
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Редактировать"
|
||||
|
||||
msgid "Connection"
|
||||
msgstr "Соединение"
|
||||
|
||||
msgid "Add a section"
|
||||
msgstr "Добавить секцию"
|
||||
|
||||
msgid "Add a server inbound"
|
||||
msgstr "Добавить серверный инбаунд"
|
||||
|
||||
msgid "Accept external proxy connections and route them with sing-box."
|
||||
msgstr "Принимать внешние прокси-подключения и маршрутизировать их через sing-box."
|
||||
|
||||
msgid "Section"
|
||||
msgstr "Секция"
|
||||
|
||||
msgid "Server"
|
||||
msgstr "Сервер"
|
||||
|
||||
msgid "Installing"
|
||||
msgstr "Установка"
|
||||
|
||||
msgid "Initializing operation..."
|
||||
msgstr "Инициализация операции..."
|
||||
|
||||
msgid "Installing package & updating configurations..."
|
||||
msgstr "Установка пакета и обновление настроек..."
|
||||
|
||||
msgid "Installation completed successfully!"
|
||||
msgstr "Установка успешно завершена!"
|
||||
|
||||
msgid "Direct"
|
||||
msgstr "Напрямую"
|
||||
|
||||
msgid "Tachyon Settings"
|
||||
msgstr "Настройки Tachyon"
|
||||
|
||||
msgid "All Services"
|
||||
msgstr "Все сервисы"
|
||||
|
||||
msgid "Service Availability Check"
|
||||
msgstr "Проверка доступности сервисов"
|
||||
|
||||
msgid "Filter by service:"
|
||||
msgstr "Фильтр по сервису:"
|
||||
|
||||
msgid "Search domain or service..."
|
||||
msgstr "Поиск домена или сервиса..."
|
||||
|
||||
msgid "Service / Target"
|
||||
msgstr "Сервис / Цель"
|
||||
|
||||
msgid "Route Type"
|
||||
msgstr "Тип маршрута"
|
||||
|
||||
msgid "Active Routes"
|
||||
msgstr "Активные маршруты"
|
||||
|
||||
msgid "Custom Domains"
|
||||
msgstr "Свои домены"
|
||||
|
||||
msgid "Unavailable"
|
||||
msgstr "Недоступно"
|
||||
|
||||
msgid "Available"
|
||||
msgstr "Доступно"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Авто"
|
||||
|
||||
msgid "Proxy"
|
||||
msgstr "Прокси"
|
||||
|
||||
msgid "TCP"
|
||||
msgstr "TCP"
|
||||
|
||||
msgid "TLS"
|
||||
msgstr "TLS"
|
||||
|
||||
msgid "Method"
|
||||
msgstr "Метод"
|
||||
|
||||
msgid "ALPN"
|
||||
msgstr "ALPN"
|
||||
|
||||
msgid "VLESS flow"
|
||||
msgstr "VLESS flow"
|
||||
|
||||
msgid "FakeTLS host"
|
||||
msgstr "FakeTLS host"
|
||||
|
||||
msgid "Concurrency"
|
||||
msgstr "Параллелизм"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Удалить"
|
||||
|
||||
msgid "Information"
|
||||
msgstr "Информация"
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr "Сохранение..."
|
||||
|
||||
msgid "Generating..."
|
||||
msgstr "Генерация..."
|
||||
|
||||
msgid "Starting..."
|
||||
msgstr "Запуск..."
|
||||
|
||||
msgid "Stopping..."
|
||||
msgstr "Остановка..."
|
||||
|
||||
msgid "None"
|
||||
msgstr "Нет"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
msgid "Configuration for Tachyon service"
|
||||
msgstr "Конфигурация сервиса Tachyon"
|
||||
|
||||
msgid "Only the specified servers"
|
||||
msgstr "Только указанные серверы"
|
||||
|
||||
msgid "Select all"
|
||||
msgstr "Выбрать все"
|
||||
|
||||
msgid "Routing mode"
|
||||
msgstr "Режим маршрутизации"
|
||||
|
||||
msgid "Routing section"
|
||||
msgstr "Секция маршрутизации"
|
||||
|
||||
msgid "Keeps FakeIP cache persistent across reboots and pre-resolves popular blocked domains on startup so first-visit latency is 0 ms."
|
||||
msgstr "Сохраняет кэш FakeIP между перезагрузками и предварительно разрешает популярные заблокированные домены при запуске, чтобы задержка первого обращения была 0 мс."
|
||||
|
||||
msgid "Configure User-Agent в the item settings"
|
||||
msgstr "Настройте User-Agent в параметрах элемента"
|
||||
|
||||
msgid "Client link"
|
||||
msgstr "Клиентская ссылка"
|
||||
|
||||
msgid "Client link QR code"
|
||||
msgstr "QR-код клиентской ссылки"
|
||||
|
||||
msgid "Client link is not available yet"
|
||||
msgstr "Клиентская ссылка пока недоступна"
|
||||
|
||||
msgid "Loading client link..."
|
||||
msgstr "Загрузка клиентской ссылки..."
|
||||
|
||||
msgid "Secret"
|
||||
msgstr "Секрет"
|
||||
|
||||
msgid "Public Key"
|
||||
msgstr "Публичный ключ"
|
||||
|
||||
msgid "Certificate path"
|
||||
msgstr "Путь к сертификату"
|
||||
|
||||
msgid "Key path"
|
||||
msgstr "Путь к ключу"
|
||||
|
||||
msgid "Specify a file path"
|
||||
msgstr "Укажите путь к файлу"
|
||||
|
||||
msgid "Specify different files"
|
||||
msgstr "Укажите разные файлы"
|
||||
|
||||
msgid "Selected section"
|
||||
msgstr "Выбранная секция"
|
||||
|
||||
msgid "Inbound JSON"
|
||||
msgstr "Inbound JSON"
|
||||
|
||||
msgid "Inbound JSON cannot be empty"
|
||||
msgstr "Inbound JSON не может быть пустым"
|
||||
|
||||
msgid "Inbound JSON must be an object with a type field"
|
||||
msgstr "Inbound JSON должен быть объектом с полем type"
|
||||
|
||||
msgid "Server information"
|
||||
msgstr "Информация о сервере"
|
||||
|
||||
msgid "Server name"
|
||||
msgstr "Имя сервера"
|
||||
|
||||
msgid "Server name / SNI"
|
||||
msgstr "Имя сервера / SNI"
|
||||
|
||||
msgid "Reality fake site"
|
||||
msgstr "Фейковый сайт Reality"
|
||||
|
||||
msgid "Reality fake site port"
|
||||
msgstr "Порт фейкового сайта Reality"
|
||||
|
||||
msgid "Reality max time difference"
|
||||
msgstr "Макс. разница времени Reality"
|
||||
|
||||
msgid "Reality private key"
|
||||
msgstr "Приватный ключ Reality"
|
||||
|
||||
msgid "Reality public key"
|
||||
msgstr "Публичный ключ Reality"
|
||||
|
||||
msgid "Reality short ID"
|
||||
msgstr "Короткий ID Reality"
|
||||
|
||||
msgid "Reality short ID must contain 1-8 hex digits"
|
||||
msgstr "Короткий ID Reality должен содержать 1-8 шестнадцатеричных цифр"
|
||||
|
||||
msgid "Fronting IP"
|
||||
msgstr "Fronting IP"
|
||||
|
||||
msgid "Fronting port"
|
||||
msgstr "Fronting порт"
|
||||
|
||||
msgid "Fronting proxy protocol"
|
||||
msgstr "Протокол Fronting прокси"
|
||||
|
||||
msgid "gRPC service"
|
||||
msgstr "Сервис gRPC"
|
||||
|
||||
msgid "Transport host"
|
||||
msgstr "Хост транспорта"
|
||||
|
||||
msgid "Transport path"
|
||||
msgstr "Путь транспорта"
|
||||
|
||||
msgid "Transport path must start with /"
|
||||
msgstr "Путь транспорта должен начинаться с /"
|
||||
|
||||
msgid "HTTP hosts"
|
||||
msgstr "HTTP хосты"
|
||||
|
||||
msgid "XHTTP mode"
|
||||
msgstr "Режим XHTTP"
|
||||
|
||||
msgid "Fallback on unknown DC"
|
||||
msgstr "Fallback при неизвестном DC"
|
||||
|
||||
msgid "Padding"
|
||||
msgstr "Padding"
|
||||
|
||||
msgid "Handshake timeout"
|
||||
msgstr "Таймаут рукопожатия"
|
||||
|
||||
msgid "Time skew tolerance"
|
||||
msgstr "Допуск отклонения времени"
|
||||
|
||||
msgid "Invalid NFQWS strategy"
|
||||
msgstr "Неверная стратегия NFQWS"
|
||||
|
||||
msgid "Invalid NFQWS2 strategy"
|
||||
msgstr "Неверная стратегия NFQWS2"
|
||||
|
||||
msgid "Installed build \\u2192 Available build"
|
||||
msgstr "Установленная сборка \\u2192 Доступная сборка"
|
||||
|
||||
msgid "Prevent multiple reload_firewall calls within 120 seconds to avoid connection drops."
|
||||
msgstr "Предотвращать множественные вызовы reload_firewall в течение 120 секунд для избежания разрыва соединений."
|
||||
|
||||
msgid "Obfuscation"
|
||||
msgstr "Обфускация"
|
||||
|
||||
msgid "Obfuscation password"
|
||||
msgstr "Пароль обфускации"
|
||||
|
||||
msgid "Auto update"
|
||||
msgstr "Автообновление"
|
||||
|
||||
msgid "Use CIDR prefixes like 192.168.1.0/24 or 2001:db8::/32"
|
||||
msgstr "Используйте CIDR-префиксы, например 192.168.1.0/24 или 2001:db8::/32"
|
||||
|
||||
msgid "Use an IP address"
|
||||
msgstr "Используйте IP-адрес"
|
||||
|
||||
msgid "Use an IP listen address"
|
||||
msgstr "Используйте IP-адрес прослушивания"
|
||||
|
||||
msgid "Use a domain name or IP address"
|
||||
msgstr "Используйте имя домена или IP-адрес"
|
||||
|
||||
msgid "Use an HTTP or HTTPS URL"
|
||||
msgstr "Используйте HTTP или HTTPS URL"
|
||||
|
||||
msgid "Use letters, digits, dots, underscores, or hyphens"
|
||||
msgstr "Используйте буквы, цифры, точки, подчёркивания или дефисы"
|
||||
|
||||
msgid "Use a non-negative integer"
|
||||
msgstr "Используйте неотрицательное целое число"
|
||||
|
||||
msgid "Use a non-zero secret"
|
||||
msgstr "Используйте ненулевой секрет"
|
||||
|
||||
msgid "Use 32 hex characters"
|
||||
msgstr "Используйте 32 шестнадцатеричных символа"
|
||||
|
||||
msgid "Use a duration like 30s, 5m, or 1h30m"
|
||||
msgstr "Используйте длительность, например 30s, 5m или 1h30m"
|
||||
|
||||
msgid "Expecting: %s"
|
||||
msgstr "Ожидалось: %s"
|
||||
|
||||
msgid "Cannot save server"
|
||||
msgstr "Не удалось сохранить сервер"
|
||||
|
||||
msgid "QR code is too large"
|
||||
msgstr "QR-код слишком большой"
|
||||
|
||||
msgid "Save Snapshot"
|
||||
msgstr "Сохранить снапшот"
|
||||
|
||||
msgid "Snapshot name"
|
||||
msgstr "Имя снапшота"
|
||||
|
||||
msgid "Config Snapshots"
|
||||
msgstr "Снапшоты конфигурации"
|
||||
|
||||
msgid "Save a snapshot of the working config and restore it later if something breaks."
|
||||
msgstr "Сохранить снапшот рабочей конфигурации, чтобы восстановить её позже, если что-то сломается."
|
||||
|
||||
msgid "Give the snapshot a name. It is saved with the current date and can be restored from this list later."
|
||||
msgstr "Дайте снапшоту имя. Он сохраняется с текущей датой, и его можно будет восстановить из этого списка позже."
|
||||
|
||||
msgid "No snapshots yet"
|
||||
msgstr "Снапшотов пока нет"
|
||||
|
||||
msgid "Failed to load snapshots"
|
||||
msgstr "Не удалось загрузить снапшоты"
|
||||
|
||||
msgid "Failed to save snapshot"
|
||||
msgstr "Не удалось сохранить снапшот"
|
||||
|
||||
msgid "Failed to delete snapshot"
|
||||
msgstr "Не удалось удалить снапшот"
|
||||
|
||||
msgid "Failed to restore snapshot"
|
||||
msgstr "Не удалось восстановить снапшот"
|
||||
|
||||
msgid "Restore Snapshot"
|
||||
msgstr "Восстановить снапшот"
|
||||
|
||||
msgid "Delete Snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "This will replace the current Tachyon settings with the snapshot"
|
||||
msgstr "Это заменит текущие настройки Tachyon на снапшот"
|
||||
|
||||
msgid "Delete the snapshot"
|
||||
msgstr "Удалить снапшот"
|
||||
|
||||
msgid "Up to 10 snapshots are kept; the oldest ones are removed automatically."
|
||||
msgstr "Хранится до 10 снапшотов; самые старые удаляются автоматически."
|
||||
|
||||
msgid "Snapshot restored. The service restarts and the page will reload."
|
||||
msgstr "Снапшот восстановлен. Сервис перезапускается, страница будет перезагружена."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -125,6 +125,8 @@ function show_help() {
|
||||
print(" component_action Manage Tachyon, sing-box, zapret, zapret2, and ByeDPI components\n");
|
||||
print(" component_action_async Start a component action in the background\n");
|
||||
print(" component_action_status Get background component action status\n");
|
||||
print(" component_action_log [job_id] [offset]\n");
|
||||
print(" Get background component action log from offset\n");
|
||||
print(" component_updates_if_due Check installed components for updates when due\n");
|
||||
print(" component_update_check_cache Get cached component update check results\n");
|
||||
print(" hosts_list_update [url] Update custom hosts lists (all or specific URL)\n");
|
||||
@@ -218,6 +220,7 @@ function command_spec(command) {
|
||||
component_action: [ "components/action.uc", "component-action", 2 ],
|
||||
component_action_async: [ "components/updates.uc", "component-action-async", 2 ],
|
||||
component_action_status: [ "components/updates.uc", "component-action-status", 1 ],
|
||||
component_action_log: [ "components/updates.uc", "component-action-log", 2 ],
|
||||
component_updates_if_due: [ "components/updates.uc", "component-updates-if-due", 0 ],
|
||||
component_update_check_cache: [ "components/updates.uc", "component-update-check-cache", 0 ],
|
||||
hosts_list_update: [ "components/hosts.uc", "list-update", 1 ],
|
||||
|
||||
@@ -172,8 +172,26 @@ function log_message(message, level) {
|
||||
command_success_from_args([ "logger", "-t", "tachyon", "[" + level + "] " + as_string(message) ]);
|
||||
}
|
||||
|
||||
function job_log_time() {
|
||||
let seconds = int(clock()[0]);
|
||||
return sprintf("%02d:%02d:%02d", int(seconds / 3600) % 24, int(seconds / 60) % 60, seconds % 60);
|
||||
}
|
||||
|
||||
function job_log_append(message, level) {
|
||||
let path = getenv("UPDATES_JOB_LOG");
|
||||
if (path == "")
|
||||
return;
|
||||
let file = fs.open(path, "a");
|
||||
if (!file)
|
||||
return;
|
||||
file.write(sprintf("[%s] [%s] %s\n", job_log_time(), as_string(level), as_string(message)));
|
||||
file.close();
|
||||
}
|
||||
|
||||
function updates_log(message, level) {
|
||||
log_message("Updates: " + as_string(message), level || "info");
|
||||
level = as_string(level || "info");
|
||||
log_message("Updates: " + as_string(message), level);
|
||||
job_log_append(message, level);
|
||||
}
|
||||
|
||||
function module_command(args) {
|
||||
|
||||
@@ -1587,6 +1587,10 @@ function component_job_output_path(job_id) {
|
||||
return COMPONENT_JOB_DIR + "/" + as_string(job_id) + ".out";
|
||||
}
|
||||
|
||||
function component_job_log_path(job_id) {
|
||||
return COMPONENT_JOB_DIR + "/" + as_string(job_id) + ".log";
|
||||
}
|
||||
|
||||
function component_job_output_path_from_state(path) {
|
||||
path = as_string(path);
|
||||
if (length(path) >= 5 && substr(path, length(path) - 5) == ".json")
|
||||
@@ -1692,6 +1696,14 @@ function component_cleanup_jobs() {
|
||||
"-mmin", "+" + as_string(COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES),
|
||||
"-delete"
|
||||
]);
|
||||
command_success_from_args([
|
||||
"find",
|
||||
COMPONENT_JOB_DIR,
|
||||
"-type", "f",
|
||||
"-name", "*.log",
|
||||
"-mmin", "+" + as_string(COMPONENT_JOB_ORPHAN_OUTPUT_TTL_MINUTES),
|
||||
"-delete"
|
||||
]);
|
||||
command_success_from_args([
|
||||
"find",
|
||||
COMPONENT_JOB_DIR,
|
||||
@@ -1849,7 +1861,9 @@ function component_action_worker(state_file, output_file, component, action) {
|
||||
// the JSON result - the whole-file parse then failed, the line scan found no
|
||||
// object, and a successful action was reported to the UI as "Failed to
|
||||
// execute" with the tail of a log as its message.
|
||||
let command = command_env(component_worker_env()) + " " +
|
||||
let worker_env = component_worker_env();
|
||||
worker_env.UPDATES_JOB_LOG = output_file + ".log";
|
||||
let command = command_env(worker_env) + " " +
|
||||
command_from_args([
|
||||
"ucode",
|
||||
"-L", LIB_DIR,
|
||||
@@ -1879,7 +1893,9 @@ function component_action_async(component, action) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!write_state_file(state_file, component_running_job_state_value(component, action, now_seconds()))) {
|
||||
let running_state = component_running_job_state_value(component, action, now_seconds());
|
||||
running_state.log_path = component_job_log_path(job_id);
|
||||
if (!write_state_file(state_file, running_state)) {
|
||||
component_job_json_response(false, "", "Failed to write component action state");
|
||||
exit(1);
|
||||
}
|
||||
@@ -1918,6 +1934,24 @@ function component_action_status(job_id) {
|
||||
print(as_string(fs.readfile(state_file)));
|
||||
}
|
||||
|
||||
function component_action_log(job_id, offset) {
|
||||
let log_path = component_job_log_path(job_id);
|
||||
let data = fs.readfile(log_path);
|
||||
if (data == null) {
|
||||
write_json({ success: false, log: "", offset: 0 });
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let start = int(offset || 0);
|
||||
if (start < 0)
|
||||
start = 0;
|
||||
data = as_string(data);
|
||||
if (start > length(data))
|
||||
start = length(data);
|
||||
|
||||
write_json({ success: true, log: substr(data, start), offset: length(data) });
|
||||
}
|
||||
|
||||
function automatic_component_check_names() {
|
||||
let result = [ "tachyon" ];
|
||||
|
||||
@@ -3300,6 +3334,8 @@ else if (mode == "component-action-async")
|
||||
component_action_async(ARGV[1], ARGV[2]);
|
||||
else if (mode == "component-action-status")
|
||||
component_action_status(ARGV[1]);
|
||||
else if (mode == "component-action-log")
|
||||
component_action_log(ARGV[1], ARGV[2]);
|
||||
else if (mode == "component-updates-if-due")
|
||||
component_updates_if_due();
|
||||
else if (mode == "component-update-check-cache")
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
# The UI shows a live log of a running component action. The worker's progress
|
||||
# messages used to go to syslog only, so the modal had nothing real to display
|
||||
# and faked its steps on a timer. The contract: the worker appends every
|
||||
# progress line to <job>.log (UPDATES_JOB_LOG), the running state advertises the
|
||||
# path, the log outlives the job (only the age-based sweep removes it), and the
|
||||
# component-action-log CLI mode serves it from an arbitrary byte offset.
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
UPDATES_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updates.uc"
|
||||
ACTION_UC="$ROOT_DIR/tachyon/files/usr/lib/components/action.uc"
|
||||
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
code_only() {
|
||||
sed 's://.*::'
|
||||
}
|
||||
|
||||
# The worker must hand the action module a dedicated log file path.
|
||||
worker="$(sed -n '/^function component_action_worker/,/^}/p' "$UPDATES_UC" | code_only)"
|
||||
[ -n "$worker" ] || fail "components/updates.uc must define component_action_worker"
|
||||
grep -Fq 'UPDATES_JOB_LOG' <<<"$worker" ||
|
||||
fail "component_action_worker must set UPDATES_JOB_LOG for the action module"
|
||||
grep -Eq 'UPDATES_JOB_LOG[^=]*=\s*.*\.log' <<<"$worker" ||
|
||||
fail "component_action_worker must point UPDATES_JOB_LOG at a .log file"
|
||||
|
||||
# The log path must be derived from the job id, the same way the .out file is.
|
||||
grep -Fq 'function component_job_log_path' "$UPDATES_UC" ||
|
||||
fail "components/updates.uc must define component_job_log_path"
|
||||
grep -Fq '.log' "$UPDATES_UC" ||
|
||||
fail "component_job_log_path must produce a .log path"
|
||||
|
||||
# The running state must carry the log path so the UI can find the file.
|
||||
grep -Fq 'log_path' "$UPDATES_UC" ||
|
||||
fail "the component job state must expose log_path"
|
||||
|
||||
# finish_component_job removes the result channel and stderr, but the log is the
|
||||
# UI's evidence of what happened: it must survive until the TTL sweep.
|
||||
finish="$(sed -n '/^function finish_component_job/,/^}/p' "$UPDATES_UC" | code_only)"
|
||||
[ -n "$finish" ] || fail "components/updates.uc must define finish_component_job"
|
||||
grep -Fq 'remove_file(output_file + ".stderr")' <<<"$finish" ||
|
||||
fail "finish_component_job must still remove the .stderr file"
|
||||
if grep -Fq '"log"' <<<"$finish"; then
|
||||
fail "finish_component_job must not remove the job .log file"
|
||||
fi
|
||||
|
||||
# Only the age-based orphan sweep may drop .log files.
|
||||
grep -Fq '"*.log"' "$UPDATES_UC" ||
|
||||
fail "the age-based orphan cleanup must cover *.log"
|
||||
|
||||
# The progress line must reach both syslog and the job log file.
|
||||
updates_log="$(sed -n '/^function updates_log/,/^}/p' "$ACTION_UC" | code_only)"
|
||||
[ -n "$updates_log" ] || fail "components/action.uc must define updates_log"
|
||||
grep -Fq 'log_message(' <<<"$updates_log" ||
|
||||
fail "updates_log must keep writing to syslog"
|
||||
grep -Fq 'job_log_append(' <<<"$updates_log" ||
|
||||
fail "updates_log must append to the job log file"
|
||||
grep -Fq 'getenv("UPDATES_JOB_LOG")' "$ACTION_UC" ||
|
||||
fail "action.uc must read the UPDATES_JOB_LOG env var"
|
||||
|
||||
# The CLI must expose the log mode.
|
||||
grep -Fq 'component_action_log' "$TACHYON_BIN" ||
|
||||
fail "/usr/bin/tachyon must declare component_action_log"
|
||||
|
||||
# --- behavioural: offset reads, clamping, missing file -----------------------
|
||||
|
||||
if command -v ucode >/dev/null 2>&1; then
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
mkdir -p "$WORK_DIR/jobs"
|
||||
printf 'line1\nline2\nline3\n' >"$WORK_DIR/jobs/abc-1_2.log"
|
||||
|
||||
run_log() {
|
||||
UPDATES_JOB_DIR="$WORK_DIR/jobs" \
|
||||
ucode -L "$ROOT_DIR/tachyon/files/usr/lib" \
|
||||
"$UPDATES_UC" component-action-log "$@"
|
||||
}
|
||||
|
||||
full="$(run_log abc-1_2 0)"
|
||||
grep -Fq '"success": true' <<<"$full" ||
|
||||
fail "component-action-log must report success for an existing log: $full"
|
||||
grep -Fq '"log": "line1\nline2\nline3\n"' <<<"$full" ||
|
||||
fail "component-action-log offset 0 must return the whole file: $full"
|
||||
grep -Fq '"offset": 18' <<<"$full" ||
|
||||
fail "component-action-log must report the total byte length: $full"
|
||||
|
||||
tail="$(run_log abc-1_2 12)"
|
||||
grep -Fq '"log": "line3\n"' <<<"$tail" ||
|
||||
fail "component-action-log must read from the requested byte offset: $tail"
|
||||
grep -Fq '"offset": 18' <<<"$tail" ||
|
||||
fail "component-action-log must report the new offset after a partial read: $tail"
|
||||
|
||||
clamped="$(run_log abc-1_2 999)"
|
||||
grep -Fq '"log": ""' <<<"$clamped" ||
|
||||
fail "component-action-log must clamp an offset past the end of the file: $clamped"
|
||||
grep -Fq '"offset": 18' <<<"$clamped" ||
|
||||
fail "component-action-log must clamp the reported offset to the file length: $clamped"
|
||||
|
||||
set +e
|
||||
missing="$(run_log nope 0 2>/dev/null)"
|
||||
status=$?
|
||||
set -e
|
||||
[ "$status" -ne 0 ] || fail "component-action-log must exit non-zero for a missing log file"
|
||||
grep -Fq '"success": false' <<<"$missing" ||
|
||||
fail "component-action-log must report failure for a missing log file: $missing"
|
||||
fi
|
||||
|
||||
printf 'component action log checks passed\n'
|
||||
Reference in New Issue
Block a user