Commit Graph
100 Commits
Author SHA1 Message Date
Dushnilin be5f41a52e feat(ai-stack-2026): add OpenAPI 3.0.3 schema, Telegram /ai_doctor & /fix integration, AI language selector, and log deduplication
- Agent REST API (agent_api.uc): Added GET /openapi.json returning OpenAPI 3.0.3 spec for ChatGPT Custom GPTs, N8N, Dify, and LLM Agents
- AI Doctor (runtime.uc): Added compress_log_snippet() for deduplicating repeated log entries and ai_doctor_lang support (ru/en prompt selection)
- Telegram Bot (telegram.uc): Added /ai_doctor and /fix commands with Inline Keyboards for instant quick-fix execution
- Settings (renderAdvancedSettings.ts, settings.js): Added AI Doctor Language selector (ru/en)
- Documentation: Updated README.md and README.en.md with 2026 AI features
2026-08-11 16:10:31 +03:00
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 4a6b24ef28 docs(agent): update AGENT.md with comprehensive AI Doctor and Agent API configuration guide
Details include:
- AI Doctor setup (enable, provider, model override, api key, custom url)
- Supported LLM providers and custom model options
- 9 Quick Fix repair codes
- Watchdog CLI commands
- HTTP REST Agent API read/write endpoints and auth
- cURL and SSH integration examples
2026-08-11 15:59:38 +03:00
Dushnilin 1e365a1f9b refactor(ai-doctor): replace model dropdown with free-text input
Simpler UX: user types any model name directly.
Placeholder shows the provider default so field can stay empty.
2026-08-11 12:40:23 +03:00
Dushnilin 3d48a22e3c feat(ai-doctor): add model selection per provider
Backend (runtime.uc):
- query_llm() accepts model_override (5th param), falls back to provider default
- ai_doctor() reads ai_doctor_model from UCI and passes to query_llm

Frontend (renderAdvancedSettings.ts):
- aiDoctorModel state field (type / init / load / save)
- Model dropdown per provider with sensible defaults:
  openai: gpt-4o-mini(default)/gpt-4o/gpt-4.1-mini/gpt-4.1/o4-mini
  anthropic: claude-3-5-haiku(default)/claude-3-5-sonnet/claude-3-7-sonnet
  deepseek: deepseek-chat(default)/deepseek-reasoner
  custom: free-text input
- Switching provider resets model to '' (use new provider default)

Legacy LuCI (settings.js):
- ai_doctor_model Value field with placeholder '(provider default)'
2026-08-11 12:37:31 +03:00
Dushnilin af7941f839 fix(tests): replace printf|grep-qx with grep<<<var to avoid broken-pipe false failures
With set -eo pipefail, when grep -qx exits early after finding a match
printf gets SIGPIPE and returns non-zero, which with pipefail triggers the
|| fail branch even though grep returned 0 (found).
Here-string avoids the pipe entirely.
2026-08-11 12:22:28 +03:00
Dushnilin a1b1531ee8 fix(tests): update watch_recovery signature in heal_escalation.sh 2026-08-11 12:16:05 +03:00
Dushnilin 188a1b4fe8 fix(tests): update tests for watchdog refactoring
heal_suppression.sh:
- claim/release_root_cause -> wan_repair_until/singbox_repair_until
- suppressed_by_root_cause(healer, priority) -> (healer)
- driver updated to use new two-var API

heal_outcome.sh:
- watch_recovery(key, recovery_event, incident, reason) -> (key, incident, reason)
- add check that reason: field still present

heal_dns_idempotent.sh:
- replace ai_enabled mock with settings() mock
- fix suppressed_by_root_cause to single-arg signature
- fix ucode // comment (was #)
2026-08-11 11:16:25 +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 c88acdffa5 fix(dns): использовать --doh-url https://1.1.1.1/dns-query в curl при отсутствии c-ares (--dns-servers)
- В сборках OpenWrt без c-ares libcurl не поддерживает --dns-servers,
  что заставляло curl использовать системный DNS (dnsmasq).
