Commit Graph
148 Commits
Author SHA1 Message Date
Dushnilin 6dd085b777 feat(ai-doctor): supercharge AI Doctor with multi-fix, log context, history, and 13 quick fix codes
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
2026-08-11 16:06:19 +03:00
Dushnilin 1bf1af3783 fix: is_dns_working() -> controller.is_dns_working() in ai_heal_dns_loop 2026-08-11 11:11:05 +03:00
Dushnilin b194666b84 ponytail: chistka watchdog + event_controller
event_controller.uc:
- check_tachyon_cli_running: zamenen polnyy /proc-skan na pgrep -f
- tproxy_port(): dobavlen kesh cached_tproxy_port

watchdog.uc:
- Udaleny 7 delegatov-oborotok
- active_root_causes zamenen dvumya var wan_repair_until/singbox_repair_until
- watch_recovery: ubrano mertvoe pole event
- ai_setting/ai_enabled udaleny, UCI chitaetsya napryamuyu
- safe_call: ubran flag ai_graceful_degradation_enabled

Testy: 511/511
2026-08-11 11:00:36 +03:00
Dushnilin 7ad428ddc8 refactor(watchdog): оптимизация и устранение дублирования кода
- 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)
2026-08-11 08:47:47 +03:00
Dushnilin 9b110b226d fix(core): исправить ложное застревание блокировок компонентных действий и отключить всплывающие ошибки при фоновой проверке
- Проверять /proc/<pid>/cmdline при валидации PID (исключает ложное определение dnsmasq/sing-box как владельца блокировки при рециркуляции PID в Linux)
- Добавить автоматическую очистку зависших блокировок старше 300 секунд (TTL)
- При фоновой автоматической проверке обновлений (action == check_update) пропускать занятые блокировки без записи [error] в syslog и появления всплывающих нотификаций
2026-08-10 19:43:30 +03:00
Dushnilin e67e8c0984 fix(package): исключить воркеры обновления из зачистки процессов при апгрейде
- kill_matching_command и цикл зачистки fd/1000 прокси в package.uc теперь пропускют процессы ucode (action.uc / updates.uc) и их предков, предотвращая выбивание воркера обновлений по SIGKILL во время prerm
- isTransientRpcError теперь учитывает 'failed to execute' в качестве транзиентной ошибки RPC при перезапуске rpcd / служб во время обновления
2026-08-10 18:57:19 +03:00
Dushnilin 23f3c8c587 feat: AI Agent API — HTTP REST + CLI for external LLM agents
Adds a self-healing infrastructure accessible via HTTP REST and CLI:

