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:
Dushnilin
2026-08-14 13:20:33 +03:00
parent 5d8e0bbfcc
commit 8ecc3ee685
17 changed files with 2159 additions and 2080 deletions
+4
View File
@@ -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) ===
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+85 -457
View File
@@ -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();
});
});
+7
View File
@@ -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
+85 -457
View File
@@ -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
+3
View File
@@ -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 ],
+19 -1
View File
@@ -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) {
+38 -2
View File
@@ -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")
+115
View File
@@ -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'