- Во время рестарта или перед стартом sing-box dnsmasq был перенаправлен
  на неработающий порт sing-box 5335, из-за чего curl получал Could not
  resolve host (exit 6), создавая замкнутый круг и бесконечный падеж
  сервиса при скачивании подписок/списков.
- Добавлен автоматический фолбэк на DoH (--doh-url https://1.1.1.1/dns-query),
  который работает без c-ares и считывает IP адреса серверов подписок/списков
  напрямую в обход падающего системного DNS.
2026-08-10 21:42:49 +03:00
Dushnilin 8be248f9fd fix(ui): непрерывная индикация анимации на кнопке обновления без мигания
- При получении 'Another component action is already running' или сетевого
  RPC-сбоя спиннер на нажатой кнопке больше не сбрасывается сразу.
- Добавлено повторное связывание (fallback lookups) с 800ms задержкой, чтобы
  бэкенд успевал прописать запущенный job в state до снятия индикатора.
- При отслеживании запущенного задания теперь учитываются любые фоновые
  акшны для того же компонента.
2026-08-10 21:38:46 +03:00
Dushnilin dd3546f29a fix(updates): предотвратить ложный сброс индикации загрузки при переустановке одинаковых версий
- В waitComponentActionJob добавлен минимальный порог ожидания (10с)
  перед срабатыванием fallback проверки версии Tachyon. При same-release
  обновлениях (одинаковый номер версии, но новый SHA) совпадение версии
  больше не приводит к мгновенному (за 1с) снятию спиннера loading, пока
  бэкенд на самом деле только скачивает и ставит пакет.
- Расширен список транзиентных RPC ошибок (isTransientRpcError) для
  надежного перешивания перезапуска uhttpd/rpcd/ubus во время переустановки.
- Увеличен COMPONENT_ACTION_TRANSIENT_RPC_GRACE_MS до 60с.
2026-08-10 21:30:03 +03:00
Dushnilin 5cbbd0f661 fix(ui): suppress 'Already running' and DNS retry toasts
Lock contention ('Another component action is already running') is an
expected transient state — user clicked a button while a previous action
was still in flight. Showing [error] toast confuses users into thinking
something broke. Filter it from the log watcher notification pipeline.

Also suppress DNS retry debug lines (issue #24 retry logs) from showing
as error toasts.

Added tests for both suppressed patterns.
2026-08-10 21:01:21 +03:00
Dushnilin 7679715fcc fix: curl --dns-servers compatibility + DNS retry on sing-box restart
- Guard --dns-servers behind runtime capability check (requires libcurl
  with c-ares). OpenWrt builds without c-ares now skip the flag silently
  instead of crashing curl with 'option not supported'.
  Affects: subscription/cache.uc, components/updates.uc

- On DNS resolution failure (curl exit 6), retry with increasing backoff
  (5s, 10s, 15s) before giving up, instead of immediately breaking.
  Fixes transient 'Could not resolve host' during sing-box restart when
  Tachyon's intercepting DNS resolver is briefly unavailable (issue #24).

- Fix stale component-action lock deadlock: pid_running now validates
  /proc/<pid>/cmdline against ucode/tachyon/sh pattern + 300s TTL.
  Background check_update exits cleanly on lock contention instead of
  emitting [error] toasts (action.uc, state.uc, initd.uc).
2026-08-10 20:48:52 +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 31c36118ef refactor(ui): сгруппировать все параметры Settings по вкладкам (cbi-tabgroup)
- Настройки разделены на 6 категорий: DNS, Сеть, Сервисы, Обновления, ИИ и Системные
- Устранена длинная простыня прокрутки — каждая категория открывается во вкладке
2026-08-10 19:30:51 +03:00
Dushnilin c930c15dc3 feat(ai): добавить поддержку Anthropic Claude API в AI Doctor
- В query_llm (runtime.uc) добавлена интеграция с Anthropic Messages API (https://api.anthropic.com/v1/messages) с заголовком x-api-key
- В настройки Settings и Dashboard добавлен выпадающий выбор провайдера 'Anthropic (Claude API)'
- Добавлен русский перевод для опций AI Doctor
2026-08-10 19:16:19 +03:00
Dushnilin a15490c67d i18n: добавить русский перевод для AI Agent REST API в PO файлы 2026-08-10 19:14:41 +03:00
Dushnilin 8a71a5a1a4 feat(luci): добавить поле AI Agent REST API Token в стандартную вкладку Settings
- Добавлена опция agent_api_token (AI Agent REST API Bearer Token) в CBI-форму настроек settings.js
- Сохраняет токен через стандартную кнопку Save & Apply в LuCI
2026-08-10 19:13:51 +03:00
Dushnilin 1171cee9fd feat(ui): добавить управление токеном AI Agent REST API в LuCI
- В Advanced Settings (Dashboard) добавлен новый блок 'AI Agent REST API (12 Endpoints)'
- Позволяет просматривать, изменять и генерировать Bearer Auth Token (agent_api_token) для REST API из веб-интерфейса
- Сохраняет токен в UCI (tachyon.settings.agent_api_token)
2026-08-10 19:11:48 +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 b11780dec3 fix: ждать TCP-готовности proxy перед загрузкой отложенных подписок
run_deferred_subscription_bootstrap теперь после проверки что процесс
sing-box запущен дополнительно проверяет что proxy-порт реально принимает
соединения (до 10x3s).

Без этого: если порт не готов в момент вызова — fallback на прямой
download, что провалится для URL доступных только через proxy.
При неудаче через 30s запускается retry worker (поведение сохранено).

Также: get_subscription_download_proxy_address делает одиночную проверку
порта — retry worker является safety net для edge cases.
2026-08-10 18:27:38 +03:00
Dushnilin f5929d24f8 fix: дождаться готовности proxy перед загрузкой подписок и списков
При обновлении sing-box Tachyon перезапускается, запускает sing-box
и сразу пытался загрузить списки/подписки через proxy 127.0.0.1:4534,
но sing-box ещё не принял соединения на этом порту.

Исправления:
- updates.uc: dns_probe_passed теперь проверяет TCP-доступность proxy
  вместо немедленного return true; ждёт до 30s (10 попыток по 3s)
- updates.uc: download_to_file при неудаче через proxy делает fallback
  на прямой download с bootstrap DNS 8.8.8.8/1.1.1.1
- cache.uc: get_subscription_download_proxy_address проверяет что proxy-порт
  реально принимает соединения, иначе переходит на прямой download
- cache.uc: download_subscription использует bootstrap DNS при прямом
  скачивании подписок (когда proxy недоступен или не настроен)
2026-08-10 18:10:36 +03:00
Dushnilin 6379361d40 chore: поднять версию до 1.2.67 2026-08-10 14:17:33 +03:00
Dushnilin 3b1eb89124 fix: graceful fallback for unknown inbound fields in sing-box config
Older sing-box versions fail on 'close_timeout' in MTProto inbound
(added in Tachyon to fix SIGHUP hot-reload crash, issue #15).

Instead of hard-failing, runtime.uc now detects 'json: unknown field'
errors from sing-box check, strips the offending inbound field from
all inbounds in the generated JSON, and retries the check once.

This makes Tachyon forward-compatible: new fields for newer sing-box
versions degrade gracefully on older installs without requiring manual
version detection or config changes.
2026-08-10 14:11:06 +03:00
Dushnilin 7def6e556f i18n: fix Russian-source translation keys in AmneziaWG .conf import
section.js had 10 _() calls with Russian strings as keys instead of English.
This is a i18n antipattern: source keys must be language-neutral (English).

Changes:
- section.js: replace 10 Russian msgids with English equivalents
  - 'Загрузить конфиг .conf' -> 'Load .conf config'
  - 'Импортировать настройки AmneziaWG из файла .conf' -> 'Import AmneziaWG settings from .conf file'
  - 'Загрузить .conf' -> 'Load .conf'
  - 'Ошибка' (3x) -> 'Error' (reuses existing key)
  - 'Выберите файл с расширением .conf' -> 'Select a file with .conf extension'
  - 'Не удалось разобрать файл:' -> 'Failed to parse file:'
  - 'Загружено!' -> 'Loaded!'
  - 'Готово' -> 'Done'
  - 'Конфиг AmneziaWG успешно загружен!' -> 'AmneziaWG config loaded successfully!'
  - 'Ошибка чтения' -> 'Read error'

- tachyon.ru.po: add RU translations for 9 new English keys
- calls.json / tachyon.pot / tachyon.po: regenerated via locales:actualize

Result: 1108/1108 translated (no orphaned Russian-source entries)
2026-08-10 14:00:35 +03:00
Dushnilin 92f1b4e991 i18n: complete RU translations — 1109/1109 (was 1030/1109)
Translated 79 missing strings:
- 15 country names (Belarus, China, Finland, France, Germany, Japan,
  Kazakhstan, Netherlands, Russia, Singapore, Sweden, Turkey,
  Ukraine, United Kingdom, United States)
- TUIC URL validation errors (14 strings)
- UI general: Cancel, Conflict, Error, Fixed, Issues, Success, etc.
- Protocol abbreviations kept as-is: DNS, HTTP, MTU, RDP, SSH, TLS
- Doctor/watchdog: 'Doctor failed', 'Doctor repair', 'No issues found'
- WARP: registration detour, proxy, skip TLS cert, country code
- Hosts file descriptions, URL validation message
- Monitoring & Analytics, Optimization, Protection sections
- Checking… and Generating… (Unicode ellipsis U+2026 variants)
- Russian-source keys: 'Ошибка', 'Не удалось разобрать файл:'

Distributed to luci-app-tachyon/po/ via npm run locales:actualize
2026-08-10 13:53:23 +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 2562420bce Delete CLAUDE.md 2026-08-10 13:12:46 +03:00
Dushnilin df426f9d1e chore: ignore CLAUDE.md 2026-08-10 12:53:50 +03:00
Dushnilin f807b20afb fix: lower min OpenWrt version to 23.05 (matches e2e test image) 2026-08-10 12:38:48 +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 6cf90e3e84 fix(updates): не выдавать успешное обновление за ошибку "Failed to execute"
Воркер компонентного действия перенаправлял stderr в тот же файл, что и
stdout (2>&1). Действие печатает результат в stdout одним JSON-объектом,
а stderr — предупреждения apk/opkg, вывод init-скрипта при рестарте —
попадал в тот же файл после JSON. Разбор всего файла падал, построчный
фолбэк не находил объекта, и finish_component_job писал синтетический
success: false с последней строкой лога в качестве сообщения.

Особенно надёжно это ломало самообновление Tachyon: install_tachyon
рестартует сервис, будучи предком воркера, поэтому вывод рестарта
гарантированно ложился поверх канала результата.

Теперь stderr пишется в отдельный <job>.out.stderr, он же используется
как источник сообщения, когда воркер действительно умер, не напечатав
результат. Файл убирается вместе с заданием и подметается orphan-очисткой.

Заодно валидация sing-box перестала судить о бинарнике по коду возврата:
popen().close() отдаёт сырой wait-статус, а command_output отбрасывал
stdout при любом ненулевом статусе. `sing-box version` печатает версию и
может завершиться ненулевым кодом или получить сигнал от остановки
сервиса перед установкой — валидация получала "", объявляла рабочий
пакет битым и откатывала установку с exit_code 129 (128+SIGHUP). Добавлен
command_output_lenient, он же чинит показ версии в дашборде.
2026-08-10 05:21:19 +03:00
Dushnilin dce098ff7c feat(doctor): проверка наличия скачанных community lists в секциях
Добавлена проверка 15b в run_doctor_checks():
- Итерирует все включённые секции через uci_core.section_objects()
- Для каждого community_list проверяет существование .srs-файла
  в TMP_RULESET_FOLDER (/tmp/sing-box/rulesets/)
- При наличии пропущенных файлов запускает tachyon list_update и
  перепроверяет результат (FIXED / остались отсутствующие)
- Правильно использует section_objects() вместо get_all() который
  всегда возвращает null при вызове без section_name
2026-08-09 22:55:42 +03:00
Dushnilin 940bf6554c fix(i18n): синхронизация упакованного русского каталога с source-локалью 2026-08-09 22:37:36 +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
Dushnilin 26d67506a4 fix(ui): report the live sing-box version for plain extended installs
The dashboard showed a sing-box version that never changed. build_system_info()
preferred /etc/tachyon/sing-box-version for every "extended" variant, and that
state file is written by the component action alone — a sing-box installed via
install.sh, opkg or by hand leaves it pinned at whatever was recorded last. The
UI then reported the stale version indefinitely, and the update badge compared
the release against it rather than against the binary actually on disk.

The state file only exists for variants whose binary cannot be executed to print
a version: extended-compressed is a self-extracting stub, and lx needs a runtime
that may be absent. Plain extended answers `sing-box version` fine — singbox
/runtime.uc:259 and subscription/cache.uc:1482 both gate on the compressed
variants only. Covering plain extended was a regression from 46ef97fe.

Both call sites now share sing_box_resolved_version(), which also falls through
to the live probe when the state file is missing or empty instead of reporting
"unknown".

Verified on a router: with the state file still holding 1.13.16-extended-2.6.2,
system-info now reports the installed 1.13.18-extended-2.6.3.
2026-08-09 21:56:44 +03:00
DushnilinandClaude Opus 5 84924a0fff fix(build): bound the prerm hooks build.sh writes into the packages
build.sh assembles the APK and IPK packages itself rather than going through
OpenWrt's packaging, so it carries its own copies of the prerm-family hooks.
Bounding Package/tachyon/prerm in tachyon/Makefile therefore never reached the
released artifacts: 1.2.66 shipped with

    exit(system("/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1"));

which is both unbounded and status-propagating. `exit(system(...))` returns a raw
wait status, so a cleanup that was merely killed took the upgrade down with it —
on the router that surfaced as

    ERROR: lib/apk/exec/tachyon-1.2.66.pre-upgrade: exited with error 247

247 being 128+119, the cleanup SIGKILLed by its own sweep.

All three hooks written here (APK pre-upgrade, APK pre-deinstall, backend IPK
prerm) now probe the BusyBox timeout spelling against /bin/true, bound the
cleanup at 90s, and exit 0 unconditionally — matching the Makefile.

tests/package_lifecycle.sh gains contracts over all three: each must be bounded,
must not propagate the cleanup's status, and must compile. They live inside
heredocs, so ucode_syntax_lint.sh never sees them and a syntax error would only
surface on a router mid-upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:13:33 +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 547f42e92d fix(install): fail when a rolled-back upgrade leaves an unstamped build
installed_backend_sha() rejects the unsubstituted __COMPILED_COMMIT_SHA__
placeholder and the literal "unknown", so a stale build on disk reports no SHA at
all. install_backend_package treated that empty value as "fresh install" and fell
through to the presence check, which passes trivially after an apk rollback — the
package and its version are still there. The run printed

  WARN Package manager reported errors during installation, but tachyon is
       installed — continuing

directly after the backend install had been rolled back, and then continued
through the remaining steps to a green banner over a failed upgrade.

Record whether tachyon was installed before the install and treat "was present,
still reports no SHA afterwards" as the failure it is. A genuine first install has
no package present beforehand and is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 20:25:32 +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 2fc57d383d fix(installer): free the rc.common lock before the package manager runs
Bounding the hooks fixes future upgrades, but the hook that runs during
THIS one belongs to the build already installed, and every release up to
1.2.66 shipped a prerm with no timeout. It blocks on rc.common's
flock -w 1000, which procd keeps re-populating with retry_start_on_wan_up
waiters between pre- and post-upgrade, so apk's pre-upgrade outlives its
patience and the transaction is rolled back.

cleanup_legacy already stops and disables the service; it now also kills
the flock waiters, so the old unbounded hook has nothing left to wait on.
2026-08-09 18:35:42 +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 ba69e3d240 fix(i18n): синхронизация упакованного русского каталога
Переводы для "Rebuilt release" и "Installed build → Available build"
были добавлены в исходный каталог после locales:distribute, поэтому
копия в luci-app-tachyon/po/ru разошлась и тест luci_localization
падал на cmp исходного и упакованного каталогов.
2026-08-09 17:09:33 +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 8f3abe5e7b fix(fe): автозавершение ожидания для действия reinstall при переустановке 2026-08-09 15:40:54 +03:00
Dushnilin f974e4a0b7 feat: автоопределение обновлений билда в рамках одного релиза 2026-08-09 15:31:42 +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 0b7494a8e9 fix: Makefile postupgrade BusyBox awk alternation
Same BusyBox awk bug as in service/package.uc: | alternation and \/
inside awk regex literals are not supported. Split into grep -E for
alternation and grep -F for the path with slashes.
2026-08-09 12:36:57 +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 9b3066c846 fix(install): retry APK commands on database lock (exit 227) in install.sh 2026-08-09 09:43:37 +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 f351164ebc fix(updates): retry apk add up to 10 times on database lock error (exit 227) before failing 2026-08-09 09:22:28 +03:00
Dushnilin 0e16860675 fix(updates): install all tachyon packages in a single apk call to avoid APK database lock (exit 227) 2026-08-09 09:10:05 +03:00
Dushnilin 2896daea51 fix(ci): update release step configuration in build.yml 2026-08-09 09:02:03 +03:00
Dushnilin d91eba3fa8 fix(ci): remove explicit target_commitish on tag push release step in build.yml 2026-08-09 09:00:12 +03:00
Dushnilin 3eeee56f2e fix(ci): separate release action steps for push vs workflow_dispatch in build.yml 2026-08-09 08:59:18 +03:00
Dushnilin 54f7b5b351 fix(ci): fix release step execution condition for tag push events in build.yml 2026-08-09 08:58:36 +03:00
Dushnilin 071f72fe43 fix(ci): replace non-existent actions/checkout@v5.0.0 with v4 in build.yml 2026-08-09 08:57:53 +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 c850633b01 feat(updates): compare commit SHA for same-release build updates 2026-08-09 08:47:11 +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 48e2492c0c fix(updates): clean stale lock directories with rm -rf to unblock update checks 2026-08-09 04:38:08 +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 b9317a553b fix(updates): validate ruleset file size and prevent 0-byte corruptions 2026-08-08 20:21:16 +03:00
Dushnilin d6860d8a15 fix(fe): исправить типизацию fetchHostnames и обновить бандл LuCI 2026-08-08 13:00:17 +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 810f6dbfc8 fix: убрать rpcd из shell uci-defaults (нарушал тест shell_inventory) 2026-08-07 20:51:38 +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 43853f5e3f fix(installer): make package removal warnings non-fatal during upgrade 2026-08-07 18:13:00 +03:00
Dushnilin 1ce9c4a3ae fix(build): replace JS-style method call with ucode global split() in apk postinst/post-upgrade scripts 2026-08-07 18:04:04 +03:00
Dushnilin 138a3831b9 fix(tests): restore exact dashboardFilterChoiceRefreshers registration signature for tests 2026-08-07 17:58:24 +03:00
Dushnilin f5fe6a7471 fix(updates): eliminate false same-release update warnings when current version matches latest release 2026-08-07 17:54:29 +03:00
Dushnilin 7c0959a969 fix(ui): dynamic refresh of outbound choices when focusing or loading section cache 2026-08-07 17:34:42 +03:00
Dushnilin ff92e79c5b fix(ui): resolve empty server list in subscription and urltest filter dropdowns 2026-08-07 17:30:50 +03:00
Dushnilin 37c56a3652 fix(updates): validate hex format for remote_sha in same-release update check 2026-08-07 17:07:14 +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 59e0dc4194 fix(subscription): add bootstrap DNS 8.8.8.8,1.1.1.1 to curl for downloads without proxy 2026-08-07 14:15:28 +03:00
Dushnilin 37569768d8 fix(tracer): fix rpcd ACL permissions for ruleset files and add source URL display for external .lst lists 2026-08-07 13:35:01 +03:00
Dushnilin 1599a7239c fix(tracer): implement resilient 3-way ruleset reading for domain/IP lists in routing tracer 2026-08-07 13:21:49 +03:00