## New files
- tachyon/files/usr/lib/service/agent_api.uc
  Full REST API handler for /tachyon/agent/v1/* endpoints
  READ (no auth): /health /snapshot /diagnose /logs /config /tools
  WRITE (Bearer): /heal /restart /reload /config/set /section/toggle /domain/add

- tachyon/files/usr/lib/cgi-bin/tachyon-agent
  CGI shell wrapper — installed into /www/cgi-bin by postinst
  Exposes agent API via uhttpd at http://<router>/cgi-bin/tachyon-agent/<endpoint>

- AGENT.md
  Full documentation: curl examples, SSH usage, Claude/Open-WebUI integration

## Modified files
- tachyon/files/usr/bin/tachyon
  + diagnose_json command -> diagnostics/runtime.uc diagnose-json
  + agent_cgi command -> service/agent_api.uc __cgi
  + agent subcommand with special arg-passing (path method body bearer)
  + show_help() updated

- tachyon/files/usr/lib/diagnostics/runtime.uc
  + diagnose_json() — structured JSON output of doctor checks for LLM agents
  + Registered in CLI dispatch as 'diagnose-json'
  Fix: UTF-8 byte offset for arrow character U+2192 (+3 not +2)

- tachyon/Makefile
  + INSTALL_BIN for usr/lib/cgi-bin/tachyon-agent
  + postinst: symlink CGI into /www/cgi-bin on install
2026-08-10 13:14:30 +03:00
Dushnilin e7ed3839ed fix(package): не удалять состояние выполняющегося задания в postinst
Самообновление Tachyon — это компонентное задание, которое устанавливает
пакет, чей postinst и вызывает remove_component_update_cache(). Очистка
стирала каталог component-actions целиком, включая файл состояния того
самого задания, которое ещё выполнялось. LuCI опрашивал задание, не
находил его, не получал ни message, ни stderr и показывал фолбэк
"Failed to execute" поверх успешно прошедшего обновления.

Теперь очистка пропускает задания, помеченные running. Устаревшие
результаты и кэш проверок обновлений убираются как раньше. Зависшее
задание не залипнет: детект по PID и started_at помечает такие stale.

Пути каталогов состояния стали переопределяемыми через окружение, чтобы
тест мог прогнать luci-postinst, не трогая /var/run.
2026-08-10 05:47:42 +03:00
Dushnilin 98d53326d9 fix(bot): добавить недостающий import constants в service/api.uc
Коммит 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().
2026-08-10 05:23:41 +03:00
Dushnilin 90980917e4 feat(telegram): выбор секции прокси для Telegram-бота + фикс краша при null cb.from
- 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
2026-08-09 22:34:06 +03:00
DushnilinandClaude Opus 5 4ce5d7ebac fix: prerm kill sweep must not kill its own ancestors
`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>
2026-08-09 20:50:08 +03:00
DushnilinandClaude Opus 5 00a5fec0c0 fix: close procd lock descriptor 1000 in background spawns
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>
2026-08-09 20:19:23 +03:00
Dushnilin f770316345 fix: bound package hooks so apk cannot roll the upgrade back
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.
2026-08-09 18:23:16 +03:00
Dushnilin af2d07b056 fix(updates): установка пересобранных релизов в рамках одного тега
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 недоступен.
2026-08-09 16:57:10 +03:00
Dushnilin 4c8ee871f1 fix(test): замена BIN_PATH на INIT_PATH в postinst_restore для прохождения теста package_lifecycle 2026-08-09 15:48:04 +03:00
Dushnilin a331d1d1f8 fix(service): устранение дедлока flock при переустановке и обновлении пакета 2026-08-09 15:46:09 +03:00
Dushnilin bf08e0720b fix: check INIT_PATH status before recording package upgrade state
- remember_upgrade_state: call INIT_PATH status to verify service status
  before writing upgrade state file, ensuring stopped services are not marked
2026-08-09 12:44:14 +03:00
Dushnilin 7db8109d5e fix: postinst must restart via INIT_PATH, not BIN_PATH
- 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
2026-08-09 12:41:40 +03:00
Dushnilin 676ee2041b fix: prerm stop-before-rt_tables, BusyBox awk, js-yaml vuln
- 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
2026-08-09 12:34:58 +03:00
Dushnilin 95cfe7914f fix(postinst): use BIN_PATH start in postinst_restore + Makefile postupgrade calls tachyon start directly (bypasses rc.common flock) 2026-08-09 12:03:31 +03:00
Dushnilin 0f43680b66 fix(pkg): remove init.d/tachyon status from remember_upgrade_state (root cause of hang); add postupgrade Makefile section; kill hotplug spawners in prerm 2026-08-09 11:51:12 +03:00
Dushnilin e47704ff14 fix(prerm/postinst): use BIN_PATH stop, clear flock in postinst, always return true; fix(install): verify package VERSION not just existence on APK error 2026-08-09 11:44:33 +03:00
Dushnilin 3a0f31bfb5 fix(prerm): kill init.d/tachyon background processes (root cause of flock storms) before stop 2026-08-09 10:07:46 +03:00
Dushnilin c0d0850b1b fix(prerm): kill flock twice + use ubus to stop service, avoiding init.d rc.common flock creation 2026-08-09 10:06:57 +03:00
Dushnilin 5b118c2abb fix(prerm): always return true from prerm_cleanup so APK never rolls back upgrade on cleanup failure 2026-08-09 10:04:27 +03:00
Dushnilin 929c936154 fix(prerm): kill flock 1000 waiters before init.d stop to prevent procd_tachyon.lock deadlock on upgrade 2026-08-09 09:55:30 +03:00
Dushnilin 2a43ea2b6b fix(prerm): use timeout 15 for init.d/tachyon stop in prerm_cleanup to prevent deadlock on procd_tachyon.lock 2026-08-09 09:36:11 +03:00
Dushnilin 3d84d96e49 feat(ui): display short commit SHA alongside release version across LuCI UI 2026-08-09 08:50:38 +03:00
Dushnilin 6f98bae53d fix(updates): clear update cache in postinst and update version in post-install check 2026-08-09 04:57:37 +03:00
Dushnilin 106ce9352a fix(memory): ограничить чтения неограниченного вывода в память
На роутере со 128 МБ несколько мест читали в память вывод, размер которого
ничем не ограничен. Распаковка gzip подписки шла через пайп с чтением целиком:
скачивание curl ограничено 15 МБ, но коэффициент сжатия base64-текста высокий,
так что пик доходил до десятков мегабайт. Оба вызывающих места передают файл и
ждут файл, поэтому распакованный поток теперь уходит редиректом сразу в
выходной файл, а успех определяется по коду возврата и ненулевому размеру.

Таблица nftables содержит сеты с десятками тысяч элементов из списков, и
`nft list table` печатает их все. Проба контроллера делала это каждый тик,
хотя ищет только имена цепочек и правил; четыре проверки доктора и вывод
чужих меток из полного рулесета — то же самое. Везде добавлен grep на
источнике, отбирающий только те строки, которые код потом и разбирает.

Два просмотра логов читали кольцевой буфер syslog целиком, чтобы показать
несколько сотен строк; теперь количество строк ограничено на стороне logread.
2026-08-08 23:02:18 +03:00
Dushnilin e6e3853f05 fix(errors): перестать глотать сбои записи состояния в пустых catch
Пустых 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 для
лока и обновлён под новый вызов.
2026-08-08 22:32:09 +03:00
Dushnilin feec8c6d35 fix(spawn): закрывать дескрипторы фоновых процессов вместо фиктивного 1000<&-
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.
2026-08-08 21:51:51 +03:00
Dushnilin ae64be7625 fix(watchdog): прекратить перезагрузку dnsmasq ради уже выставленного значения
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 в каждом случае.
2026-08-08 12:55:54 +03:00
Dushnilin 5a10cc158b feat(watchdog): приостановка вторичных ремонтов во время устранения первопричины
Падение 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` проверяет ранги, точки захвата/освобождения, защиту восстановителей и логирование пропусков.
2026-08-08 12:47:23 +03:00
Dushnilin b4ca111d34 feat(watchdog): перезапуск только sing-box перед эскалацией до полного стека
При падении процесса sing-box или отсутствии ответа вочдог выполнял полный перезапуск стека, включая правила файрвола и прокси обхода. Это вызывало избыточный простой сети при локальных сбоях sing-box.

Теперь вочдог сначала пробует точечный перезапуск службы sing-box. Эскалация до полного перезапуска стека происходит только если sing-box не восстановился.
2026-08-08 12:26:43 +03:00
Dushnilin ad8087ba76 feat(watchdog): логирование фактических результатов ремонта вместо намерений
Ранее вочдог логировал попытки ремонта как успешные ещё до их выполнения. Если переключение сервиса или перезапуск завершались ошибкой, в логе всё равно отображался успех.

Теперь восстановители проверяют результат действий и логируют фактический исход (успех или ошибка) вместе с подробной контекстной информацией.
2026-08-08 11:57:56 +03:00
Dushnilin 2647061646 refactor(watchdog): разделение наблюдения и восстановления через шину событий
Ранее вочдог опрашивал состояние системы и устранял проблемы в едином смешанном цикле. Добавление новой проверки или действия по восстановлению требовало изменения монолитной функции, где мониторинг и ремонт были переплетены, что затрудняло аудит порядка выполнения и изолированное тестирование.

Вводится `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` добавляет интеграционные тесты для отправки событий, сортировки приоритетов и обработки подписчиков.
2026-08-08 11:01:09 +03:00
Dushnilin 55e0f9b303 fix: исправить список серверов в URLTest, ACL для rpcd, Telegram-бот
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 уже полный год
2026-08-07 20:45:06 +03:00
Dushnilin c8052ee682 fix(watchdog): eliminate aggressive DNS restarts breaking active TCP/RDP connections 2026-08-07 18:22:19 +03:00
Dushnilin b7fa83dfd2 fix: resolve export_metrics crash and MTProto SIGHUP FATAL (issues #14, #15)
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
2026-08-07 16:20:56 +03:00
Dushnilin f2a22a6730 fix(tracer): fix fs.readfile call for domain/IP lists and implement sing-box SIGHUP graceful recovery 2026-08-07 13:14:06 +03:00
Dushnilin 4e419d5aea fix: OpenWrt 25.12 compatibility fixes, startup speedup, and Telegram polling fix
- 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
2026-08-07 11:46:52 +03:00
Dushnilin 33387ace3d fix(watchdog): use real domains (google.com/example.com) for DNS health check to fix Telegram notification spam 2026-08-07 08:11:53 +03:00
Dushnilin 35f1c76a01 fix(dns): multi-fallback sing-box PID detection (ubus/procd/pidof/pidfile) resolving 'dhcp contains dns server' error on start 2026-08-06 19:53:49 +03:00
Dushnilin 7125e1a48e feat: карточки сервисов на дашборде + секция Hosts в правилах + скрытие вкладки Conditions для Hosts
Дашборд:
- Добавлены карточки статуса Zapret, Zapret2, ByeDpi на панель мониторинга
- Показывают статус (работает/остановлен), количество перезапусков, предупреждения
- Баг: API возвращает boolean а не число — исправлено сравнение

Секция Hosts:
- Перенесены опции hosts из глобальных настроек в отдельный тип секции (action=hosts)
- Опции: пользовательские DNS записи, удалённые списки хостов, автообновление, интервал
- Миграция: автоматический перенос глобальных настроек hosts в секцию
- Вкладка «Условия» скрыта для секций типа Hosts (hosts записи применяются глобально)
- Возвращена кнопка «Import Hosts File» с модальным окном загрузки из файла
- Убраны неиспользуемые опции из settings.js (~280 строк)

Локализация:
- Переведены 6 новых строк для превью импорта hosts
2026-08-06 14:16:18 +03:00
Dushnilin 488bbfefd5 fix: комплексный аудит и исправление ucode кодовой базы
Критические исправления:
- 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 перед вызовом
2026-08-06 00:03:51 +03:00
Dushnilin a32ad76285 telegram: fix regex injection, state race, and oversized callback_data (BUG7/8, BUG9, BUG13)
- 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.
2026-08-05 11:45:01 +03:00
Dushnilin bddd0a972b fix: 10 bugs in Telegram bot
- 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
2026-08-05 11:38:14 +03:00
Dushnilin 4d4bdb16d0 fix: watchdog rpcd FD leak heal + updates show outdated_same_release
- 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.
2026-08-04 22:57:39 +03:00
Dushnilin 9815198585 fix: исправлена команда /check_updates в Telegram боте
- exec_check_updates в telegram.uc: исправлен парсинг JSON ответа component_update_check_cache
  (формат { enabled, results: [] } вместо словаря по имени компонента)
- исправлено поле installed_version -> current_version
- добавлена обработка случая disabled/пустой кэш/ошибка success
- добавлено отображение zapret2 и других компонентов
2026-08-04 21:03:24 +03:00