feat: AI доктор — 5 багфиксов + Telegram кнопка

Багфиксы:
- rag.uc: исправлен порядок аргументов push() — RAG-эмбеддинги снова строятся
- fix_uci_config: правильный путь бэкапа (/etc/backup/tachyon_config + fallback)
- apply_quick_fix: честные exit codes для всех 23 фиксов
- doctor(): прокидывается busy flag в JSON-вывод
- diagnose_json: строится из checks[] вместо парсинга эмодзи из текста

UI:
- Кнопка Telegram канал (https://t.me/tachyon_proxy) на странице диагностики

Тесты:
- 5 новых проверок в ai_doctor_local.sh (uci_backup, exit codes, busy, structured checks, rag push)
This commit is contained in:
Dushnilin
2026-08-25 21:37:15 +03:00
parent 2cec97371c
commit 77e8cdaad0
12 changed files with 3434 additions and 2783 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+95 -104
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-21 14:59+0300\n"
"PO-Revision-Date: 2026-08-21 14:59+0300\n"
"POT-Creation-Date: 2026-08-25 21:32+0300\n"
"PO-Revision-Date: 2026-08-25 21:32+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -71,6 +71,9 @@ msgstr "Принимать маршруты"
msgid "Action"
msgstr "Действие"
msgid "Action failed"
msgstr "Действие не выполнено"
msgid "Action Mode"
msgstr "Режим действия"
@@ -95,6 +98,9 @@ msgstr "Активные соединения"
msgid "Active Main DNS"
msgstr "Активный основной DNS"
msgid "Active routes"
msgstr "Активные маршруты"
msgid "Add a schedule rule"
msgstr "Добавить правило расписания"
@@ -242,6 +248,9 @@ msgstr "Все маршруты"
msgid "All servers"
msgstr "Все серверы"
msgid "All services"
msgstr "Все сервисы"
msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
@@ -374,12 +383,21 @@ msgstr "Автовыбор самого быстрого узла уровня"
msgid "Automatically update remote hosts lists on schedule"
msgstr "Автоматически обновлять удалённые списки Hosts по расписанию"
msgid "Available"
msgstr "Доступно"
msgid "Available actions"
msgstr "Доступные действия"
msgid "Avg. latency"
msgstr "Ср. задержка"
msgid "Backend validation failed: %s"
msgstr "Ошибка проверки бэкенда: %s"
msgid "Basic connectivity"
msgstr "Базовая связность"
msgid "Bearer Auth Token"
msgstr "Токен авторизации (Bearer)"
@@ -416,6 +434,9 @@ msgstr "Блокировать:"
msgid "Block: deny the listed sites. Allow only (whitelist): deny everything except the listed sites. Whitelist mode requires DNS-level blocking and applies to the schedule window."
msgstr "Блокировать: запретить перечисленные сайты. Только разрешённые (белый список): запретить всё, кроме перечисленных сайтов. Режим белого списка требует блокировки на уровне DNS и действует в окне расписания."
msgid "Blocked resources"
msgstr "Заблокированные ресурсы"
msgid "Blocked Sites"
msgstr "Заблокированные сайты"
@@ -521,15 +542,27 @@ msgstr "Путь к сертификату"
msgid "Certificate pin is unavailable. Apply or restart the server to generate the default certificate, then open this dialog again."
msgstr "Пин сертификата недоступен. Примените настройки или перезапустите сервер для генерации сертификата, затем откройте это окно снова."
msgid "Check again"
msgstr "Проверить снова"
msgid "Check all"
msgstr "Проверить все"
msgid "Check completed!"
msgstr "Проверка завершена!"
msgid "Check Connection"
msgstr "Проверить подключение"
msgid "Check domain"
msgstr "Проверить домен"
msgid "Check interval"
msgstr "Интервал проверки"
msgid "Check mode:"
msgstr "Режим проверки:"
msgid "Check Services"
msgstr "Проверить сервисы"
@@ -758,6 +791,9 @@ msgstr "Текущая секция не может загружать собс
msgid "Currently unavailable"
msgstr "Временно недоступно"
msgid "Custom"
msgstr ""
msgid "Custom API Endpoint URL"
msgstr "Пользовательский URL эндпоинта API"
@@ -767,6 +803,9 @@ msgstr "Пользовательские заголовки устройства
msgid "Custom DNS Records (Hosts)"
msgstr "Пользовательские DNS-записи (Hosts)"
msgid "Custom domains"
msgstr "Собственные домены"
msgid "Custom header for cookie packet"
msgstr "Пользовательский заголовок для пакета cookie"
@@ -1217,6 +1256,9 @@ msgstr "Введите полный объект outbound sing-box"
msgid "Enter a display name"
msgstr "Введите отображаемое имя"
msgid "Enter a domain or IP to check (e.g. example.com)..."
msgstr "Введите домен или IP для проверки (напр. example.com)..."
msgid "Enter a level name"
msgstr "Введите имя уровня"
@@ -1328,6 +1370,9 @@ msgstr "Не удалось загрузить снапшоты"
msgid "Failed to parse file:"
msgstr "Ошибка разбора файла:"
msgid "Failed to parse target list."
msgstr "Ошибка обработки списка целей."
msgid "Failed to parse WARP config:"
msgstr "Не удалось разобрать конфигурацию WARP:"
@@ -1367,6 +1412,9 @@ msgstr "Не удалось сохранить снапшот"
msgid "Failed to start bot"
msgstr "Не удалось запустить бота"
msgid "Failed to start service check."
msgstr "Не удалось запустить проверку сервисов."
msgid "Failed to start watchdog"
msgstr "Не удалось запустить watchdog"
@@ -1409,6 +1457,9 @@ msgstr "Интервал поиска более быстрого сервера
msgid "Fastest"
msgstr "Самый быстрый"
msgid "Filter by service:"
msgstr "Фильтр по сервису:"
msgid "Filter the servers that will be displayed on the dashboard."
msgstr "Фильтровать серверы, отображаемые на дашборде."
@@ -1526,84 +1577,6 @@ msgstr "Глобальная проверка"
msgid "Global check failed"
msgstr "Не удалось выполнить глобальную проверку"
msgid "View logs failed"
msgstr "Не удалось открыть логи"
msgid "Show sing-box config failed"
msgstr "Не удалось показать конфигурацию sing-box"
msgid "Action failed"
msgstr "Действие не выполнено"
msgid "Active routes"
msgstr "Активные маршруты"
msgid "All services"
msgstr "Все сервисы"
msgid "Avg. latency"
msgstr "Ср. задержка"
msgid "Basic connectivity"
msgstr "Базовая связность"
msgid "Blocked resources"
msgstr "Заблокированные ресурсы"
msgid "Check again"
msgstr "Проверить снова"
msgid "Check all"
msgstr "Проверить все"
msgid "Check domain"
msgstr "Проверить домен"
msgid "Check mode:"
msgstr "Режим проверки:"
msgid "Custom domains"
msgstr "Собственные домены"
msgid "Enter a domain or IP to check (e.g. example.com)..."
msgstr "Введите домен или IP для проверки (напр. example.com)..."
msgid "Failed to parse target list."
msgstr "Ошибка разбора списка целей."
msgid "Failed to start service check."
msgstr "Не удалось запустить проверку сервисов."
msgid "Filter by service:"
msgstr "Фильтр по сервису:"
msgid "Loading service list..."
msgstr "Получение списка сервисов..."
msgid "No check targets found in the configuration."
msgstr "Цели для проверки не найдены в конфигурации."
msgid "Pending..."
msgstr "Ожидание..."
msgid "Search domain or service..."
msgstr "Поиск домена или сервиса..."
msgid "Service / Target"
msgstr "Сервис / Цель"
msgid "Service availability check"
msgstr "Проверка доступности сервисов"
msgid "Testing..."
msgstr "Проверка..."
msgid "Total targets"
msgstr "Всего целей"
msgid "UDP / QUIC protocols"
msgstr "UDP / QUIC протоколы"
msgid "gRPC service"
msgstr "Сервис gRPC"
@@ -2234,6 +2207,9 @@ msgstr "Загрузка списка устройств LAN..."
msgid "Loading connections"
msgstr "Загрузка подключений"
msgid "Loading service list..."
msgstr "Получение списка сервисов..."
msgid "Loading..."
msgstr "Загрузка..."
@@ -2417,6 +2393,9 @@ msgstr "Активные устройства в локальной сети н
msgid "No active routing sections found."
msgstr "Активные секции маршрутизации не найдены."
msgid "No check targets found in the configuration."
msgstr "Цели для проверки не найдены в конфигурации."
msgid "No closed connections"
msgstr "Нет закрытых подключений"
@@ -2711,6 +2690,9 @@ msgstr "Приватный ключ клиента в формате PEM"
msgid "Pending"
msgstr "Ожидает запуска"
msgid "Pending..."
msgstr "Ожидание..."
msgid "Per-Device Routing"
msgstr "Маршрутизация по устройствам"
@@ -2819,6 +2801,9 @@ msgstr "Защита"
msgid "Protocol"
msgstr "Протокол"
msgid "Proxy"
msgstr "Прокси"
msgid "Proxy / DPI"
msgstr "Прокси / DPI"
@@ -3161,6 +3146,9 @@ msgstr "Правило расписания"
msgid "Search"
msgstr "Поиск"
msgid "Search domain or service..."
msgstr "Поиск домена или сервиса..."
msgid "Secret"
msgstr "Секрет"
@@ -3377,9 +3365,15 @@ msgstr "Серверы"
msgid "Servers on dashboard"
msgstr "Серверы на дашборде"
msgid "Service / Target"
msgstr "Сервис / Цель"
msgid "Service action failed"
msgstr "Действие сервиса не выполнено"
msgid "Service availability check"
msgstr "Проверка доступности сервисов"
msgid "Service list"
msgstr "Список сервисов"
@@ -3434,6 +3428,9 @@ msgstr "Показывать серверы, чьи имена соответс
msgid "Show sing-box config"
msgstr "Показать sing-box конфигурацию"
msgid "Show sing-box config failed"
msgstr "Не удалось показать конфигурацию sing-box"
msgid "Show subscription metadata for this source on the dashboard"
msgstr "Показывать метаданные подписки для этого источника на дашборде"
@@ -3755,6 +3752,9 @@ msgstr "Telegram Бот"
msgid "Telegram Bot Token"
msgstr "Токен Telegram-бота"
msgid "Telegram Channel"
msgstr "Telegram канал"
msgid "Test Connection"
msgstr "Проверить подключение"
@@ -3785,6 +3785,9 @@ msgstr "Проверить подключение к Telegram"
msgid "Testing URL"
msgstr "URL проверки"
msgid "Testing..."
msgstr "Тестирование..."
msgid "Testing…"
msgstr "Проверка…"
@@ -3884,6 +3887,9 @@ msgstr "Верхний уровень имеет наивысший приори
msgid "Total lines:"
msgstr "Всего строк:"
msgid "Total targets"
msgstr "Всего целей"
msgid "Trace"
msgstr "Трассировка"
@@ -3962,6 +3968,9 @@ msgstr "Тип"
msgid "UDP (Unprotected DNS)"
msgstr "UDP (незащищённый DNS)"
msgid "UDP / QUIC protocols"
msgstr "UDP / QUIC протоколы"
msgid "Ukraine (ua)"
msgstr "Украина (ua)"
@@ -3977,6 +3986,9 @@ msgstr "Не удалось проверить стратегию NFQWS2 чер
msgid "Unavailability timeout"
msgstr "Таймаут недоступности"
msgid "Unavailable"
msgstr "Недоступно"
msgid "Underload packet header (H3)"
msgstr "Заголовок пакета под нагрузкой (H3)"
@@ -4211,6 +4223,9 @@ msgstr "Видео"
msgid "View logs"
msgstr "Посмотреть логи"
msgid "View logs failed"
msgstr "Не удалось открыть логи"
msgid "Visible name of this section"
msgstr "Видимое имя этой секции"
@@ -4523,18 +4538,12 @@ 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 "Цели в конфигурациях секций не найдены."
@@ -4571,15 +4580,6 @@ 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 "Тип маршрута"
@@ -4589,15 +4589,6 @@ msgstr "Активные маршруты"
msgid "Custom Domains"
msgstr "Свои домены"
msgid "Unavailable"
msgstr "Недоступно"
msgid "Available"
msgstr "Доступно"
msgid "Proxy"
msgstr "Прокси"
msgid "TCP"
msgstr "TCP"
+1
View File
@@ -21,3 +21,4 @@ export * from './renderLinkIcon24';
export * from './renderDownloadIcon24';
export * from './renderInfoIcon24';
export * from './renderGlobeIcon24';
export * from './renderSendIcon24';
@@ -0,0 +1,27 @@
import { svgEl } from '../helpers/svgEl';
export function renderSendIcon24() {
return svgEl(
'svg',
{
xmlns: 'http://www.w3.org/2000/svg',
width: '24',
height: '24',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
class: 'lucide lucide-send-icon lucide-send',
},
[
svgEl('path', {
d: 'M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z',
}),
svgEl('path', {
d: 'm21.854 2.147-10.94 10.939',
}),
],
);
}
@@ -1,4 +1,4 @@
import { renderBookOpenTextIcon24 } from '../../../../icons';
import { renderBookOpenTextIcon24, renderSendIcon24 } from '../../../../icons';
import { renderButton } from '../../../../partials';
import { insertIf } from '../../../../helpers';
@@ -36,5 +36,16 @@ export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') {
'noopener,noreferrer',
),
}),
renderButton({
classNames: ['cbi-button-save'],
icon: renderSendIcon24,
text: _('Telegram Channel'),
onClick: () =>
window.open(
'https://t.me/tachyon_proxy',
'_blank',
'noopener,noreferrer',
),
}),
]);
}
@@ -1931,6 +1931,33 @@ function renderGlobeIcon24() {
);
}
// src/icons/renderSendIcon24.ts
function renderSendIcon24() {
return svgEl(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "lucide lucide-send-icon lucide-send"
},
[
svgEl("path", {
d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"
}),
svgEl("path", {
d: "m21.854 2.147-10.94 10.939"
})
]
);
}
// src/helpers/prettyBytes.ts
function prettyBytes(n) {
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
@@ -11509,6 +11536,16 @@ function renderWikiDisclaimer(kind) {
"_blank",
"noopener,noreferrer"
)
}),
renderButton({
classNames: ["cbi-button-save"],
icon: renderSendIcon24,
text: _("Telegram Channel"),
onClick: () => window.open(
"https://t.me/tachyon_proxy",
"_blank",
"noopener,noreferrer"
)
})
]);
}
+95 -104
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-21 14:59+0300\n"
"PO-Revision-Date: 2026-08-21 14:59+0300\n"
"POT-Creation-Date: 2026-08-25 21:32+0300\n"
"PO-Revision-Date: 2026-08-25 21:32+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -71,6 +71,9 @@ msgstr "Принимать маршруты"
msgid "Action"
msgstr "Действие"
msgid "Action failed"
msgstr "Действие не выполнено"
msgid "Action Mode"
msgstr "Режим действия"
@@ -95,6 +98,9 @@ msgstr "Активные соединения"
msgid "Active Main DNS"
msgstr "Активный основной DNS"
msgid "Active routes"
msgstr "Активные маршруты"
msgid "Add a schedule rule"
msgstr "Добавить правило расписания"
@@ -242,6 +248,9 @@ msgstr "Все маршруты"
msgid "All servers"
msgstr "Все серверы"
msgid "All services"
msgstr "Все сервисы"
msgid "All traffic from these IP addresses will be routed through the section unconditionally, ignoring all other conditions."
msgstr "Весь трафик от этих IP-адресов будет маршрутизирован через секцию безусловно, игнорируя все остальные условия."
@@ -374,12 +383,21 @@ msgstr "Автовыбор самого быстрого узла уровня"
msgid "Automatically update remote hosts lists on schedule"
msgstr "Автоматически обновлять удалённые списки Hosts по расписанию"
msgid "Available"
msgstr "Доступно"
msgid "Available actions"
msgstr "Доступные действия"
msgid "Avg. latency"
msgstr "Ср. задержка"
msgid "Backend validation failed: %s"
msgstr "Ошибка проверки бэкенда: %s"
msgid "Basic connectivity"
msgstr "Базовая связность"
msgid "Bearer Auth Token"
msgstr "Токен авторизации (Bearer)"
@@ -416,6 +434,9 @@ msgstr "Блокировать:"
msgid "Block: deny the listed sites. Allow only (whitelist): deny everything except the listed sites. Whitelist mode requires DNS-level blocking and applies to the schedule window."
msgstr "Блокировать: запретить перечисленные сайты. Только разрешённые (белый список): запретить всё, кроме перечисленных сайтов. Режим белого списка требует блокировки на уровне DNS и действует в окне расписания."
msgid "Blocked resources"
msgstr "Заблокированные ресурсы"
msgid "Blocked Sites"
msgstr "Заблокированные сайты"
@@ -521,15 +542,27 @@ msgstr "Путь к сертификату"
msgid "Certificate pin is unavailable. Apply or restart the server to generate the default certificate, then open this dialog again."
msgstr "Пин сертификата недоступен. Примените настройки или перезапустите сервер для генерации сертификата, затем откройте это окно снова."
msgid "Check again"
msgstr "Проверить снова"
msgid "Check all"
msgstr "Проверить все"
msgid "Check completed!"
msgstr "Проверка завершена!"
msgid "Check Connection"
msgstr "Проверить подключение"
msgid "Check domain"
msgstr "Проверить домен"
msgid "Check interval"
msgstr "Интервал проверки"
msgid "Check mode:"
msgstr "Режим проверки:"
msgid "Check Services"
msgstr "Проверить сервисы"
@@ -758,6 +791,9 @@ msgstr "Текущая секция не может загружать собс
msgid "Currently unavailable"
msgstr "Временно недоступно"
msgid "Custom"
msgstr ""
msgid "Custom API Endpoint URL"
msgstr "Пользовательский URL эндпоинта API"
@@ -767,6 +803,9 @@ msgstr "Пользовательские заголовки устройства
msgid "Custom DNS Records (Hosts)"
msgstr "Пользовательские DNS-записи (Hosts)"
msgid "Custom domains"
msgstr "Собственные домены"
msgid "Custom header for cookie packet"
msgstr "Пользовательский заголовок для пакета cookie"
@@ -1217,6 +1256,9 @@ msgstr "Введите полный объект outbound sing-box"
msgid "Enter a display name"
msgstr "Введите отображаемое имя"
msgid "Enter a domain or IP to check (e.g. example.com)..."
msgstr "Введите домен или IP для проверки (напр. example.com)..."
msgid "Enter a level name"
msgstr "Введите имя уровня"
@@ -1328,6 +1370,9 @@ msgstr "Не удалось загрузить снапшоты"
msgid "Failed to parse file:"
msgstr "Ошибка разбора файла:"
msgid "Failed to parse target list."
msgstr "Ошибка обработки списка целей."
msgid "Failed to parse WARP config:"
msgstr "Не удалось разобрать конфигурацию WARP:"
@@ -1367,6 +1412,9 @@ msgstr "Не удалось сохранить снапшот"
msgid "Failed to start bot"
msgstr "Не удалось запустить бота"
msgid "Failed to start service check."
msgstr "Не удалось запустить проверку сервисов."
msgid "Failed to start watchdog"
msgstr "Не удалось запустить watchdog"
@@ -1409,6 +1457,9 @@ msgstr "Интервал поиска более быстрого сервера
msgid "Fastest"
msgstr "Самый быстрый"
msgid "Filter by service:"
msgstr "Фильтр по сервису:"
msgid "Filter the servers that will be displayed on the dashboard."
msgstr "Фильтровать серверы, отображаемые на дашборде."
@@ -1526,84 +1577,6 @@ msgstr "Глобальная проверка"
msgid "Global check failed"
msgstr "Не удалось выполнить глобальную проверку"
msgid "View logs failed"
msgstr "Не удалось открыть логи"
msgid "Show sing-box config failed"
msgstr "Не удалось показать конфигурацию sing-box"
msgid "Action failed"
msgstr "Действие не выполнено"
msgid "Active routes"
msgstr "Активные маршруты"
msgid "All services"
msgstr "Все сервисы"
msgid "Avg. latency"
msgstr "Ср. задержка"
msgid "Basic connectivity"
msgstr "Базовая связность"
msgid "Blocked resources"
msgstr "Заблокированные ресурсы"
msgid "Check again"
msgstr "Проверить снова"
msgid "Check all"
msgstr "Проверить все"
msgid "Check domain"
msgstr "Проверить домен"
msgid "Check mode:"
msgstr "Режим проверки:"
msgid "Custom domains"
msgstr "Собственные домены"
msgid "Enter a domain or IP to check (e.g. example.com)..."
msgstr "Введите домен или IP для проверки (напр. example.com)..."
msgid "Failed to parse target list."
msgstr "Ошибка разбора списка целей."
msgid "Failed to start service check."
msgstr "Не удалось запустить проверку сервисов."
msgid "Filter by service:"
msgstr "Фильтр по сервису:"
msgid "Loading service list..."
msgstr "Получение списка сервисов..."
msgid "No check targets found in the configuration."
msgstr "Цели для проверки не найдены в конфигурации."
msgid "Pending..."
msgstr "Ожидание..."
msgid "Search domain or service..."
msgstr "Поиск домена или сервиса..."
msgid "Service / Target"
msgstr "Сервис / Цель"
msgid "Service availability check"
msgstr "Проверка доступности сервисов"
msgid "Testing..."
msgstr "Проверка..."
msgid "Total targets"
msgstr "Всего целей"
msgid "UDP / QUIC protocols"
msgstr "UDP / QUIC протоколы"
msgid "gRPC service"
msgstr "Сервис gRPC"
@@ -2234,6 +2207,9 @@ msgstr "Загрузка списка устройств LAN..."
msgid "Loading connections"
msgstr "Загрузка подключений"
msgid "Loading service list..."
msgstr "Получение списка сервисов..."
msgid "Loading..."
msgstr "Загрузка..."
@@ -2417,6 +2393,9 @@ msgstr "Активные устройства в локальной сети н
msgid "No active routing sections found."
msgstr "Активные секции маршрутизации не найдены."
msgid "No check targets found in the configuration."
msgstr "Цели для проверки не найдены в конфигурации."
msgid "No closed connections"
msgstr "Нет закрытых подключений"
@@ -2711,6 +2690,9 @@ msgstr "Приватный ключ клиента в формате PEM"
msgid "Pending"
msgstr "Ожидает запуска"
msgid "Pending..."
msgstr "Ожидание..."
msgid "Per-Device Routing"
msgstr "Маршрутизация по устройствам"
@@ -2819,6 +2801,9 @@ msgstr "Защита"
msgid "Protocol"
msgstr "Протокол"
msgid "Proxy"
msgstr "Прокси"
msgid "Proxy / DPI"
msgstr "Прокси / DPI"
@@ -3161,6 +3146,9 @@ msgstr "Правило расписания"
msgid "Search"
msgstr "Поиск"
msgid "Search domain or service..."
msgstr "Поиск домена или сервиса..."
msgid "Secret"
msgstr "Секрет"
@@ -3377,9 +3365,15 @@ msgstr "Серверы"
msgid "Servers on dashboard"
msgstr "Серверы на дашборде"
msgid "Service / Target"
msgstr "Сервис / Цель"
msgid "Service action failed"
msgstr "Действие сервиса не выполнено"
msgid "Service availability check"
msgstr "Проверка доступности сервисов"
msgid "Service list"
msgstr "Список сервисов"
@@ -3434,6 +3428,9 @@ msgstr "Показывать серверы, чьи имена соответс
msgid "Show sing-box config"
msgstr "Показать sing-box конфигурацию"
msgid "Show sing-box config failed"
msgstr "Не удалось показать конфигурацию sing-box"
msgid "Show subscription metadata for this source on the dashboard"
msgstr "Показывать метаданные подписки для этого источника на дашборде"
@@ -3755,6 +3752,9 @@ msgstr "Telegram Бот"
msgid "Telegram Bot Token"
msgstr "Токен Telegram-бота"
msgid "Telegram Channel"
msgstr "Telegram канал"
msgid "Test Connection"
msgstr "Проверить подключение"
@@ -3785,6 +3785,9 @@ msgstr "Проверить подключение к Telegram"
msgid "Testing URL"
msgstr "URL проверки"
msgid "Testing..."
msgstr "Тестирование..."
msgid "Testing…"
msgstr "Проверка…"
@@ -3884,6 +3887,9 @@ msgstr "Верхний уровень имеет наивысший приори
msgid "Total lines:"
msgstr "Всего строк:"
msgid "Total targets"
msgstr "Всего целей"
msgid "Trace"
msgstr "Трассировка"
@@ -3962,6 +3968,9 @@ msgstr "Тип"
msgid "UDP (Unprotected DNS)"
msgstr "UDP (незащищённый DNS)"
msgid "UDP / QUIC protocols"
msgstr "UDP / QUIC протоколы"
msgid "Ukraine (ua)"
msgstr "Украина (ua)"
@@ -3977,6 +3986,9 @@ msgstr "Не удалось проверить стратегию NFQWS2 чер
msgid "Unavailability timeout"
msgstr "Таймаут недоступности"
msgid "Unavailable"
msgstr "Недоступно"
msgid "Underload packet header (H3)"
msgstr "Заголовок пакета под нагрузкой (H3)"
@@ -4211,6 +4223,9 @@ msgstr "Видео"
msgid "View logs"
msgstr "Посмотреть логи"
msgid "View logs failed"
msgstr "Не удалось открыть логи"
msgid "Visible name of this section"
msgstr "Видимое имя этой секции"
@@ -4523,18 +4538,12 @@ 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 "Цели в конфигурациях секций не найдены."
@@ -4571,15 +4580,6 @@ 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 "Тип маршрута"
@@ -4589,15 +4589,6 @@ msgstr "Активные маршруты"
msgid "Custom Domains"
msgstr "Свои домены"
msgid "Unavailable"
msgstr "Недоступно"
msgid "Available"
msgstr "Доступно"
msgid "Proxy"
msgstr "Прокси"
msgid "TCP"
msgstr "TCP"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -180,7 +180,7 @@ function ensure_embeddings(index, provider, api_key, custom_url, model_override)
let texts = [];
for (let i = 0; i < length(index.chunks); i++)
push(index.chunks[i].text, texts);
push(texts, index.chunks[i].text);
let embeddings = call_embedding_api(api_url, api_key, model, texts);
if (!embeddings || length(embeddings) != length(texts))
+139 -103
View File
@@ -2336,7 +2336,7 @@ function run_doctor_checks_impl(repair) {
push(report, "");
push(report, "ℹ️ Автоматические проверки приостановлены в режиме Safe Bypass.");
return { report: join("\n", report) + "\n", issues, fixed };
return { report: join("\n", report) + "\n", issues, fixed, checks };
}
let binary_name = "sing-box";
@@ -3460,6 +3460,7 @@ function doctor(format, repair) {
}
print(sprintf("%J\n", {
success: true,
busy: res.busy == true,
issues: res.issues,
fixed: res.fixed,
planned_fixes: res.planned_fixes || [],
@@ -3512,43 +3513,76 @@ function diagnose_json() {
let res = run_doctor_checks();
let problems = [];
for (let line in split(res.report, "\n")) {
line = trim(as_string(line));
if (line == "") continue;
let checks = res.checks || [];
let severity = null;
let fixed_inline = false;
if (index(line, "❌") >= 0) {
severity = "critical";
} else if (index(line, "⚠️") >= 0) {
severity = "warning";
} else if (index(line, "ℹ️") >= 0) {
severity = "info";
if (length(checks) > 0) {
// Structured path: build problems directly from checks[]
for (let chk in checks) {
if (chk.status == "pass") continue;
let severity = (chk.status == "fail") ? "critical" : ((chk.status == "warn") ? "warning" : "info");
let raw_fix = trim(as_string(chk.fix || ""));
let fixed_inline = index(raw_fix, "FIXED:") >= 0;
if (fixed_inline) severity = "info";
// Strip leading arrow from fix hint
let suggested_fix = raw_fix;
if (length(suggested_fix) >= 3 && substr(suggested_fix, 0, 3) == "→") {
suggested_fix = trim(substr(suggested_fix, 3));
}
let description = as_string(chk.name);
if (suggested_fix == "" && chk.status != "info") {
description = trim(description + " " + as_string(chk.detail));
}
push(problems, {
check: chk.name,
severity: severity,
description: description,
suggested_fix: suggested_fix,
evidence: as_string(chk.detail),
fixed: fixed_inline
});
}
} else {
// Legacy fallback for recovery mode (no structured checks)
for (let line in split(res.report, "\n")) {
line = trim(as_string(line));
if (line == "") continue;
if (severity == null) continue;
let severity = null;
let fixed_inline = false;
if (index(line, "❌") >= 0) {
severity = "critical";
} else if (index(line, "⚠️") >= 0) {
severity = "warning";
} else if (index(line, "ℹ️") >= 0) {
severity = "info";
}
// Extract check name and message (format: icon name message)
let clean = replace(replace(replace(replace(
line, "❌", ""), "⚠️", ""), "ℹ️", ""), "✅", "");
clean = trim(clean);
if (severity == null) continue;
let arrow_idx = index(clean, "→");
let description = trim(arrow_idx >= 0 ? substr(clean, 0, arrow_idx) : clean);
// "→" is U+2192, encoded as 3 UTF-8 bytes (E2 86 92); skip all 3
let suggested_fix = arrow_idx >= 0 ? trim(substr(clean, arrow_idx + 3)) : "";
let clean = replace(replace(replace(replace(
line, "❌", ""), "⚠️", ""), "ℹ️", ""), "✅", "");
clean = trim(clean);
if (index(suggested_fix, "FIXED:") >= 0) {
fixed_inline = true;
severity = "info"; // was critical but already fixed
let arrow_idx = index(clean, "→");
let description = trim(arrow_idx >= 0 ? substr(clean, 0, arrow_idx) : clean);
let suggested_fix = arrow_idx >= 0 ? trim(substr(clean, arrow_idx + 3)) : "";
if (index(suggested_fix, "FIXED:") >= 0) {
fixed_inline = true;
severity = "info";
}
push(problems, {
severity: severity,
description: description,
suggested_fix: suggested_fix,
fixed: fixed_inline
});
}
push(problems, {
severity: severity,
description: description,
suggested_fix: suggested_fix,
fixed: fixed_inline
});
}
let ai_status_data = {};
@@ -3573,6 +3607,8 @@ function diagnose_json() {
return 0;
}
// A fix that has been applied several times without changing the picture is
// not going to work on the next attempt either — recommending it again is how
// the doctor ends up proposing the same repairs forever (issue #31). Every
@@ -3715,97 +3751,99 @@ function apply_quick_fix(codes_str) {
let msg = "";
if (c == "start_singbox") {
command_status("/usr/bin/tachyon restore_dnsmasq 2>/dev/null; /etc/init.d/sing-box restart >/dev/null 2>&1");
status = true;
msg = "sing-box restarted";
let rc = command_status("/usr/bin/tachyon restore_dnsmasq 2>/dev/null; /etc/init.d/sing-box restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "sing-box restarted" : "sing-box restart failed (exit " + rc + ")";
} else if (c == "rebuild_rules") {
command_status("/etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "Firewall rules rebuilt";
let rc = command_status("/etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Firewall rules rebuilt" : "Firewall reload failed (exit " + rc + ")";
} else if (c == "fix_dnsmasq") {
command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1");
status = true;
msg = "dnsmasq restarted";
let rc = command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "dnsmasq restarted" : "dnsmasq restart failed (exit " + rc + ")";
} else if (c == "fix_resolv_symlink") {
command_status("ln -sf /tmp/resolv.conf.auto /etc/resolv.conf 2>/dev/null || ln -sf /tmp/resolv.conf.d/resolv.conf.auto /etc/resolv.conf 2>/dev/null");
status = true;
msg = "resolv.conf symlink fixed";
let rc = command_status("ln -sf /tmp/resolv.conf.auto /etc/resolv.conf 2>/dev/null || ln -sf /tmp/resolv.conf.d/resolv.conf.auto /etc/resolv.conf 2>/dev/null");
status = (rc == 0);
msg = status ? "resolv.conf symlink fixed" : "resolv.conf symlink fix failed (exit " + rc + ")";
} else if (c == "start_watchdog") {
command_status("/etc/init.d/tachyon restart >/dev/null 2>&1");
status = true;
msg = "Watchdog started";
let rc = command_status("/etc/init.d/tachyon restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Watchdog started" : "Watchdog start failed (exit " + rc + ")";
} else if (c == "restart_singbox_dns") {
command_status("/etc/init.d/sing-box restart >/dev/null 2>&1");
status = true;
msg = "sing-box DNS restarted";
let rc = command_status("/etc/init.d/sing-box restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "sing-box DNS restarted" : "sing-box DNS restart failed (exit " + rc + ")";
} else if (c == "fix_uci_config") {
command_status("cp /etc/config/tachyon.bak /etc/config/tachyon 2>/dev/null");
status = true;
msg = "UCI config restored from backup";
status = uci_backup_restore();
if (!status) {
status = command_status("cp /etc/config/tachyon.bak /etc/config/tachyon 2>/dev/null") == 0;
}
msg = status ? "UCI config restored from backup" : "UCI config restore failed (no valid backup found)";
} else if (c == "fix_wan_interface") {
command_status("ubus call network.interface.wan up 2>/dev/null; ifup wan 2>/dev/null; ip route flush cache 2>/dev/null");
status = true;
msg = "WAN interface re-up triggered";
let rc = command_status("ubus call network.interface.wan up 2>/dev/null; ifup wan 2>/dev/null; ip route flush cache 2>/dev/null");
status = (rc == 0);
msg = status ? "WAN interface re-up triggered" : "WAN interface re-up failed (exit " + rc + ")";
} else if (c == "fix_gateway") {
command_status("/etc/init.d/network restart >/dev/null 2>&1");
status = true;
msg = "Network restarted to resolve gateway";
let rc = command_status("/etc/init.d/network restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Network restarted to resolve gateway" : "Network restart failed (exit " + rc + ")";
} else if (c == "clear_dns_cache") {
command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1; ip route flush cache 2>/dev/null");
status = true;
msg = "DNS cache cleared and dnsmasq restarted";
let rc = command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1; ip route flush cache 2>/dev/null");
status = (rc == 0);
msg = status ? "DNS cache cleared and dnsmasq restarted" : "DNS cache clear failed (exit " + rc + ")";
} else if (c == "update_subscriptions") {
command_status(command_from_args([ "/usr/bin/tachyon", "component_action_async", "update_subscriptions", "update" ]));
status = true;
msg = "Subscription update triggered";
let rc = command_status(command_from_args([ "/usr/bin/tachyon", "component_action_async", "update_subscriptions", "update" ]));
status = (rc == 0);
msg = status ? "Subscription update triggered" : "Subscription update trigger failed (exit " + rc + ")";
} else if (c == "reset_firewall") {
command_status("/etc/init.d/firewall restart >/dev/null 2>&1; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "Firewall restarted";
let rc = command_status("/etc/init.d/firewall restart >/dev/null 2>&1; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Firewall restarted" : "Firewall restart failed (exit " + rc + ")";
} else if (c == "restart_network") {
command_status("/etc/init.d/network restart >/dev/null 2>&1");
status = true;
msg = "Network service restarted";
let rc = command_status("/etc/init.d/network restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Network service restarted" : "Network restart failed (exit " + rc + ")";
} else if (c == "restart_zapret") {
command_status("/etc/init.d/zapret restart 2>/dev/null; /etc/init.d/zapret2 restart 2>/dev/null; /etc/init.d/byedpi restart 2>/dev/null");
status = true;
msg = "Zapret/ByeDPI engines restarted";
let rc = command_status("/etc/init.d/zapret restart 2>/dev/null; /etc/init.d/zapret2 restart 2>/dev/null; /etc/init.d/byedpi restart 2>/dev/null");
status = (rc == 0);
msg = status ? "Zapret/ByeDPI engines restarted" : "Zapret/ByeDPI restart failed (exit " + rc + ")";
} else if (c == "optimize_memory") {
command_status("sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null; rm -rf /tmp/sing-box/*.tmp 2>/dev/null; /etc/init.d/sing-box restart >/dev/null 2>&1");
status = true;
msg = "Memory caches flushed and sing-box restarted";
let rc = command_status("sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null; rm -rf /tmp/sing-box/*.tmp 2>/dev/null; /etc/init.d/sing-box restart >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Memory caches flushed and sing-box restarted" : "Memory optimization failed (exit " + rc + ")";
} else if (c == "switch_to_doh") {
command_status("uci set tachyon.settings.dns_type='doh'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "Switched DNS mode to DoH and reloaded firewall";
let rc = command_status("uci set tachyon.settings.dns_type='doh'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Switched DNS mode to DoH and reloaded firewall" : "DoH switch failed (exit " + rc + ")";
} else if (c == "heal_network_stack") {
command_status("ln -sf /tmp/resolv.conf /etc/resolv.conf 2>/dev/null; /etc/init.d/dnsmasq restart >/dev/null 2>&1; /etc/init.d/tachyon restart >/dev/null 2>&1; ip route flush cache 2>/dev/null; ubus call network.interface.wan up 2>/dev/null; ifup wan 2>/dev/null");
status = true;
msg = "Full network stack recovery executed";
let rc = command_status("ln -sf /tmp/resolv.conf /etc/resolv.conf 2>/dev/null; /etc/init.d/dnsmasq restart >/dev/null 2>&1; /etc/init.d/tachyon restart >/dev/null 2>&1; ip route flush cache 2>/dev/null; ubus call network.interface.wan up 2>/dev/null; ifup wan 2>/dev/null");
status = (rc == 0);
msg = status ? "Full network stack recovery executed" : "Network stack recovery failed (exit " + rc + ")";
} else if (c == "enable_safe_bypass") {
command_status("uci set tachyon.settings.recovery_bypass='1'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "Safe Direct WAN bypass enabled";
let rc = command_status("uci set tachyon.settings.recovery_bypass='1'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Safe Direct WAN bypass enabled" : "Safe bypass enable failed (exit " + rc + ")";
} else if (c == "restore_native_internet") {
command_status("/usr/bin/tachyon restore_dnsmasq 2>/dev/null; /etc/init.d/tachyon stop >/dev/null 2>&1; nft delete table inet TachyonTable 2>/dev/null; ip -4 rule del fwmark 0x04000000/0x04000000 table tachyon 2>/dev/null; ip -6 rule del fwmark 0x04000000/0x04000000 table tachyon 2>/dev/null; ip route flush table tachyon 2>/dev/null; /etc/init.d/dnsmasq restart >/dev/null 2>&1; ip route flush cache 2>/dev/null");
status = true;
msg = "Tachyon stopped, stock dnsmasq and native direct internet routing restored";
let rc = command_status("/usr/bin/tachyon restore_dnsmasq 2>/dev/null; /etc/init.d/tachyon stop >/dev/null 2>&1; nft delete table inet TachyonTable 2>/dev/null; ip -4 rule del fwmark 0x04000000/0x04000000 table tachyon 2>/dev/null; ip -6 rule del fwmark 0x04000000/0x04000000 table tachyon 2>/dev/null; ip route flush table tachyon 2>/dev/null; /etc/init.d/dnsmasq restart >/dev/null 2>&1; ip route flush cache 2>/dev/null");
status = (rc == 0);
msg = status ? "Tachyon stopped, stock dnsmasq and native direct internet routing restored" : "Native internet restore failed (exit " + rc + ")";
} else if (c == "fix_system_time") {
command_status("ntpd -q -p pool.ntp.org 2>/dev/null || rdate -s time.cloudflare.com 2>/dev/null; /etc/init.d/sysntpd restart 2>/dev/null");
status = true;
msg = "System time synchronized with NTP";
let rc = command_status("ntpd -q -p pool.ntp.org 2>/dev/null || rdate -s time.cloudflare.com 2>/dev/null; /etc/init.d/sysntpd restart 2>/dev/null");
status = (rc == 0);
msg = status ? "System time synchronized with NTP" : "NTP sync failed (exit " + rc + ")";
} else if (c == "flush_conntrack") {
command_status("sysctl -w net.netfilter.nf_conntrack_max=65536 2>/dev/null; echo 1 > /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null || true; conntrack -F 2>/dev/null || true");
status = true;
msg = "Conntrack table limits expanded and flushed";
let rc = command_status("sysctl -w net.netfilter.nf_conntrack_max=65536 2>/dev/null; echo 1 > /proc/sys/net/netfilter/nf_conntrack_max 2>/dev/null || true; conntrack -F 2>/dev/null || true");
status = (rc == 0);
msg = status ? "Conntrack table limits expanded and flushed" : "Conntrack flush failed (exit " + rc + ")";
} else if (c == "fix_bootstrap_dns") {
command_status("uci set tachyon.settings.bootstrap_dns_server='77.88.8.8'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "Bootstrap DNS reset to reliable public resolver (77.88.8.8) and reloaded";
let rc = command_status("uci set tachyon.settings.bootstrap_dns_server='77.88.8.8'; uci commit tachyon; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "Bootstrap DNS reset to reliable public resolver (77.88.8.8) and reloaded" : "Bootstrap DNS fix failed (exit " + rc + ")";
} else if (c == "optimize_mtu") {
command_status("/usr/bin/tachyon discover-awg-mtu 2>/dev/null; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = true;
msg = "AWG MTU discovery executed and tunnels reloaded";
let rc = command_status("/usr/bin/tachyon discover-awg-mtu 2>/dev/null; /etc/init.d/tachyon reload >/dev/null 2>&1");
status = (rc == 0);
msg = status ? "AWG MTU discovery executed and tunnels reloaded" : "MTU optimization failed (exit " + rc + ")";
} else {
status = false;
msg = "Unknown fix code: " + c;
@@ -3813,9 +3851,7 @@ function apply_quick_fix(codes_str) {
}
push(results, { code: c, success: status, message: msg });
if (status) {
doctor_fix_record(c);
}
doctor_fix_record(c);
}
let post_verify = verify_system();
+25 -2
View File
@@ -76,11 +76,12 @@ grep -Fq 'wan_fail_streak: int(st.wan_fail_streak || 0)' "$TACHYON_LIB/service/w
# 5. Fix usage tracker: applying the same fix must be recorded, and fixes
# applied 3+ times within the hour must be withheld from recommendations
# (issue #31: doctor proposing the same repairs forever).
# Use fix_resolv_symlink (ln -sf works without system services in Docker).
rm -f /tmp/tachyon_doctor_fixes.json
for i in 1 2 3; do
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/diagnostics/runtime.uc" apply-quick-fix "clear_dns_cache" >/dev/null 2>&1 || true
ucode -L "$TACHYON_LIB" "$TACHYON_LIB/diagnostics/runtime.uc" apply-quick-fix "fix_resolv_symlink" >/dev/null 2>&1 || true
done
grep -Fq '"clear_dns_cache"' /tmp/tachyon_doctor_fixes.json ||
grep -Fq '"fix_resolv_symlink"' /tmp/tachyon_doctor_fixes.json ||
fail "apply_quick_fix must record applied fixes in the doctor usage tracker"
grep -Eq '"count": ?3' /tmp/tachyon_doctor_fixes.json ||
fail "doctor usage tracker must count repeated applications"
@@ -89,4 +90,26 @@ grep -Fq 'function doctor_fix_overused(' "$TACHYON_LIB/diagnostics/runtime.uc" |
grep -Fq 'if (doctor_fix_overused(code)) return;' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "add_fix must withhold overused fixes"
# 6. fix_uci_config must use uci_backup_restore() (correct path)
grep -Fq 'uci_backup_restore()' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "fix_uci_config must call uci_backup_restore() for correct backup path"
# 7. apply_quick_fix must capture honest exit codes
grep -Fq 'let rc = command_status(' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "apply_quick_fix must capture command exit codes"
# 8. doctor() must propagate busy flag
grep -Fq 'busy: res.busy == true' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "doctor() must propagate busy flag in output"
# 9. diagnose_json must use structured checks[] when available
grep -Fq 'Structured path: build problems directly from checks' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "diagnose_json must build problems from structured checks[]"
grep -Fq 'Legacy fallback for recovery mode' "$TACHYON_LIB/diagnostics/runtime.uc" ||
fail "diagnose_json must have legacy emoji fallback for recovery mode"
# 10. rag.uc push args must be correct (push array, value)
grep -Fq 'push(texts, index.chunks[i].text)' "$TACHYON_LIB/diagnostics/rag.uc" ||
fail "rag.uc must push with correct argument order: push(array, value)"
printf 'local AI Doctor checks passed\n'