# 579 — Псевдо-направление NETWORKS: узлы вне списков выбора | Поле | Значение | |------|----------| | Статус | Implemented (проверка на устройстве: DEVICE-PENDING) | | Дата старта | 2026-09-27 | | Дата завершения | — | | Коммиты | 298b3000 | | Контракт | Не затрагивается: конфиг не меняется | | Связанные spec'ы | [§578](578-tailscale-preset-template-for-each.md) (пресет Tailscale), [§435](435-node-sections-tailscale.md) (узел без выхода не идёт в пул Направлений), [features/392](../tasks/392F-node-diagnostics/spec.md) | ## Проблема Узел, который не попал ни в один список выбора, на главном экране не виден. Главный пример: Tailscale без `exit_node`. Пользователь завёл узел, включил VPN и не видит ни узла, ни его состояния, ни пути к диагностике. ## Решение владельца (27.09.2026) 1. На главном экране появляется псевдо-направление NETWORKS. 2. В него входят только узлы Tailscale без `exit_node`. 3. В строке узла на месте задержки показывается состояние узла. Отвергнуто владельцем: отдельный блок Networks над списком узлов; широкое условие «любой endpoint вне списков выбора». ## Условия ### Какие узлы входят в NETWORKS Узел входит, когда выполнены все условия: 1. запись узла есть в разделе `endpoints[]` собранного конфига; 2. тип узла `tailscale`; 3. в теле узла не задан `exit_node` (реестр не считает узел выходом: `exit_capable_when` ложно). Такой узел не входит ни в одну группу `selector` и `urltest`: это следует из условия 3 и правила сборки. Не входят: endpoint WireGuard и AWG, скрытые из списков как звенья цепочки; Tailscale с заданным `exit_node` (он виден в обычных списках). ### Когда NETWORKS показывается 1. в конфиге есть хотя бы один узел по условиям выше; 2. VPN включён (решение владельца 27.09.2026). Иначе псевдо-направления нет, экран прежний. Если настоящих направлений нет, NETWORKS показывается один (решение владельца 27.09.2026). При выбранном NETWORKS заголовок списка показывает число узлов NETWORKS; кнопок сортировки и фильтров нет. ## Решение ### 1. Что такое псевдо-направление | Свойство | Значение | |---|---| | название | `NETWORKS`, не переводится | | место | последним в перечне направлений главного экрана | | в конфиге | отсутствует: группы с таким тегом в конфиге нет | | в хранилище | отсутствует: направлением пользователя не является | | в списках выбора цели правила, цели DNS, звена цепочки | отсутствует | | переименование, удаление, настройка | недоступны | Состав считается из собранного конфига, по которому работает ядро. При выключенном VPN состав считается из последнего собранного конфига. Тег `NETWORKS` у настоящего направления пользователя: псевдо-направление называется так же, конфликта нет, потому что в конфиг оно не пишется. Исполнитель проверяет, что перечень направлений экрана различает записи не по названию. ### 2. Список узлов в NETWORKS | Элемент строки | Значение | |---|---| | название | тег узла | | протокол | как у обычного узла | | отметка активного узла | нет | | нажатие | открывает экран узла (тот же, что пункт «Подробности») | | выбор узла | недоступен: нажатие не переключает выход | | место задержки | состояние узла, см. раздел 3; замера задержки нет | | меню строки | пункты обычного узла, кроме выбора и замера задержки | Общий замер задержки («проверить все») в NETWORKS недоступен. ### 3. Состояние узла Источник: поток ядра `SubscribeTailscaleStatus` через `CommandClient`, поле `BackendState` записи `TailscaleEndpointStatus` с тегом узла. API апстримное, дельты ядра нет. | Условие | Что показывается в строке | |---|---| | VPN выключен | состояния нет | | VPN включён, записи об узле от ядра ещё нет | `starting` | | `BackendState` = `Running` | `running` | | `BackendState` = `NeedsLogin` | `sign-in needed` | | `BackendState` = `Stopped` | `stopped` | | прочее значение | текст `StateText` ядра как есть | Цвет: `running` как у рабочего узла, `sign-in needed` и `stopped` как у предупреждения. Подписка на поток живёт, пока VPN включён и в конфиге есть узел по условиям NETWORKS. Правила работы с биндингом: память проекта и `docs/KERNEL.md` (наносекунды, один приёмник, без исключений наружу). ### 4. Порядок работы исполнителя 1. Опись: откуда главный экран берёт перечень направлений и состав узлов, где строится строка узла и её меню. Опись вносится в спеку. 2. Функция отбора узлов NETWORKS из конфига: чистая, с тестом. 3. Встраивание в перечень направлений и список узлов. 4. Строки интерфейса на английском, переводы по `docs/l10n.md`. ## Опись (исполнитель, 27.09.2026) - **Перечень направлений.** `HomeState.groups` — теги `selector`-групп из снапшота ядра (`HomeController._applyGroups` ← `CcChannel.groups`, `selectorGroupTags` без `GLOBAL`); подписи — `groupLabels` из хранилища. Выпадающий список — `screens/home/widgets/home_controls.dart`, значение пункта = тег группы (записи различались бы по названию, отсюда заглушка ниже). - **Состав узлов.** `HomeController.applyGroup` → `HomeState.nodes` = члены выбранной группы; порядок и фильтр — `node_list_presenter.dart`. - **Строка узла и меню.** `screens/home/widgets/node_list.dart` (`_buildNodeCell` → `widgets/node_row.dart` + `node_view_item.dart`); тап = подсветка, кнопка ▶ и пункт «Use this node» = `switchNode`; «View details» = `viewOutboundJson` (`node_actions.dart`) → `outbound_view_screen`. - **Обёртка `SubscribeTailscaleStatus`.** В Kotlin и Dart её не было. В AAR (`v1.14.2-lx.4`, javap) есть: `CommandClient.subscribeTailscaleStatus( TailscaleStatusHandler): TailscaleStatusSubscription`, колбэки `onStatusUpdate(TailscaleStatusUpdate)` / `onError(String)`, `TailscaleStatusUpdate.endpoints()` → итератор `TailscaleEndpointStatus` (`getEndpointTag`, `getBackendState`, `getStateText`, …), `TailscaleStatusSubscription.close()`. В Go подписка неблокирующая (горутина `subscribeStatus`), каждое обновление — полный список endpoint'ов. ## Реализация - **Отбор:** `app/lib/services/networks_direction.dart` — `networksNodeTags(ParsedConfig)` (условия 1–3, `exitCapableByRegistry`, кеш на экземпляр модели), `tailnetRowState` (таблица раздела 3). Модель — `HomeState.activeModel` (конфиг ядра, иначе последний собранный). - **Псевдо-направление — только вид.** `HomeState.networksOpen` + `showingNetworks` (VPN включён, узлы есть, выбран NETWORKS либо настоящих направлений нет). `selectedGroup` не меняется, поэтому выход трафика, шторка, автоматизация и Debug API NETWORKS не видят. Значение пункта — `kNetworksDirectionValue` (`\u0001networks`), не тег: направление пользователя с тегом `NETWORKS` не совпадает. Узлы пропали — снова виден выбранный настоящий список (edge case «при перезапуске»). - **Экран:** пункт `NETWORKS` последним в выпадающем списке (`home_controls.dart`), общий замер («speed») при NETWORKS выключен. Список — `_buildNetworksList` в `node_list.dart`: без фильтра, сортировки и перетаскивания; тап и «View details» открывают `outbound_view_screen`. `NodeRow` при `tailnetState != null`: нет кнопки ▶, нет пунктов Ping и Use this node; на месте задержки — состояние (`running` зелёным, `sign-in needed`/`stopped` оранжевым). Переводы ru/zh добавлены. - **Мост состояния:** `BoxCommandClient.startTailscaleStatus` / `stopTailscaleStatus` — отдельный клиент без команд + подписка, колбэки в `runCatching`, снапшот `{tag, backend_state, state_text}` через `SnapshotEmitter` в `BoxVpnService.ccTailscaleSink`; EventChannel `lxbox/cc/tailscale`, методы `ccStartTailscaleStatus` / `ccStopTailscaleStatus` (на `Dispatchers.IO`); `shutdownAll` закрывает подписку. Dart: `CcChannel.tailscaleStatus` (общий поток с кешем, один приёмник), `HomeController._syncTailnetStatus` в `_emit`: слушатель до старта, подписка живёт при VPN включён и узле NETWORKS, переподнимается при смене состава или снапшота конфига ядра. - **Тесты:** `app/test/services/networks_direction_test.dart` — отбор (с `exit_node`, без, WireGuard, `outbounds[]`, пустой итог), показ, каждая строка таблицы раздела 3, разбор сообщения канала, строка (нажатие не выбирает узел, в меню нет Ping и Use this node). - **Дефект после 298b3000:** при NETWORKS заголовок показывал «Nodes (N)» прежнего направления и кнопки сортировки и фильтров. Исправлено: `NodesHeader.listCount` / `showsListTools`; заголовок виден и при открытой панели фильтров (у NETWORKS панели нет). Тест — группа «заголовок списка при NETWORKS» в `networks_direction_test.dart`. - **Не покрыто тестом:** выпадающий список целиком (виджет-тест `HomeControls` тянет контроллеры); Kotlin-часть без устройства не проверяется. - **DEVICE-PENDING:** на эмуляторе с узлом Tailscale без `exit_node` — пункт NETWORKS последним, строка с состоянием `running`/`sign-in needed`, тап открывает экран узла с вкладкой Diagnostics; после reload ядра состояние обновляется; конфиг байт в байт прежний. ## Риски и edge cases - **Выбранное направление NETWORKS при перезапуске.** Если экран запоминает выбранное направление, а NETWORKS исчезло (узел удалён), выбирается первое настоящее направление. - **Конфиг без настоящих направлений**, но с узлом NETWORKS: экран показывает одно псевдо-направление. - **Узел стал выходом** (пользователь задал `exit_node`): после пересборки конфига узел уходит из NETWORKS в обычные списки. - **Автоматизация и Debug API**, которые переключают направление по тегу: NETWORKS целью переключения не является, запрос отклоняется как для неизвестного направления. ## Верификация Тесты (гонять только затронутые файлы): - функция отбора: Tailscale без `exit_node` входит; Tailscale с `exit_node` не входит; endpoint WireGuard вне групп не входит; пустой итог означает отсутствие NETWORKS; - состояние: каждая строка таблицы раздела 3; - экран: при узле по условиям NETWORKS есть и стоит последним; без таких узлов его нет; нажатие на узел не вызывает переключение выхода; - в NETWORKS нет замера задержки. Критерии приёмки: 1. Конфиг для любого состояния совпадает с прежним байт в байт. 2. Tailscale без `exit_node` виден на главном экране в NETWORKS, из строки открывается экран узла с вкладкой Diagnostics. ## Docs to update | Файл | Что | |---|---| | `docs/ARCHITECTURE.md` | псевдо-направление, условия состава | | `docs/PROTOCOLS.md` §9.7 | где виден узел Tailscale без выхода | | `CHANGELOG.md` | запись в Unreleased | ## Нерешённое / follow-up - Вкладка Network у узла Tailscale (отдельная задача): устройства сети, вход и выход из аккаунта, выбор exit node. Решение владельца 27.09.2026 по выбору exit node: выбор в списке переключает выход на ходу вызовом ядра `SetTailscaleExitNode`, тело узла не меняется; отдельная кнопка «записать выбор» пишет `exit_node` в тело узла и пересобирает конфиг. Когда у узла в теле `exit_node` не задан, а на ходу выбран, у выбора показывается знак предупреждения (восклицательный знак в треугольнике): выбор не записан, трафик в узел не направлен, заработает после записи. Остальной состав вкладки предложен владельцу, решения нет. - Диагностика узла без выхода: проверка запросом к внешнему адресу к нему неприменима (на эмуляторе даёт `context deadline exceeded` без пояснения).