Backend (runtime.uc, agent_api.uc, tachyon CLI):
- Enrich AI Doctor LLM context with Watchdog state (OOM, streaks, active repairs) and last 15 critical log lines
- Support multi-fix parsing (quick_fixes array) and comma-separated FIX tags
- Add persistent history saving to /tmp/ai_doctor_last.json and ai_doctor_last endpoint
- Expand quick fix codes from 9 to 13 (added clear_dns_cache, update_subscriptions, reset_firewall, restart_network)
- Implement apply_quick_fix CLI command and REST API endpoint
Frontend (fe-app-tachyon, LuCI main.js):
- Add AI Doctor action button in Diagnostics tab
- Add shell methods aiDoctor, aiDoctorLast, applyQuickFix
- Render AI Doctor modal with Multi-Fix buttons and 'Fix All' option
- Integrate toast notifications and state updates
- watchdog: удалён дублирующий check_tachyon_cli_running(), везде используется controller.check_tachyon_cli_running()
- watchdog: smart_detect_process_pending использует controller.proxy_port() вместо повторного парсинга config.json
- watchdog: удалена однострочная обёртка is_dns_dead() — заменена is_dns_working() напрямую
- watchdog: heal_proxy_connectivity требует streak >= 2 перед очисткой cache.db — устраняет ложные ночные уведомления при кратких просадках прокси-ноды
- event_controller: push_history переведён на slice() вместо ручного цикла
- event_controller: direct_ok использует тот же check_url что и proxy check — корректность при кастомном ai_proxy_health_url
- event_controller: добавлен subscription_fail_streak — одиночный таймаут сети больше не триггерит Telegram
- telegram: tg_request больше не создаёт временные файлы в /tmp — JSON передаётся напрямую через curl -d, устранена утечка tg_payload_*.json при kill процесса
- telegram: worker() при старте удаляет оставшиеся tg_payload_*.json от предыдущих сессий
- tests: исправлен шаблон mktemp в background_spawn_fds.sh — busybox не поддерживает двойное расширение (.XXXXXX.uc)
- Проверять /proc/<pid>/cmdline при валидации PID (исключает ложное определение dnsmasq/sing-box как владельца блокировки при рециркуляции PID в Linux)
- Добавить автоматическую очистку зависших блокировок старше 300 секунд (TTL)
- При фоновой автоматической проверке обновлений (action == check_update) пропускать занятые блокировки без записи [error] в syslog и появления всплывающих нотификаций
- kill_matching_command и цикл зачистки fd/1000 прокси в package.uc теперь пропускют процессы ucode (action.uc / updates.uc) и их предков, предотвращая выбивание воркера обновлений по SIGKILL во время prerm
- isTransientRpcError теперь учитывает 'failed to execute' в качестве транзиентной ошибки RPC при перезапуске rpcd / служб во время обновления
Самообновление Tachyon — это компонентное задание, которое устанавливает
пакет, чей postinst и вызывает remove_component_update_cache(). Очистка
стирала каталог component-actions целиком, включая файл состояния того
самого задания, которое ещё выполнялось. LuCI опрашивал задание, не
находил его, не получал ни message, ни stderr и показывал фолбэк
"Failed to execute" поверх успешно прошедшего обновления.
Теперь очистка пропускает задания, помеченные running. Устаревшие
результаты и кэш проверок обновлений убираются как раньше. Зависшее
задание не залипнет: детект по PID и started_at помечает такие stale.
Пути каталогов состояния стали переопределяемыми через окружение, чтобы
тест мог прогнать luci-postinst, не трогая /var/run.
Коммит 3d84d96e добавил constants.TACHYON_COMMIT_SHA в get_system_status()
без соответствующего require(). В результате constants == null и каждый
вызов api.get_system_status() крашил с 'left-hand side expression is null'.
Это ломало буквально все команды Telegram-бота (/menu, /status, и т.д.)
так как каждая из них вызывает view_menu() -> api.get_system_status().
- get_proxy_args() теперь поддерживает опцию UCI bot_proxy_section:
если sing-box запущен и секция указана, переключает GLOBAL outbound через REST API
- Исправлен краш 'left-hand side expression is null' при обработке
callback_query без поля from (например channel_post callbacks)
- В LuCI settings.js добавлен ListValue 'bot_proxy_section' с выпадающим
списком активных секций (паттерн аналогичен download_via_proxy_section)
- Локализация: ru/tachyon.po и templates/tachyon.pot синхронизированы
- Пересборка main.js через npm run build
`ps | grep '/usr/bin/tachyon'` matches the hook itself, because
/usr/bin/tachyon is what invoked it. The sweep therefore killed its own
parent, the parent died on SIGKILL, and apk saw
`tachyon-<version>.pre-upgrade: exited with error 247` (128+119) and
reported a failed hook.
The sweep now walks /proc/<pid>/stat to collect its own ancestor chain and
skips those pids.
Holders of procd's serialization lock are also cleared by descriptor rather
than by name. procd_lock() does `exec 1000>` on
/var/lock/procd_tachyon.lock and then `flock 1000` with no -w, so any
background spawn inheriting fd 1000 keeps the lock held. On a live router
the holders were watchdog workers, logread, the zapret2 supervisors, nfqws2
and a stray curl — none of which match a tachyon name pattern. While any of
them lives, every init.d call blocks in flock until the package manager
gives up on the hook.
Same descriptor-based sweep added to install.sh, which faces the same lock
left behind by pre-fix builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
procd_lock() in /lib/functions/procd.sh does `exec 1000>/var/lock/procd_<svc>.lock`
followed by `flock 1000` with no -w timeout, so descriptor 1000 IS the procd
serialization lock. The spawn prologue capped its /proc/self/fd sweep at 999 and
therefore skipped exactly that descriptor: every background spawn inherited the
held lock and kept it alive long after the init script that took it had exited.
On the router eleven processes were found pinning /var/lock/procd_tachyon.lock
that way — watchdog workers, logread -f, both zapret2 supervisors and their sh
wrappers, both nfqws2 instances, the telegram worker and a stray curl. With the
lock permanently held, every later /etc/init.d/tachyon call blocked forever in
flock. That is what hung package_prerm until apk killed the pre-upgrade hook and
rolled the whole transaction back, leaving the old build installed and
__COMPILED_COMMIT_SHA__ unsubstituted in the installed constants module.
Killing the flock processes could not fix it: the descriptor lives on in every
inheriting child.
Raise the ceiling past 1000 in all three copies of the prologue (core/common.uc
and the two early-boot duplicates that deliberately carry their own helpers).
tests/background_spawn_fds.sh gains a structural check that every __tfd ceiling
covers 1000 — the runtime probe only runs on a shell that can close high
descriptors, and dash cannot, so CI would never reach it — plus a runtime case
asserting fd 1000 does not survive the prologue where the shell allows it.
Verified on the router: lock holders 0, package_prerm no longer blocks,
the 1.2.66 rebuild installs and constants.uc reports 2fc57d38.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The router upgrade died at the installer's 120s ceiling while apk was
running tachyon-1.2.66.pre-upgrade, so apk rolled the transaction back
and the old build stayed installed.
prerm reached INIT_PATH status through rc.common, which waits on
flock -w 1000. A stuck start or retry_start_on_wan_up holding that lock
stalls the hook for up to a quarter of an hour. Every init call in the
hooks is now bounded, the status probe moved after the flock waiters are
killed, and the Makefile hook bounds the old version's cleanup from the
outside since that code cannot be trusted to bound itself.
The installer also called a rollback a success: a rebuilt tag keeps its
version, so both the presence and version checks still passed. It now
compares the commit SHA baked into the installed constants module and
fails loudly when it did not move. Package installs get 300s, since a
kill mid-transaction is what caused the rollback.
opkg считает пакет уже установленным при равенстве версий, поэтому
пересборка под тем же тегом молча не применялась: задание завершалось
успехом, билд оставался старым. Добавлен --force-reinstall в
pkg_install_files_command (opkg) и в install.sh; apk не тронут, он
всегда пишет переданный файл.
Детект пересборки больше не зависит только от SHA, которого нет в
релизах старше f974e4a0: добавлен отпечаток билда по published_at и
метаданным ipk-ассета, решение вынесено в tachyon_build_differs с
префиксным сравнением SHA разной длины. Отпечаток установленного
билда пишется в state-файл и попадает в extra.
Telegram уведомляет о пересборках: check_notified_updates пропускает
outdated_same_release, дедуп идёт по версии и SHA, текст показывает
короткие SHA вместо пары одинаковых версий.
Кэш проверки обновлений сохраняет current_sha из результата действия
вместо безусловного latest, иначе неудачная установка выглядела как
"актуально".
Фронт пересобран из TS, ручная правка main.js снята. Добавлен
describeSameReleaseBuild с фолбэк-текстом, когда SHA недоступен.
- postinst_restore: switch from BIN_PATH start to INIT_PATH start so
the test-injected init script is exercised (the test sets TACHYON_INIT
to a fake init that writes to a log; BIN_PATH was never the fake init)
- postinst_restore: guard process-killing with !PACKAGE_TEST_MODE,
consistent with prerm_cleanup — the start call itself is always made
- flock deadlock risk is gone: prerm_cleanup now kills all waiters
before removing the routing table entry, so INIT_PATH start is safe
- service/package.uc: call INIT_PATH stop before process-killing in
prerm_cleanup so the routing table entry is still visible when the
service shuts down (fixes test: 'prerm must stop Tachyon before
removing its routing table name')
- service/package.uc: replace awk regex with | alternation and \/
escaping with grep -E / grep -F; BusyBox awk does not support |
in regex literals and silently mishandles \/ — fixes awk syntax
errors in CI (both prerm_cleanup and postinst_restore)
- fe-app-tachyon: pin js-yaml to ^5.2.1 via overrides to fix the
quadratic O(n^2) !!omap resolution DoS (same root cause as
CVE-2026-59870 / GHSA-724g-mxrg-4qvm, never backported to 3.x/4.x)
- fe-app-tachyon: npm audit fix for brace-expansion and nanoid vulns
На роутере со 128 МБ несколько мест читали в память вывод, размер которого
ничем не ограничен. Распаковка gzip подписки шла через пайп с чтением целиком:
скачивание curl ограничено 15 МБ, но коэффициент сжатия base64-текста высокий,
так что пик доходил до десятков мегабайт. Оба вызывающих места передают файл и
ждут файл, поэтому распакованный поток теперь уходит редиректом сразу в
выходной файл, а успех определяется по коду возврата и ненулевому размеру.
Таблица nftables содержит сеты с десятками тысяч элементов из списков, и
`nft list table` печатает их все. Проба контроллера делала это каждый тик,
хотя ищет только имена цепочек и правил; четыре проверки доктора и вывод
чужих меток из полного рулесета — то же самое. Везде добавлен grep на
источнике, отбирающий только те строки, которые код потом и разбирает.
Два просмотра логов читали кольцевой буфер syslog целиком, чтобы показать
несколько сотен строк; теперь количество строк ограничено на стороне logread.
Пустых catch в кодовой базе около полусотни, и они не одинаковые. Большинство
выражает намерение: fs.unlink бросает на ENOENT, а вызывающий говорит «пусть
этого файла не будет», и отсутствующий файл это условие уже выполняет; разбор
json в цикле пропускает мусорную строку и берёт следующую. Такие места теперь
несут пояснение, почему пустое тело здесь и есть правильный ответ.
Семь мест к этому классу не относились: там сбой означал потерю состояния, а
дальше код шёл так, будто всё получилось.
Watchdog писал через пустой catch файл лока перезапуска прокси, шкалу
GOMEMLIMIT после OOM, бэкап конфига и метку keepalive. Не лёгший лок означает,
что следующий тик перезапустит прокси заново, а счётчик rate-limit об этом не
узнает; не записанная метка keepalive заставляет init.d считать watchdog мёртвым
и перезапускать его. Все четыре теперь идут через write_state_file, который
логирует отказ. Метка пишется на каждом тике uloop, поэтому безусловная строка
залила бы syslog именно при переполненной файловой системе — там, где лог нужнее
всего; вместо этого каждый путь сообщает о первом отказе и молчит до следующего
успеха, что заодно делает видимым восстановление.
Контроллер разбирал /etc/sing-box/config.json ради портов http и tproxy и при
неудаче молча брал 4534 и 4530. Watchdog после этого опрашивает не тот порт,
вечно считает прокси нерабочим и лечит его по кругу — при том, что в логе нет
ничего, что связало бы это с конфигом. Разбор вынесен в общую функцию, которая
сообщает о непарсящемся конфиге один раз на процесс.
Telegram молча терял карту callback-токенов: токен уже вшит в клавиатуру к
моменту записи, поэтому провал отдаёт пользователю кнопку, которая на нажатие
ответит «кнопка устарела». Обновление списков молча теряло маркер отложенного
применения nft — списки обновлены на диске и до nftables не доехали никогда,
а строкой выше в лог написано, что маркер сохраняется. urltest молча выкидывал
битое регулярное выражение из конфига пользователя, отчего группа тихо
подхватывала меньше outbound-ов, чем сказано в конфиге.
tests/heal_escalation.sh проверял порядок строк по литералу fs.writefile для
лока и обновлён под новый вызов.
ucode держит по одному открытому дескриптору на каждый require()-модуль всё
время жизни интерпретатора и не даёт ни закрыть их, ни пометить close-on-exec,
поэтому каждый фоновый процесс наследует весь набор. На роутере watchdog несёт
около 90 таких дескрипторов и передаёт их детям: logread, sh и nfqws2 найдены
держащими дескрипторы на собственных .uc-файлах Tachyon, часть уже удалена
обновлением пакета — старый инод прибит к диску процессом, которому он не нужен.
Против этого код писал литерал 1000<&- в 22 местах 13 модулей. Он не закрывал
ничего: дескриптор 1000 никогда не открывается, а все настоящие лежат в 6..89.
Хуже того, этого литерала требовал tests/service_start_trap.sh — именно поэтому
он и разошёлся по кодовой базе. Проверка пинила видимость защиты и продолжала бы
проходить вечно при живой утечке.
Замена оказалась не такой прямой, как выглядит. Счёт от 3 до потолка не годится:
в dash закрытие дескриптора 10 и выше фатально даже для номера, который никогда
не открывался — dash перемещает туда собственную служебную бухгалтерию, и
перенаправление убивает шелл целиком, а 2>/dev/null прячет сообщение, но не
смерть. Перечисление /proc/self/fd трогает только реально открытое, но сами
высокие дескрипторы никуда не деваются: набор модулей любого размера уводит их
за 10. Поэтому пролог сначала спрашивает шелл в подоболочке, которая поглощает
смерть, и опускает собственный потолок, если ответ отрицательный. busybox ash,
на котором работает OpenWrt, отвечает утвердительно и закрывает весь набор.
Три хелпера в core/common.uc задают форму фонового запуска — раньше каждое место
собирало перенаправления вручную, и они разошлись. Форма с пидом ставит exec
внутрь подоболочки, чтобы $! называл сам демон, а не обёртку, которая тут же
завершается; ведущие присваивания VAR=x при этом выносятся перед exec, иначе
шелл ищет программу с именем VAR=x. Для конвейеров, которые exec заменить не
может, есть отдельный хелпер. initd.uc и state.uc лежат на пути раннего старта и
несут собственные копии, как уже несут shell_quote и command_from_args.
Попутно вскрылись два настоящих дефекта. В лестнице эскалации watchdog строка
читалась как restart & rm -f lock &, то есть лок снимался параллельно с
рестартом, который он охраняет, и вторая эскалация могла начаться посреди
первой. В пакетном прогреве Zero-RTT знак & стоял внутри строки, поэтому
перенаправления после него цеплялись к пустой команде в родительском шелле,
а не к фоновому циклу.
Ещё четыре места запускали фон вообще мимо механизма; одно из них аудит
пропустил, его нашёл новый тест.
tests/background_spawn_fds.sh считает дескрипторы у ребёнка при обоих способах
запуска: структурная проверка такое показать не может, строку надо выполнить.
На роутере старый способ даёт ребёнку 7 дескрипторов, новый — 3.
heal_dns_continuous выставляет noresolv в единственное фиксированное значение, поэтому начиная со второго подряд сбоя DNS он переписывал настройку, которая уже содержала это значение, — и всё равно выполнял commit и reload dnsmasq. Перезагрузка сбрасывает кеш резолвера и заново перечитывает все конфигурационные файлы, чтобы вернуться туда же, откуда начали. Три сбоя DNS подряд — это ровно тот момент, когда вероятен четвёртый, так что бесполезная перезагрузка приходила именно тогда, когда резолвер меньше всего мог себе её позволить.
Теперь восстановитель сначала читает dhcp.@dnsmasq[0].noresolv через uci_core. Расхождение устраняется как прежде; совпадение не трогается, а исход честно сообщается как `skipped`, а не как несостоявшийся ремонт.
Счётчик подряд идущих сбоев сбрасывается и на этом пути. Без сброса факт DNS_DOWN приходит на каждом быстром тике, и восстановитель входит в работу бесконечно — ничего не делая, но каждый раз рапортуя. Сброс фиксирует, что факт обработан: dnsmasq уже содержит нужную конфигурацию, значит у сбоя другая причина и она принадлежит другому подписчику.
Тест `tests/heal_dns_idempotent.sh` прогоняет извлечённую функцию на отсутствующем значении, неверном значении, верном значении и событии ниже порога, подсчитывая команды commit и reload в каждом случае.
Падение WAN-интерфейса влечет за собой сбой прокси, DNS и проверок доступности. У каждого из них был свой подписчик, и каждый независимо решал перезапустить службу — три ремонта на один сбой, ни один из которых не мог сработать до восстановления WAN. Ограничение частоты в safe_proxy_restart() сдерживало этот каскад только постфактум, но в коде не было явной связи, что сбои прокси и DNS — последствия сбоя WAN.
Шина уже ранжирует восстановители: WAN выполняется первым с приоритетом 10, sing-box с 20, прокси и DNS — с 40 и 50. Чтобы не вводить вторую иерархию, эти ранги вынесены в именованные константы (PRIORITY_WAN, PRIORITY_SINGBOX, PRIORITY_PROXY, PRIORITY_DNS), используемые как при подписке, так и при проверке подавления.
Восстановитель, устраняющий первопричину, удерживает свой ранг на время ремонта. Вторичный восстановитель приостанавливает работу и выводит сообщение в лог. Три свойства гарантируют надежность: причина никогда не подавляет сама себя; следствие не может подавить причину; срок удержания ограничен, поэтому зависший ремонт не заблокирует вочдог навсегда.
Удержание снимается по событию восстановления, не дожидаясь таймаута. Для этого добавлено новое событие EV.WAN_UP — ранее probe_wan публиковал только сбои.
Удержание WAN захватывается до `ifdown wan`, так как сам ifdown вызовет ошибки прокси и DNS ещё до завершения работы ifup.
Тест `tests/heal_suppression.sh` проверяет ранги, точки захвата/освобождения, защиту восстановителей и логирование пропусков.
При падении процесса sing-box или отсутствии ответа вочдог выполнял полный перезапуск стека, включая правила файрвола и прокси обхода. Это вызывало избыточный простой сети при локальных сбоях sing-box.
Теперь вочдог сначала пробует точечный перезапуск службы sing-box. Эскалация до полного перезапуска стека происходит только если sing-box не восстановился.
Ранее вочдог логировал попытки ремонта как успешные ещё до их выполнения. Если переключение сервиса или перезапуск завершались ошибкой, в логе всё равно отображался успех.
Теперь восстановители проверяют результат действий и логируют фактический исход (успех или ошибка) вместе с подробной контекстной информацией.
Ранее вочдог опрашивал состояние системы и устранял проблемы в едином смешанном цикле. Добавление новой проверки или действия по восстановлению требовало изменения монолитной функции, где мониторинг и ремонт были переплетены, что затрудняло аудит порядка выполнения и изолированное тестирование.
Вводится `usr/lib/service/event_controller.uc` — шина событий pub/sub для вочдога. Теперь зонды публикуют факты (`EV.PROXY_DOWN`, `EV.DNS_DOWN`, `EV.SINGBOX_STOPPED`, `EV.OOM_DETECTED`, `EV.WAN_DOWN` и т.д.), не зная, кто их обрабатывает. Восстановители (healers) подписываются на эти факты и независимо выполняют ремонт.
Ключевые изменения:
- `event_controller.uc` управляет словарём событий, регистрацией подписчиков, сортировкой по приоритетам и синхронной отправкой.
- Зонды (`probe_proxy`, `probe_dns`, `probe_singbox`, `probe_oom`, `probe_wan`) публикуют события вместо прямого вызова функций ремонта.
- Восстановители регистрируются как подписчики с явными приоритетами, чтобы первопричины устранялись раньше вторичных сбоев.
- `tests/event_bus.sh` добавляет интеграционные тесты для отправки событий, сортировки приоритетов и обработки подписчиков.
UI (section.js):
- showUrlTestSettingsModal и showPriorityLevelSettingsModal: предзагрузка
кэша outbound-имён до открытия модала — CBINamedSection.render() не
вызывает section.load(), из-за чего DynamicList рендерился без данных
- renderStackedJsonSettingsModal: вызов map.load() перед map.render()
LuCI ACL (luci-app-tachyon.json):
- Добавлены пути /tmp/run/tachyon/... наряду с /var/run/tachyon/...
На OpenWrt /var — симлинк на /tmp; rpcd резолвит симлинки перед
проверкой ACL, поэтому /var/run/... никогда не давал прав чтения
и fs.read() возвращал PermissionError
Postinstall (50_luci-tachyon):
- Добавлен перезапуск rpcd после установки пакета, чтобы новые ACL
вступали в силу без ручного logout/login в LuCI
Telegram-бот (telegram.uc):
- handle_toggle_mac(): блокировка/разблокировка устройств теперь
через правила firewall (fw4), а не через UCI blocked_macs
- /cb команда: разрешение токенизированных коллбэков для oversized
inline-клавиатур (лимит Telegram 64 байта на callback_data)
- /qh_start, /qh_end: интерактивная настройка часов тихого режима
- in_quiet_hours() и daily report: исправлен localtime(time())
вместо clock() — clock() возвращает [сек, мксек], а не tm-структуру
- Уведомления об обновлениях: current_version вместо installed_version
- Описание команды /restart исправлено
Backend (api.uc, state.uc):
- http_probe(): выделена функция с проверкой HTTP-статуса
(curl выходит 0 даже при 4xx/5xx — теперь проверяем код явно)
- run_speedtest(): учёт curl exit status
- Math.random() заменён на clock()[1] (Math недоступен без import)
- current_year(): убрано +1900 — localtime().year уже полный год
Issue #14: move average_latency() before export_metrics() in watchdog.uc
- average_latency was declared AFTER export_metrics (line 1213 vs 1178)
- in ucode timer/closure contexts the forward reference was not reliably
resolved, causing 'left-hand side is not a function' every 2 min
- fix: move average_latency to before export_metrics so it is always
defined before it is referenced
Issue #15: add close_timeout to MTProxy inbound in singbox/servers.uc
- MTProto clients maintain persistent connections that never close naturally
- without close_timeout, sing-box waits indefinitely for them during SIGHUP
hot-reload and exits with FATAL: 'sing-box did not close!'
- fix: add close_timeout=10s (configurable via mtproto_close_timeout UCI)
so sing-box forcefully closes stale MTProto connections after 10 s and
completes the hot-reload cleanly
- wget: remove -t flag incompatible with BusyBox 25.12+, add curl-first fallback
in download_to_file() with wget as fallback
- startup/reload speedup (5 improvements):
- cut sing-box verification timeout from 45 iterations to 15
- remove redundant second stability wait for sing-box process
- batch requirement check + runtime validation into single ucode fork
- replace dnsmasq full restart with procd reload (config-conditional bounce)
- move telegram and watchdog runtime start to background
- telegram: fix "API failure" spam - empty result[] from getUpdates is now
treated as a successful idle poll, not an error that triggers backoff
- watchdog SmartDetect: extract domain from proxy and VPN URLs for DNS
health check, use real domains (google.com/example.com) instead of
provider-specific endpoints that trigger false alerts
- tests: new invariants for wget -t prohibition, telegram empty-result
handling, DNS apply reload-vs-restart, and combined validation mode ownership
Дашборд:
- Добавлены карточки статуса Zapret, Zapret2, ByeDpi на панель мониторинга
- Показывают статус (работает/остановлен), количество перезапусков, предупреждения
- Баг: API возвращает boolean а не число — исправлено сравнение
Секция Hosts:
- Перенесены опции hosts из глобальных настроек в отдельный тип секции (action=hosts)
- Опции: пользовательские DNS записи, удалённые списки хостов, автообновление, интервал
- Миграция: автоматический перенос глобальных настроек hosts в секцию
- Вкладка «Условия» скрыта для секций типа Hosts (hosts записи применяются глобально)
- Возвращена кнопка «Import Hosts File» с модальным окном загрузки из файла
- Убраны неиспользуемые опции из settings.js (~280 строк)
Локализация:
- Переведены 6 новых строк для превью импорта hosts
Критические исправления:
- safe_proxy_restart: удаление lock-файла после фонового перезапуска
с защитой от stale lock (>300 сек)
- write_json_file: атомарная запись через tmp+mv вместо прямой
- write_reload_state: атомарная запись через tmp+mv
- state_add_list: отклонение значений с пробелами
Исправления безопасности:
- killall logread заменён на pkill -f 'logread -f'
- SIGTERM→SIGKILL для Telegram и sing-box при остановке
- Recover OOM scale: инкремент +0.05 каждые 10 минут
- nfqueue и byedpi: trap для корректного завершения дочерних процессов
- nfqueue: валидация стратегий и откат при ошибке запуска
- nfqueue: flush mangle_output перед добавлением правил
- Исправлен парсинг exit code в 14 файлах (status & 127)
Улучшения стабильности:
- init.d: trap для start_service/stop_service
- watchdog: signal traps для legacy fallback loop
- lifecycle: логирование ошибок при остановке компонентов
- singbox/priority: SIGTERM→SIGKILL при остановке
- telegram: exponential backoff (1x→300s) при ошибках API
- telegram: process_updates возвращает boolean для отслеживания успеха
- watchdog: кэш proxy port для уменьшения чтения конфига sing-box
- singbox/generator: упрощён atomic_write_json_file
- packages.uc: переупорядочивание apk_version (лёгкие запросы первыми)
- dns/apply: проверка PID sing-box перед настройкой dnsmasq
- server/service: cleanup_orphaned_server_certs перед вызовом
- BUG7/8 CRITICAL: view_test_rule() used escape_html() as regex escape,
allowing user input injection. Now uses plain string suffix matching.
- BUG9 HIGH: set_tg_state(chat_id, null) ran BEFORE action execution,
so if the action threw an exception the user lost their state context.
Now state is cleared only after successful UCI commit in each branch.
sec_create with invalid name preserves state for retry.
- BUG13 LOW: /set_arr_del and /sec_del_it callback_data could exceed
Telegram's 64-byte limit with long domain names. Added cb_data()
helper that truncates and appends a hash when over limit.
- CRITICAL: /dns_apply substr offset 12→11 was always applying preset 0
- CRITICAL: /test_rule IP CIDR match now checks subnet, not just format
- HIGH: check_notified_updates now iterates data.results (was iterating
top-level object keys, so update notifications were completely broken)
- HIGH: exec_support_bundle cleanup now deletes /etc/.tachyon/ paths
(was deleting /tmp/ paths that didn't exist)
- HIGH: command_status() now uses command_from_args() for ai_heal,
watchdog_start, watchdog_stop (was passing raw strings)
- HIGH: set_tg_state fs.unlink now wrapped in try/catch
- MEDIUM: close_connections now reports error when curl fails
- MEDIUM: view_connections validates page >= 0
- LOW: /noop callback now silently ignored instead of falling through
- LOW: > command HTML output now uses <code> instead of backticks
- watchdog: add ai_heal_rpcd() - monitors rpcd file descriptor count,
restarts rpcd when FD count exceeds 512 (threshold of 1024 limit).
Prevents LuCI from showing all services as 'stopped' due to FD exhaustion
when rpcd can no longer fork to execute /usr/bin/tachyon commands.
Runs on every watchdog health audit cycle (~15s).
- watchdog: move safe_reload_firewall() definition before ai_heal_nftables()
and ai_heal_qos() to fix 'left-hand side is not a function' error.
ucode does not hoist function declarations.
- updates: show Update button for outdated_same_release status (same version
number but different commit SHA). Previously the button was hidden when
current_version == latest_version even if SHAs differed.