Files
elmprodvpn/docs/EXECUTION_TRACKER.md

1167 lines
188 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Execution Tracker
Дата обновления: 2026-03-15
Владелец: Engineering
## Статусы фаз
- [x] A. Аудит API и HTTP-маршрутов
- [~] B. Проверка ядра и внешних зависимостей
- [~] C. Подготовка к веб-совместимости
- [~] D. Документирование и итоговые проверки
- [~] E. Дизайн multi-client PBR и anti-conflict guardrails
- [~] F. Рефакторинг и модульность (декомпозиция крупных файлов GUI/API/Go)
## Порядок реализации (фикс)
- 1. Сначала полностью закрываем backend-интеграцию transport-клиентов (`singbox`, `dnstt-client`, `phoenix`) в Go-ядре.
- 2. Затем переиспользуем backend-контракт в desktop GUI (без дублирования бизнес-логики).
- 3. Веб-прототип (`Vite + React + TS`) начинаем только после завершения backend+GUI этапа.
## Фокус текущего этапа (freeze)
- Desktop-first: сейчас работаем только по `SingBox` вкладке и `SingBox` Go API.
- `DNSTT` и `Phoenix`: backend foundation/e2e уже зафиксированы, UI-вкладки и протокольные экраны для них в этом этапе не развиваем.
- Приоритет реализации:
- временно сужаем активный backend scope до `E3.3/E3.6`:
- единый `interface orchestrator` path для всех mutating transport-операций,
- `validate -> plan -> confirm -> apply -> health-check -> commit` без частично подтверждённого runtime;
- `E5.4` GUI-протоколы внутри отдельной вкладки `SingBox` не расширяем, пока `E3.3/E3.6` не доведены до стабильного backend-state.
## Текущие шаги
- Step A1: [x] каталогизация маршрутов и сопоставление с Go-обработчиками
- Step A2: [x] валидация GUI как клиента (api_client.py, dashboard_controller)
- Step B1: [~] оценка nftables/systemd/SmartDNS зависимостей (`routes_*`, `traffic_*`, `dns_*`, `smartdns_runtime`)
- Step B2: [~] запись требований root/long-running/состояний для веба
- Step B3: [~] resolver hardening plan: зафиксировать разницу `system resolver` vs `singbox DNS` и закрыть backlog улучшений
- Step C1: [~] ревью binding и SSE (`server.go`, `events_bus.go`, `trace_handlers.go`)
- Step C2: [x] запись потребностей по CORS/auth/token для веб-интерфейса
- Step C3: [x] зафиксирован стек web prototype: `Vite + React + TypeScript` (SPA, без Next.js на MVP-этапе)
- Step C4: [x] создан web foundation-модуль `selective-vpn-web/` (routing + query + SSE connectivity, без mutating controls)
- Step D1: [x] матрица endpoint → handler → dependencies + web-ready
- Step D2: [x] чеклист проверок (curl, SSE, VPN login)
- Step D3: [~] последовательность запуска multi-client (web + iOS + Android) зафиксирована
- Step D4: [~] зафиксирован транспортный интеграционный бэклог (`sing-box client`, `dnstt-client`, `phoenix->slipstream`)
- Step D4.1: [x] внедрён минимальный общий transport backend-контракт в Go (`lifecycle + health + metrics + unified errors + capabilities contract hints`)
- Step D4.2: [~] внедрены backend-адаптеры foundation (`mock/systemd`) + template `exec_start` + restart/watchdog tuning + unit hardening + `runtime_mode` foundation + packaging updater/rollback foundation для `singbox|dnstt|phoenix`
- Step D4.2.8: [x] e2e backend-проверки по клиентам (`singbox -> dnstt(+ssh) -> phoenix`) через API lifecycle/runbook
- Step D4.2.9: [x] добавлен операционный runbook helper (`scripts/transport_runbook.py`) + smoke (`tests/transport_runbook_cli_smoke.sh`)
- Step D4.2.10: [x] добавлены real-systemd e2e и backend-cleanup unit artifacts при delete client
- Step D4.2.11: [x] добавлены production-like e2e (template commands + packaging profiles `bundled|system`)
- Step D4.2.12: [x] добавлен recovery runbook (health->restart->recheck->provision/start fallback + diagnostics)
- Step D4.2.13: [x] внедрён `singbox` bootstrap-bypass в Go backend (`transport`): endpoint `/32` host-routes в `table agvpn` через `main` default-route перед `start/restart`, cleanup на `stop`
- Step D4.2.14: [x] добавлен `netns` test-contour для transport backend (`setup/cleanup + NAT + optional strict mode`)
- Step D4.2.15: [x] миграция legacy DNS-конфига `sing-box` (`address -> type/server`) в `Provision()` + backup + strict-mode
- Step D4.3: [x] зафиксирована матрица совместимости `web + iOS + Android` (единый transport control-plane контракт + platform constraints)
- Step V1: [x] стабилизация UX списка VPN локаций (async + cache + auto-apply + лёгкий manual refresh trigger)
- Step V2: [x] добавить "умный поиск" в списке локаций (по набору символов без отдельной строки поиска)
- Step E1: [x] подготовлен дизайн multi-client PBR (`docs/phase-e/E1_MULTI_CLIENT_PBR_DESIGN.md`)
- Step E2: [x] спроектирован API `transport/*` (clients/policies/validate/apply/conflicts)
- Step E2.2: [x] реализован dry-run validator конфликтов (`ownership`, `cidr_overlap`, `unknown_client`)
- Step E2.3: [x] реализован apply flow (`base_revision`, `confirm_token`, snapshot rollback, SSE events)
- Step E3.1: [x] усилен allocator (`mark/pref` резервы, auto re-balance, deterministic restore)
- Step E3.2: [x] добавлен endpoint-level rollback (`/transport/policies/rollback`)
- Step E3.3: [x] реализовать единый interface orchestrator в Go (`create/bind/start/stop/cleanup` для engine-инстансов, netns/table sync без конфликтов)
- Step E3.3.0: [x] зафиксирован execution roadmap мультиинтерфейса (`docs/phase-e/E3_MULTI_INTERFACE_EXECUTION_PLAN.md`)
- Step E3.3.1: [x] foundation-этап: `iface_id` в `transport client` + state `transport-interfaces.json` + `GET /api/v1/transport/interfaces` (без изменения data-plane)
- Step E3.3.2: [x] внедрить interface orchestrator lock+mapping (`iface_id -> runtime iface/netns/table`) с единым lifecycle path
- Step E3.3.2.1: [x] добавлен per-`iface_id` lock manager для mutating lifecycle/provision операций (`start/stop/restart/provision`) в transport-контуре; операции одного интерфейса сериализуются
- Step E3.3.2.2: [x] runtime mapping-layer завершён: `iface_id -> routing_table` (dedicated iface default + interface hints + sync в create/patch/lifecycle/netns-toggle/provision) + owner-scoped nft compile naming (`iface+client+selector`) без shared-set mixing
- Step E3.3.2.3: [x] оставшиеся mutating transport-paths переведены на iface-aware orchestration lock:
- `create`, `PATCH /transport/clients/{id}`, `DELETE /transport/clients/{id}`, `POST /transport/netns/toggle`;
- добавлен multi-`iface_id` lock helper с детерминированным порядком захвата для patch/multi-target операций;
- `lifecycle start/restart` теперь расширяют lock-set до всех `same-netns` `SingBox` peer-ов, включая binding через `iface -> netns`, поэтому cross-`iface_id` netns-конфликт не обходит orchestrator;
- `netns toggle` больше не делает прямой `backend restart`: restart/provision теперь идут через общий backend execution path (`provision` + lifecycle preflight/orchestrator), поэтому peer-stop guard, egress refresh и runtime commit не дублируются отдельной веткой;
- lock-resolver для lifecycle/netns toggle теперь использует `same-netns` peer matching без фильтра по runtime-status (lock захватывает всех netns-peer клиентов), чтобы избежать гонок при stale/lag status и не пропускать cross-iface сериализацию.
- lifecycle/provision/create/patch/delete/netns теперь используют общий `iface` serialization pattern, без отдельной GUI/local orchestration-ветки.
- Step E3.3.3: [x] внедрён M3 per-interface policy compiler + atomic apply executor foundation:
- compile-plan `iface_id -> table/mark/pref/nft-set` подключён в validate/apply/rollback/get-policy;
- apply/rollback теперь проходят через atomic runtime executor с snapshot/restore (`transport-policies.runtime.json`, `transport-policies.runtime.prev.json`) до commit policy revision;
- подключён kernel stage в executor:
- nft stage: per-interface CIDR set apply/cleanup (`inet agvpn`, managed sets `agvpn_pi_*`);
- optional ip rule stage (`fwmark/pref -> lookup table`) под отдельным флагом;
- kernel stage включается через env-флаги: `SVPN_TRANSPORT_POLICY_KERNEL_APPLY=1`, `SVPN_TRANSPORT_POLICY_KERNEL_IPRULES=1`.
- M3 owner scope закрыт: compile-plan теперь генерирует owner-scoped `nft_set` (`owner_scope=iface+client`), что исключает shared-set mixing для нескольких клиентов на одном `iface_id`.
- Step E3.4: [x] внедрён strict ownership registry для policy-intent (single-owner на domain/ip/cidr/app intent, явные conflict reason до apply)
- Step E3.4.1: [x] добавлен persisted ownership registry (`transport-ownership.json`) + endpoint `GET /api/v1/transport/owners` (auto-rebuild из compile-plan при рассинхроне revision)
- Step E3.4.2: [x] apply-guardrails усилены: `force_override` разрешён только для `owner_switch`; non-overridable block (`ownership`, `cidr_overlap`, `unknown_client`, allocator conflicts) не обходятся override-флагом
- Step E3.4.3: [x] read-side ownership rebuild усилен `plan_digest` guard: `GET /api/v1/transport/owners` пересобирает ownership не только по `policy_revision`, но и при drift compile-plan digest; в response добавлен `plan_digest`, ownership records возвращают `owner_scope`
- Step E3.5: [x] добавлен anti-mixing guard (`conntrack` stickiness + destination owner lock), чтобы один destination не ходил через два engine одновременно
- Step E3.5.1: [x] добавлен runtime owner-lock guard в validate/apply: owner switch блокируется, если previous owner клиента активен (`up|starting|degraded`); `force_override` это не обходит
- Step E3.5.2: [x] расширен observability ownership: `GET /api/v1/transport/owners` теперь возвращает `owner_status` + `lock_active` по каждой записи и агрегат `lock_count`
- Step E3.5.3: [x] добавлен conntrack stickiness foundation: kernel stage умеет собирать destination-lock state (`transport-owner-locks.json`) по `mark->owner` mapping; endpoint `GET /api/v1/transport/owner-locks`; validate/apply блокируют owner-switch по `cidr` при активных `destination_lock`
- Step E3.5.4: [x] добавлен безопасный recovery clear-flow для owner-locks: `POST /api/v1/transport/owner-locks/clear` (filter: `client_id`, `destination_ip(s)`), двухшаговый confirm-token (`clr-*`), без unscoped clear-all
- Step E3.5.5: [x] расширен destination-lock guard на `domain` selector: owner-switch по `domain`/`*.domain` теперь учитывает resolver `domain-cache` (`direct+wildcard`) и блокируется при sticky-совпадении `destination_ip` у previous owner
- Step E3.6: [x] расширен apply pipeline до `validate -> plan -> confirm -> apply -> health-check -> commit` с auto-rollback при fail health-check
- Step E3.6.1: [x] добавлен post-apply transport health-check до policy commit:
- apply/rollback после runtime apply выполняют backend health probe по активным transport-owner клиентам из compile-plan;
- для `enabled/up|starting|degraded` клиентов failure/down считается commit-blocking, inactive draft clients (`enabled=false`, `status=down`) пропускаются;
- при health-check fail выполняется runtime auto-rollback на `transport-policies.runtime.prev.json`, policy revision не коммитится;
- `TransportPolicyResponse` дополнен `health_check` summary для GUI/Web наблюдаемости.
- Step E3.6.2: [x] добить transaction pipeline:
- уменьшить удержание глобального `transportMu` на long-running runtime steps после стабилизации orchestrator path;
- расширить health gate на более полный per-interface observability/runtime coverage.
- Step E3.6.2.1: [x] добавлен persisted `Idempotency-Key` storage/replay для mutating policy endpoints:
- `POST /api/v1/transport/policies/apply` и `POST /api/v1/transport/policies/rollback` сохраняют response snapshot по `(scope, idempotency_key, request_hash)`;
- повтор того же ключа с тем же payload возвращает сохранённый response без повторного runtime apply/rollback и без инкремента `policy_revision`;
- reuse того же ключа с другим payload блокируется кодом `IDEMPOTENCY_KEY_REUSED`;
- state вынесен в отдельный persisted backend store (`transport-policy-idempotency.json`) с pruning по TTL/size.
- Step E3.6.2.2: [x] уменьшить удержание глобального `transportMu` на long-running runtime steps после стабилизации orchestrator path
- `transport client provision` переведён на двухфазный flow:
- под `transportMu` остаются только snapshot/bind и final commit,
- сам `backend.Provision()` выполняется под `iface`-lock, но вне глобального mutex;
- `transport client lifecycle` переведён на двухфазный flow:
- `same-netns` peer-stop и target `backend.Action()` выполняются под `iface`-lock set, но вне глобального mutex;
- под `transportMu` остаются только snapshot/bind/reload/final commit, включая commit peer-stop результатов;
- partial peer-stop failure теперь сохраняет уже выполненные peer changes в state до возврата ошибки target lifecycle, без silent drift между runtime и persisted state;
- `transport netns toggle` переведён на такой же двухфазный flow:
- config patch/save остаётся под `transportMu`,
- долгие `provision/restart` шаги выполняются уже через общий provision/lifecycle path под `iface`-lock set, но вне глобального mutex;
- lock resolver для toggle теперь заранее включает `same-netns` peer `iface_id`, если restart затрагивает binding после toggle.
- Step E3.6.2.3: [x] расширен health gate на более полный per-interface observability/runtime coverage
- `TransportPolicyResponse.health_check` возвращает per-`iface_id` summary c runtime-полями:
- `iface_id/mode/runtime_iface/netns_name/routing_table`,
- `client_count/checked_count/failed_count/skipped_count`,
- `status`, `latency_ms`, `last_error`, `active_client_id`,
- interface-level `ok/message` для GUI/Web без ручной агрегации по client items;
- summary строится из того же post-probe runtime snapshot (включая netns-клиентов), что и client-level health items, без отдельной UI-склейки.
- Step E3.6.2.4: [x] сокращён hold-time `transportMu` в read-side snapshot handlers (`/transport/interfaces`, `/transport/policies`, `/transport/ownership`, `/transport/runtime/observability`):
- под `transportMu` выполняется только короткий state snapshot (`clients/interfaces/policy/plan/ownership`) + token capture;
- нормализация/compile-plan выполняются вне глобального mutex;
- добавлен snapshot-aware conditional commit helper (`compileTransportPolicyPlanForSnapshot`, `saveTransportInterfacesIfSnapshotCurrent`, `saveTransportPlanIfSnapshotCurrent`, `saveTransportOwnershipIfSnapshotCurrent`) для safe save без stale overwrite;
- API/DTO контракты endpointов сохранены без изменений.
- Step E3.6.2.5: [x] доведён аналогичный snapshot/conditional-save подход для `netns` toggle-пути (`POST /api/v1/transport/netns/toggle`):
- lock-id resolver теперь использует короткий snapshot (`clients/interfaces`) под `transportMu`, нормализация interfaces и conditional-save выполняются вне глобального mutex;
- в execute-path убран persist interfaces из-под `transportMu`: используется in-memory normalize для binding, а persist вынесен в post-unlock `conditional-save helper`;
- добавлен pure helper `resolveTransportLifecycleLockIDsForSnapshot` (in-memory), lock-resolver теперь использует snapshot-state без повторного I/O под mutex; flaky unit-case стабилизирован.
- устранён stale-guard gap для mutating netns-path: post-commit persist interfaces больше не зависит от `clients.updated_at` (используется interfaces-only snapshot), поэтому нормализованные iface-записи не теряются после `saveTransportClientsState`.
- mutating контракт netns-toggle сохранён (clients state commit остаётся atomic под `transportMu`, provision/restart по-прежнему вне глобального mutex).
- Step E3: [x] схема allocator'ов (`mark/table/pref`) и стратегия atomic apply/rollback
- Step E4.1: [x] зафиксирован UX-flow `validate -> confirm -> apply` для web/mobile
- Step E4.2: [x] внедрён foundation state-machine в GUI controller (`draft/validated/risky/confirm/applied`)
- Step E4.3.1: [x] добавлен GUI foundation-блок `Transport engine` (select + prepare/connect/disconnect/restart через `/api/v1/transport/clients/{id}/*`)
- Step E4.3.2: [x] `Connect/Switch` переведён на pipeline `validate -> confirm -> apply` + добавлен `Rollback policy` button
- Step E4.3: [~] детализирован UX-подпоток `Engine Switch / Connect` (desired/active engine, switch states, rollback action)
- Step E4.4: [x] зафиксирован desktop-first дизайн общего multi-interface GUI блока (`Ownership + Destination locks + safe clear-flow`) в `docs/phase-e/E4_2_MULTI_INTERFACE_GUI_DESIGN.md`
- Step E4.5: [x] desktop GUI подключён к ownership/lock API:
- read-only панель `Ownership & destination locks` во вкладке `SingBox` (таблицы ownership + destination locks, summary/revision);
- refresh-хук встроен в `refresh_singbox_tab` и transport refresh-поток;
- подключён безопасный clear-flow через `POST /api/v1/transport/owner-locks/clear` (2-step confirm token, без optimistic update, только re-fetch).
- Step E4.5.1: [x] desktop GUI подключён к `GET /api/v1/transport/interfaces`:
- в панели `Ownership & destination locks` добавлена таблица интерфейсов (`iface_id/mode/runtime_iface/netns/routing_table/client up/total`);
- summary расширен данными `interfaces count + policy revision + intents count`.
- Step E4.5.2: [x] добавлен desktop GUI policy-intents editor (в `SingBox` блоке `Ownership & destination locks`):
- draft-таблица intents (`selector_type/value`, `client_id`, `mode`, `priority`);
- actions: `Reload policy`, `Add intent`, `Remove selected`, `Validate policy`, `Apply policy`, `Rollback policy`;
- apply использует backend flow `validate -> (optional confirm for risky) -> apply`, rollback через `/transport/policies/rollback`;
- черновик не перетирается авто-refresh'ем при dirty state.
- Step E4.5.3: [x] UX-polish policy-intents в MultiIF:
- добавлены quick templates (`domain/wildcard/cidr/ip/app_key/uid`) с prefill без auto-add;
- добавлен `Load selected` + double-click по строке draft для обратной подгрузки intent в форму редактирования;
- добавлен Enter-to-add для selector value и duplicate guard (не даёт добавить полностью идентичный intent);
- в draft-таблице клиент показывается как `name (client_id)` для лучшей читаемости.
- Step E4.5.4: [x] добавлено inline-редактирование draft intent в MultiIF:
- добавлена кнопка `Update selected` (обновляет выбранную строку без remove/add);
- общий validator формы переиспользуется для add/update (domain/cidr/uid guardrails едины);
- duplicate guard поддерживает `skip_index` при update (строку можно обновлять без ложного self-duplicate).
- Step E4.5.5: [x] MultiIF policy/ownership отвязаны от `transport-only` и включают `AdGuard VPN` как virtual policy target:
- в backend compile/validate/apply/rollback path добавлен virtual client `adguardvpn` (без записи в `transport-clients.json`);
- ownership/locks/read-side используют единый `policyTargets` snapshot (`transport clients + adguardvpn`);
- в GUI включён scope-filter `All/Transport/AdGuard VPN`, client selector для intents поддерживает `AdGuard VPN (adguardvpn)`;
- сняты artificial guard-блоки `adguard-only`, чтобы low-level MultiIF слой работал единообразно для всех engine.
- Step E4.5.6: [x] интерфейсный слой MultiIF переведён на backend-source-of-truth для AdGuard:
- `GET /api/v1/transport/interfaces` теперь добавляет virtual interface-row `adguardvpn` (status/iface/table/up-count) из backend observer;
- GUI больше не строит adguard-строку локально через `vpn_status_model`/egress-хелперы;
- filter `All/Transport/AdGuard VPN` применяется единообразно к interfaces/ownership/locks таблицам.
- Step E4.5.7: [x] добавлен быстрый UX-flow для первичной проверки MultiIF policy editor:
- кнопка `Add demo intent` создаёт тестовый `domain` intent (`demo.invalid`, авто-уникализация `demo-N.invalid`) на выбранный client;
- quick-help обновлён до пошагового сценария `Add demo/fill -> Validate -> Apply`;
- flow использует тот же draft/validate/apply pipeline без отдельной ветки логики.
- Step E4.5.8: [x] в MultiIF добавлено визуальное разделение `Draft` и `Applied` policy intents:
- добавлена отдельная read-only таблица `Applied intents` (текущая backend policy);
- status/state строка теперь показывает оба счётчика (`draft` и `applied`) для быстрого сравнения;
- API-unavailable/error paths очищают обе таблицы синхронно, чтобы не было stale-данных.
- Step E4.5.9: [x] добавлена визуализация конфликтов валидации policy (MultiIF):
- отдельная read-only таблица `Validation conflicts` (`type/severity/owners/reason/suggested resolution`);
- таблица синхронизируется из `validate/apply` flow и сохраняет последний результат проверки;
- при API-unavailable/endpoint-error таблица очищается вместе с draft/applied, чтобы не показывать stale-conflicts.
- Step E4: [~] UX предупреждения и conflict flow (`validate -> confirm -> apply`, включая engine switch/connect)
- Step E5: [~] зафиксировать требования для протоколов во вкладке `SingBox` и target Go API `singbox profiles`
- Step E5.1: [x] requirements freeze для `SingBox Protocols` (UI + Go API + storage/events)
- Step E5.1.2: [x] собран шаблон и матрица полей по протоколам `singbox` (`vless/trojan/shadowsocks/wireguard/hysteria2/tuic`) + JSON manifest для генерации форм
- Step E5.1.3: [x] зафиксирована client-side UI матрица (по блокам формы, VLESS baseline + guardrails, без server-only полей)
- Step E5.2: [x] реализовать Go model/state для `singbox profiles` (CRUD + versioning + secrets store)
- Step E5.3: [x] реализовать Go flow `validate/render/apply/rollback/history/features` для `singbox profiles`
- Step E5.4: [~] реализовать GUI-блок протоколов в `SingBox` вкладке (list/editor/validate/preview/apply/rollback)
- Step E5.4.1: [x] внедрён desktop foundation-дизайн `SingBox` вкладки: `connection card` + `profile settings` + `global defaults`
- Step E5.4.2: [x] подключены GUI-кнопки `Validate profile`/`Apply profile` к Go API `/api/v1/transport/singbox/profiles/{id}/validate|apply` (с activity-log и runtime refresh)
- Step E5.4.3: [x] реализованы `Preview render` + `Rollback profile` + `History` и auto-link `engine -> profile` (auto-create/patch по `client_id`) + лёгкий рефакторинг handlers вкладки `SingBox`
- Step E5.4.4: [x] добавлен VLESS client-editor flow в GUI: auto-load по выбранному engine/profile, `Save draft` в Go API (`raw_config`), auto-sync draft перед `Preview/Validate/Apply`
- Step E5.4.5: [x] `Connection profiles` переведены в dashboard-плитки с context-menu `Run/Edit/Delete`; активный профиль подсвечивается зелёным, protocol editor вынесен в отдельный modal edit-dialog
- Step E5.4.6: [x] улучшен editor UX: non-destructive переключение `security/transport` (без reset полей), `Flow` сделан editable (preset + custom), добавлен `Create connection` (`Clipboard|Link|Manual`)
- Step E5.4.7: [x] унифицирован link-import pipeline: общий dispatcher + shared raw-config helpers для `vless/trojan/ss/hysteria2(hy2)/tuic`, без привязки к одному протоколу
- Step E5.4.8: [x] расширен form-editor на `trojan/shadowsocks/hysteria2/tuic` (единая форма + protocol switch + guardrails + raw save по выбранному protocol)
- Step E5.4.9: [x] добавлен `wireguard` в единый editor/import pipeline (поля WG + raw save + wireguard:// link parser)
- Step E6: [x] реализовать единый Go-сервис `egress identity` (IP + geo/country) для всех движков (`AdGuardVPN`, `transport:*`)
- Step E6.1: [x] зафиксировать backend-контракт `/api/v1/egress/identity` + `/api/v1/egress/identity/refresh` (scope-aware: `adguardvpn|transport:<id>|system`)
- Step E6.2: [x] реализовать provider-адаптеры в Go (общий интерфейс источника egress IP; netns-aware для transport-клиентов)
- Step E6.3: [x] добавить общий SWR/cache/backoff для egress identity (без блокировки UI, с `stale/updated_at/last_error`)
- Step E6.4: [x] добавить GeoIP-слой (country_code/country_name) и нормализованный ответ для GUI/Web/Mobile
- Step E6.5: [x] интегрировать в desktop-карточки `AdGuardVPN` и `SingBox` (показывать `IP + country`, флаг рендерится в UI из `country_code`)
- Step E6.6: [x] добавить unified runtime observability API для multi-interface (`active_iface`, `egress`, `latency`, `last_error`, counters per engine/policy)
- Step E6.6.1: [x] добавить backend DTO snapshot для карточек multi-interface (`client_id`, `iface_id`, `active_iface`, `egress`, `latency`, `last_error`, counters)
- новый backend aggregator собирает per-`iface_id` runtime snapshot из `transport interfaces + clients + policy compile-plan + egress identity`;
- DTO включает binding (`runtime_iface/active_iface/netns/routing_table`), active client, aggregate `status/latency/last_error`, status counters и `engine_counts/rule_count` без ручной склейки на UI.
- Step E6.6.2: [x] endpoint `GET /api/v1/transport/runtime/observability` + SSE `transport_runtime_snapshot_changed`
- handler и SSE publisher переиспользуют один и тот же backend snapshot builder без отдельной event-specific логики;
- `transport_runtime_snapshot_changed` публикуется после create/patch/delete, lifecycle, health refresh, policy apply/rollback, netns toggle и transport egress updates;
- payload события содержит unified snapshot (`items`) и метаданные `reason/client_ids/iface_ids/generated_at`, так что UI может либо re-fetch endpoint, либо обновиться напрямую тем же DTO.
- Step E6.7: [~] (обязательный post-transport этап) подключить `AdGuardVPN` через adapter к unified engine control-plane (без ломки autoloop/login/location flow), чтобы multi-interface observability/control был единым для всех движков
- Step E6.7.1: [x] backend adapter-путь `adguardvpn` подключён к unified transport control-plane:
- `GET /api/v1/transport/clients?include_virtual=true` возвращает virtual client `adguardvpn` в том же DTO-контракте, что и transport clients;
- `GET /api/v1/transport/clients/adguardvpn` + `health|metrics|start|stop|restart|provision` работают через virtual adapter без записи в `transport-clients.json`;
- legacy `POST /api/v1/vpn/autoconnect` переведён на тот же adapter execution path, чтобы исключить рассинхрон между old VPN flow и unified control-plane;
- runtime observability (`/transport/runtime/observability`) теперь включает virtual snapshot `adguardvpn` и использует scope-aware egress lookup (`adguardvpn` вместо `transport:adguardvpn`).
- Step E6.7.2: [x] GUI policy-editor переведён на unified policy targets из backend (`include_virtual=true`) без локального adguard-костыля:
- добавлен отдельный UI-cache `transport_policy_clients` (source of truth для `policy client selector` и label-резолва);
- client selector и draft/applied таблицы теперь используют общий backend-список targets (`transport + virtual`) и не подмешивают `adguardvpn` вручную;
- refresh policy locks делает явный prefetch policy-targets, а при ошибке использует безопасный fallback на текущий transport-list, без остановки UI.
- Step E6.7.3: [x] добавлена визуальная индикация типа policy target в MultiIF (`[transport]`/`[virtual]`):
- policy client selector, draft/applied intents и ownership/locks таблицы показывают тип target в label;
- interfaces таблица маркирует строки по типу target и отдельно помечает virtual-mode (`... | VIRTUAL`);
- summary дополнен счётчиками targets (`transport=N`, `virtual=M`) и hint с legend по меткам.
- Step E7: [ ] (deferred) разделить policy-слои `System selective PBR` и `SingBox L7 routing` (реализация позже, после текущего контура)
- Step E7.1: [ ] зафиксировать единый L7 rule-contract для SingBox (`domain/suffix/keyword/regex`, `ip_cidr`, `port`, `network`, `protocol`, `process/user/package`, `rule_set`, `priority`, `action`)
- Step E7.2: [ ] реализовать Go renderer `policy -> singbox.json` (`dns`, `route.rules`, `rule_set`, `outbounds`, `final`) без ручного JSON в UI
- Step E7.3: [ ] внедрить pipeline `validate -> dry-run -> apply -> rollback` для L7 policy (`sing-box check` обязателен)
- Step E7.4: [ ] реализовать DNS-стратегию для L7 (`SingBox DNS policy` + `bootstrap bypass`) без дублирования intent в системном resolver-контуре
- Step E7.5: [ ] добавить conflict guardrails между PBR и L7 (single-owner intent: правило живет только в одном policy-слое)
- Step E7.6: [ ] добавить наблюдаемость L7 (`effective policy`, `active outbound`, `last match reason`, counters/hits per rule, egress per-engine)
- Step F1: [~] выполнить поэтапный рефакторинг и модульность без изменения поведения
- Step F1.1: [x] зафиксирован план декомпозиции крупных файлов (`docs/phase-f/F1_REFACTOR_MODULARITY_PLAN.md`)
- Step F1.2: [x] netns-логика вынесена в отдельные модули GUI/API (`netns_debug.py`, `transport_netns_exec.go`) + оформлен runtime-case документ
- Step F1.2.1: [x] orchestration netns-toggle перенесён в Go API endpoint `/api/v1/transport/netns/toggle`, GUI переключатель переведён на единый backend вызов
- Step F1.2.2: [x] добавлен общий backend `refresh coordinator` (SWR + single-flight + backoff), `vpn locations` переведены на общую схему без изменения API-контракта
- Step F1.2.3: [x] та же SWR-схема применена для `transport health` (`/transport/clients` background refresh + `POST /transport/health/refresh` + SSE `transport_client_health_changed`)
- Step F1.6: [x] перевести точки входа Go API на `cmd/*` (отдельные бинари `api`, `routes-update`, `routes-clear`, `autoloop`) с сохранением legacy entrypoint
- Step F1.7: [x] разрезать `selective-vpn-api/app/server.go` на `entrypoints/api_bootstrap/api_routes` без изменения API-контракта и CLI-поведения
- Step F1.8: [x] вынести доменные route-helper'ы в отдельные `app/api_routes_*.go` файлы (без изменения endpoint-path/handler-контракта)
- Step F1.9: [x] вынести CLI/bootstrap раннеры в подпакеты `app/cli` и `app/bootstrap` с сохранением фасадов `Run*` в `app`
- Step F1.10: [x] разложить `app/transport_handlers.go` на модульные файлы (`handlers_clients`, `handlers_policy`, `policy_validate`, `client_runtime`, `tokens_state`, `shared`) без изменения endpoint-path
- Step F1.11: [~] вынести переиспользуемую transport-логику в отдельные подпакеты (`app/transport/*`) только для частей без циклических зависимостей (через facade+deps)
- Step F1.11.1: [x] вынесен runtime/config helper-блок transport backend в подпакет `app/transportcfg` с сохранением app-facade (`transport_backends_runtime_helpers.go`)
- Step F1.11.2: [x] вынесен systemd helper-блок transport backend в `app/transportcfg` с сохранением app-facade (`transport_backends_systemd_helpers.go`)
- Step F1.11.3: [x] вынесен exec/binary/template helper-блок transport backend в `app/transportcfg` с сохранением app-facade (`transport_backends_exec_helpers.go`)
- Step F1.11.4: [x] вынесен probe/endpoint helper-блок transport backend в `app/transportcfg` с сохранением app-facade (`transport_backends_probe_helpers.go`)
- Step F1.11.5: [x] вынесен in-memory SSE event-bus в подпакет `app/eventsbus` с сохранением app-facade (`events_bus.go`)
- Step F1.11.6: [x] вынесен API route-registry в подпакет `app/apiroutes` (удалены `api_routes_{core,dns,trace,traffic,transport,vpn}.go`, сохранён единый `registerAPIRoutes` facade)
- Step F1.11.7: [x] объединены misc transport adapters в `transport_backends.go` (удалён `transport_backends_adapters_misc.go`)
- Step F1.11.8: [x] вынесены низкоуровневые command helpers в `app/syscmd` (`RunCommand`, `RunCommandTimeout`, `CheckPolicyRoute`) с сохранением фасада `app/shell.go`
- Step F1.11.9: [x] вынесены общие HTTP helper'ы в `app/httpx` (`LogRequests`, `WriteJSON`, `HandleHealthz`) с сохранением фасада `app/http_helpers.go`
- Step F1.11.10: [x] вынесен SSE stream-loop в `app/eventstream` (`ParseSinceID`, `Serve`) с сохранением фасада `app/events_handlers.go`
- Step F1.11.11: [x] вынесен SWR/backoff coordinator в `app/refreshcoord` с сохранением фасада `app/refresh_coordinator.go` и без изменения поведения egress/vpn-locations/transport-health контуров
- Step F1.11.12: [x] вынесен NFT update engine в `app/nftupdate` (atomic + chunked fallback) с сохранением фасада `app/nft_update.go`
- Step F1.11.13: [x] вынесен traffic candidates collector в `app/trafficcandidates` с сохранением API-контракта endpoint `GET /api/v1/traffic/candidates`
- Step F1.11.14: [x] устранена «россыпь bridge-файлов» resolver-контура: `resolver_*_bridge.go` схлопнуты в единый `app/resolver_bridge.go` (без изменения API/логики)
- Step F1.11.15: [x] вынесен traffic app profiles store в `app/trafficprofiles` с сохранением API-контракта `GET/POST/DELETE /api/v1/traffic/app-profiles`
- Step F1.11.16: [x] вынесена canonical `app_key` нормализация в `app/trafficprofiles` и удалён root-файл `traffic_appkey.go` (совместимость через app-facade функции сохранена)
- Step F1.11.17: [x] вынесены state/dedupe/persistence helper'ы `traffic app marks` в подпакет `app/trafficappmarks` с сохранением runtime/API-контракта
- Step F1.11.18: [x] вынесены cgroup path/inode helper'ы `traffic app marks` в `app/trafficappmarks` с сохранением фасадов в `app/traffic_appmarks.go`
- Step F1.11.19: [x] вынесены NFT helper'ы `traffic app marks` в `app/trafficappmarks/nft.go` (insert/delete/has/local-bypass/handle parse/ipv4-compact) с сохранением фасадов в `app/traffic_appmarks.go`
- Step F1.11.20: [x] вынесены cleanup helper'ы `traffic app marks` в `app/trafficappmarks/nft.go` (`CleanupLegacyRules`, `ClearManagedRules`) с сохранением фасадов в `app/traffic_appmarks.go`
- Step F1.11.21: [x] вынесена TTL prune-логика `traffic app marks` в `app/trafficappmarks/store.go` (`PruneExpired`) с сохранением удаления runtime nft-правил через app-facade callback
- Step F1.11.22: [x] вынесен базовый nft ensure bootstrap `traffic app marks` в `app/trafficappmarks/nft.go` (`EnsureBase`) с сохранением фасада `ensureAppMarksNft` в `app/traffic_appmarks.go`
- Step F1.11.23: [x] вынесены общие `singbox profile flow` helper'ы в `app/transportcfg/singbox_helpers.go` (`typed protocol support`, `parse port`, `digest/diff config`, `json config file io`, `optional file restore`, `history stamp`, `issue message join`) с сохранением app-facade вызовов в `transport_singbox_profiles_flow.go`
- Step F1.11.24: [x] вынесены history helper'ы `singbox profile flow` в `app/transportcfg/history_helpers.go` (`WriteFileAtomic`, `ReadJSONFiles`, `SelectRecordCandidate`, `DecodeBase64Optional`) и переведены вызовы `append/load/select/decode` в `transport_singbox_profiles_flow.go`
- Step F1.11.25: [x] удалены локальные helper'ы `findSingBoxBinary`/`sanitizeHistoryStamp` в `transport_singbox_profiles_flow.go`; вызовы переведены на `transportcfg.FirstExistingBinaryCandidate` и `transportcfg.SanitizeHistoryStamp`
- Step F1.11.26: [x] вынесены secrets-map helper'ы в `app/transportcfg/secrets_helpers.go` (`NormalizeSecretUpdates`, `Clone/Equal/MaskStringMap`, `Read/WriteStringMapJSON`), `transport_singbox_profiles.go` переведён на пакетные вызовы
- Step F1.11.27: [x] очищен `transport_singbox_profiles_flow.go` от остаточных локальных wrapper'ов (config io/file optional/diff/normalize), вызовы переведены на `transportcfg` напрямую
- Step F1.11.28: [x] вынесен state/normalization-блок `singbox profiles` в отдельный файл `app/transport_singbox_profiles_state.go` (`load/save/normalize/derive/index/active/select + cloneMapDeep`) без изменения API-контрактов
- Step F1.11.29: [x] вынесены secrets/error helper'ы `singbox profiles` в `app/transport_singbox_profiles_secrets.go` и `app/transport_singbox_profiles_errors.go`; `transport_singbox_profiles.go` оставлен как handlers/mutate фасад
- Step F1.11.30: [x] вынесен mutate-блок `singbox profiles` (`create/patch`) в `app/transport_singbox_profiles_mutate.go`; `transport_singbox_profiles.go` сфокусирован на HTTP handlers/route dispatch
- Step F1.11.31: [x] вынесены runtime helper'ы `singbox profile flow` в `app/transport_singbox_profiles_runtime.go` (`find profile by client`, `prepare profile`, `resolve apply client`, `apply runtime`)
- Step F1.11.32: [x] вынесены history helper'ы `singbox profile flow` в `app/transport_singbox_profiles_history.go` (`append/load/select/decode history`, `join issue messages`)
- Step F1.11.33: [x] вынесен eval/render helper-блок `singbox profile flow` в `app/transport_singbox_profiles_eval.go` (`evaluate`, `binary validate`, `rendered path/config writer`, issue mapping)
- Step F1.11.34: [x] вынесен state/normalization-блок `transport client runtime` в `app/transport_client_runtime_state.go` (`create builder`, `kind/capabilities normalize`, `runtime snapshot normalize`, `uptime/index/clone helpers`) без изменения API-контрактов
- Step F1.11.35: [x] вынесен `transport client runtime` health/netns-блок в отдельные файлы `app/transport_client_runtime_health.go` и `app/transport_client_runtime_netns.go` (response snapshot + netns peer-stop guard) без изменения lifecycle/API-контрактов
- Step F1.11.36: [x] выполнен разнос transport client handlers: netns toggle вынесен в `app/transport_handlers_netns.go`, lifecycle action-dispatch вынесен в `app/transport_handlers_actions.go`, `transport_handlers_clients.go` оставлен list/card routing-фасадом
- Step F1.11.37: [x] вынесен helper-блок `routes update` в `app/routes_update_helpers.go` (table/list/io/counter/fs/readiness helpers) без изменения orchestration-пайплайна `routesUpdate`
- Step F1.11.38: [x] вынесен state/storage/normalize блок `dns settings` в `app/dns_settings_state.go` (`upstream pool`, `mode state`, `smartdns helpers`, `dnscfg adapters`) без изменения API handlers
- Step F1.11.39: [x] начата декомпозиция `types.go`: DNS/SmartDNS DTO вынесены в `app/types_dns.go` (без изменения JSON-контрактов/имён типов)
- Step F1.11.40: [x] продолжена декомпозиция `types.go`: вынесены доменные DTO в `app/types_traffic.go`, `app/types_egress.go`, `app/types_transport.go`, `app/types_singbox.go`; `types.go` оставлен для shared/system/event/resolver моделей
- Step F1.11.41: [x] выполнен разнос `transport policy handlers`: `validate/apply/rollback` вынесены в `app/transport_handlers_policy_mutations.go`; `transport_handlers_policy.go` оставлен для `list/conflicts/capabilities`
- Step F1.11.42: [x] вынесен endpoint `POST /transport/health/refresh` в `app/transport_handlers_health_refresh.go`; `transport_health_refresh.go` оставлен как runtime S-W-R/probe logic
- Step F1.11.43: [x] вынесен low-level probe/helper блок `egress identity` в `app/egress_identity_probe.go` (`probe via system/iface/netns/proxy`, endpoint helpers, singbox socks parse, iface bind address) без изменения API/SWR-контракта
- Step F1.11.44: [x] выполнен разнос `routes handlers` service/timer блока в `app/routes_handlers_service.go`; `routes_handlers.go` оставлен для status/clear/fix/update endpoints
- Step F1.11.45: [x] выполнен разнос `routes handlers` clear/precheck/op-lock блока в `app/routes_handlers_ops.go`; `routes_handlers.go` оставлен для status/policy-fix/update endpoints
- Step F1.11.46: [x] вынесены `egress identity` HTTP handlers в `app/egress_identity_handlers.go`; `egress_identity.go` оставлен для service/SWR/state orchestration
- Step F1.11.47: [x] выполнен разнос монолита `app/traffic_appmarks.go` на `traffic_appmarks_handlers.go`, `traffic_appmarks_ops.go`, `traffic_appmarks_runtime.go`; `traffic_appmarks.go` оставлен для core-констант/типов (без изменения API/runtime поведения)
- Step F1.11.48: [x] вынесен helper-блок `routes cache` в `app/routes_cache_helpers.go` (table lines/io/restore-route/nft-snapshot/meta/file helpers); `routes_cache.go` оставлен orchestration-фасадом restore/save
- Step F1.11.49: [x] выполнен разнос `egress identity` runtime-контура: provider-probe вынесен в `app/egress_identity_providers.go`, refresh/snapshot orchestration в `app/egress_identity_refresh.go`, geo cache/lookup в `app/egress_identity_geo.go`, scope/identity helpers в `app/egress_identity_scope.go`; `app/egress_identity.go` оставлен с константами/типами/инициализацией
- Step F1.11.50: [x] выполнен разнос `vpn handlers` монолита на `app/vpn_handlers_auth.go` (login/logout/systemd), `app/vpn_handlers_status.go` (autoloop/status parse), `app/vpn_handlers_locations.go` (autoconnect/locations/set-location resolver); `app/vpn_handlers.go` оставлен как модульный фасад-комментарий
- Step F1.11.51: [x] вынесены helper'ы `autoloop` в `app/autoloop_helpers.go` (`login-state write/parse`, `connected check`, `location resolve`, `ISO/cache helpers`), `app/autoloop.go` оставлен как loop orchestration без изменения runtime-поведения
- Step F1.11.52: [x] выполнен разнос `transport systemd backend` на роль-файлы: `transport_backends_adapter_systemd_action.go`, `..._health.go`, `..._provision.go`, `..._cleanup.go`; базовый `transport_backends_adapter_systemd.go` оставлен для shared type aliases/render helpers
- Step F1.11.53: [x] выполнен stage-разнос `routes_update.go`: preflight вынесен в `routes_update_stage_preflight.go`, policy routing в `routes_update_stage_policy.go`, nft setup/progress update в `routes_update_stage_nft.go`, domains+resolver merge в `routes_update_stage_resolve.go`, artifacts/status publish в `routes_update_stage_artifacts.go`; `routes_update.go` оставлен orchestration-фасадом
- Step F1.11.54: [x] выполнен разнос `vpn_login_session.go`: HTTP endpoints и `loginStateAlreadyLogged` вынесены в `vpn_login_session_handlers.go`, базовый `vpn_login_session.go` оставлен для manager/PTY state-machine (без изменения API login session `/api/v1/vpn/login/session/*`)
- Step F1.11.55: [x] выполнен разнос `vpn_locations_cache.go`: refresh/SWR orchestration вынесен в `vpn_locations_cache_refresh.go`, parser/location-target блок в `vpn_locations_cache_parse.go`, cache io/normalize/store в `vpn_locations_cache_store.go`; базовый файл оставлен с типами/константами/инициализацией
- Step F1.11.56: [x] выполнен разнос `transport_health_refresh.go`: SWR/state mutex блок вынесен в `transport_health_refresh_state.go`, queue/candidate scheduling в `transport_health_refresh_queue.go`, probe runtime в `transport_health_refresh_probe.go`, persist/change-bucket helpers в `transport_health_refresh_compare.go`; базовый файл оставлен с константами/типами/конструктором
- Step F1.11.57: [x] выполнен разнос `transport_client_runtime_alloc.go`: normalize/reconcile блок вынесен в `transport_client_runtime_alloc_normalize.go`, table/mark/pref slot helper'ы в `transport_client_runtime_alloc_slots.go`; базовый файл оставлен для `allocateTransportSlots` фасада
- Step F1.11.58: [x] выполнен разнос `transport_netns.go`: spec/name/prefix/hash/existence вынесены в `transport_netns_spec.go`, nft/policy-route helpers в `transport_netns_rules.go`, command soft/must wrappers в `transport_netns_run.go`; базовый файл оставлен для `ensure/cleanup` orchestration
- Step F1.11.59: [x] выполнен разнос `transport_bootstrap_bypass.go`: candidate/source extractors вынесены в `transport_bootstrap_bypass_candidates.go`, resolve/target/main-route в `transport_bootstrap_bypass_resolve.go`, route mutation в `transport_bootstrap_bypass_routes.go`, state io/normalize в `transport_bootstrap_bypass_state.go`; базовый файл оставлен для control-flow и shared error helpers
- Step F1.11.60: [x] выполнен разнос `transport_singbox_profiles_flow.go`: HTTP handlers вынесены в role-файлы `transport_singbox_profiles_flow_{validate,render,apply,rollback,history}.go`; базовый `transport_singbox_profiles_flow.go` оставлен для shared моделей/dispatcher
- Step F1.11.61: [x] выполнен разнос `transport_singbox_profiles.go`: list handlers вынесены в `transport_singbox_profiles_list.go`, by-id/card handlers в `transport_singbox_profiles_card.go`, features endpoint в `transport_singbox_profiles_features.go`; базовый файл оставлен для state/constants/shared mutex
- Step F1.11.62: [x] выполнен разнос benchmark-контура `dns_settings.go`: heavy benchmark handlers/helper'ы вынесены в `dns_settings_benchmark.go`; `dns_settings.go` оставлен для DNS mode/upstreams/smartdns control handlers
- Step F1.11.63: [x] выполнен разнос `resolver_bridge.go`: базовые alias/type/const вынесены в `resolver_bridge.go`, util wrappers в `resolver_bridge_utils.go`, domain-cache adapters в `resolver_bridge_cache.go`, dns-mode/lookup adapters в `resolver_bridge_dns.go`, planning/pipeline adapters в `resolver_bridge_pipeline.go`
- Step F1.11.64: [x] выполнен разнос `transport_backends.go`: probe adapters (`dial/probe/endpoints/deps`) вынесены в `transport_backends_probe.go`, runtime-mode backend structs (`unsupported/mock`) в `transport_backends_runtime_modes.go`; базовый файл оставлен для backend selection/config facade
- Step F1.11.65: [x] выполнен разнос `vpn_login_session.go`: session state/mutex methods вынесены в `vpn_login_session_state.go`, PTY lifecycle/parser в `vpn_login_session_pty.go`, базовый файл оставлен для DTO/models/manager struct; HTTP handlers остаются в `vpn_login_session_handlers.go`
- Step F1.11.66: [x] выполнен разнос `types_transport.go`: модели разделены на `types_transport_core.go` (client/runtime/capabilities), `types_transport_policy.go` (policy/conflicts), `types_transport_runtime.go` (health/lifecycle/refresh/netns responses); поведение API/JSON-контракты не изменены
- Step F1.11.67: [x] выполнен разнос `transport_singbox_dns_migration.go`: rule helper'ы вынесены в `transport_singbox_dns_migration_rules.go`, convert/parse блок в `transport_singbox_dns_migration_convert.go`, общий helper `asString/parsePort` вынесен в `helpers_values.go`; миграционный runtime/API-поток сохранён без изменений
- Step F1.11.68: [x] выполнен разнос `vpn_handlers_locations.go`: selection/validation helper'ы вынесены в `vpn_locations_selection.go`, основной handler-файл оставлен для `autoconnect/list/set-location` endpoint-потока и egress refresh trigger
- Step F1.11.69: [x] выполнен разнос `egress_identity_refresh.go`: runtime refresh/publish блок вынесен в `egress_identity_refresh_runner.go`, entry snapshot/state/sem helper'ы в `egress_identity_refresh_state.go`; базовый файл оставлен для queue/scope orchestration
- Step F1.11.70: [x] выполнен разнос `dns_settings_state.go`: upstream/pool persistence вынесены в `dns_settings_state_upstreams.go`, mode load/save в `dns_settings_state_mode.go`, smartdns env/systemd/normalize helper'ы в `dns_settings_state_smartdns.go`; DNS API-контракты и runtime-поведение сохранены
- Step F1.11.71: [x] выполнен разнос `traffic_audit.go`: проверки/парсинг nft-правил вынесены в `traffic_audit_checks.go`, pretty-render блока в `traffic_audit_render.go`, базовый файл оставлен как HTTP handler-orchestration
- Step F1.11.72: [x] выполнен разнос `routes_update_helpers.go`: list/io helper'ы вынесены в `routes_update_helpers_lists.go`, smartdns wildcard trace в `routes_update_helpers_trace.go`, fs/readiness helper'ы в `routes_update_helpers_fs.go`; базовый файл оставлен для table identifiers (`routesTableName/routesTableNum`)
- Step F1.11.73: [x] выполнена декомпозиция `types_traffic.go`: traffic mode/candidates DTO вынесены в `types_traffic_mode.go`, app-marks/app-profiles DTO вынесены в `types_traffic_apps.go`; JSON-контракты сохранены без изменений
- Step F1.11.74: [x] выполнен разнос `transport_handlers_actions.go`: action-routing оставлен в `transport_handlers_actions.go`, heavy action execution блоки (`health/metrics/provision/start-stop-restart`) вынесены в `transport_handlers_actions_exec.go` без изменения transport lifecycle/API поведения
- Step F1.11.75: [x] выполнен разнос `transport_handlers_policy_mutations.go`: thin wrappers оставлены в базовом файле, validate/apply/rollback execution вынесены в `transport_handlers_policy_mutations_{validate,apply,rollback}.go` без изменения policy-контрактов (`confirm token`, `snapshot/rollback`, `conflicts`)
- Step F1.11.76: [x] выполнен разнос `resolver_pipeline.go`: `buildResolverJobContext` вынесен в `resolver_pipeline_context.go`, базовый `resolver_pipeline.go` оставлен для execution pipeline (`planning -> batch -> artifacts -> summary -> precheck finalize`)
- Step F1.11.77: [x] выполнен разнос `resolver_dns_policy.go`: DNS attempt-policy helper'ы вынесены в `resolver_dns_attempt_policy.go`, cooldown runtime в `resolver_dns_cooldown.go`, precheck force flags в `resolver_precheck_flags.go`; базовый файл оставлен с типами policy/cooldown state
- Step F1.11.78: [x] выполнен разнос `trace_handlers.go`: read endpoints вынесены в `trace_handlers_read.go`, append/write helper'ы в `trace_handlers_write.go`, bounded tail reader в `trace_tail.go`; базовый `trace_handlers.go` оставлен как module doc/header
- Step F1.11.79: [x] выполнен разнос `transport_policy_validate.go`: normalization/CIDR helper'ы вынесены в `transport_policy_validate_normalize.go`, diff+conflict-dedupe блок в `transport_policy_validate_diff.go`; базовый файл оставлен для основной валидации ownership/overlap
- Step F1.11.80: [x] выполнен разнос `watchers.go`: bootstrap/start блок вынесен в `watchers_start.go`, file/hash watchers в `watchers_state_files.go`, runtime/systemd watchers в `watchers_runtime.go`; базовый `watchers.go` оставлен как module doc/header
- Step F1.11.81: [x] выполнен разнос `domains_handlers.go`: table endpoint вынесен в `domains_handlers_table.go`, file CRUD в `domains_handlers_file.go`, smartdns wildcard endpoints в `domains_handlers_smartdns.go`, observed-hosts helper в `domains_handlers_helpers.go`; базовый `domains_handlers.go` оставлен для shared mapping `domainFiles`
- Step F1.11.82: [x] выполнен разнос `egress_identity_probe.go`: external probe блок вынесен в `egress_identity_probe_external.go`, netns/interface/proxy probe блок в `egress_identity_probe_netns.go`, endpoint/socks/bind-address helper'ы в `egress_identity_probe_helpers.go`; базовый файл оставлен как модульный entrypoint
- Step F1.11.83: [x] выполнен разнос `traffic_appmarks_handlers.go`: POST mutation flow вынесен в `traffic_appmarks_handlers_post.go`, items list endpoint вынесен в `traffic_appmarks_handlers_items.go`, базовый handler оставлен для method routing + status response
- Step F1.11.84: [x] выполнен разнос `transport_handlers_netns.go`: per-client apply/restart/provision + response finalize вынесены в `transport_handlers_netns_apply.go`, базовый файл оставлен для HTTP endpoint + target resolution/orchestration
- Step F1.11.85: [x] выполнен разнос `transport_handlers_clients.go`: list/create endpoints вынесены в `transport_handlers_clients_list_create.go`, card get/patch/delete endpoints вынесены в `transport_handlers_clients_card_ops.go`, базовый handler оставлен для route-dispatch (`/transport/clients`, `/transport/clients/{id}`) без изменения API-контрактов
- Step F1.11.86: [x] выполнен разнос `smartdns_runtime.go`: config detect/apply/normalize вынесены в `smartdns_runtime_config.go`, state load/save/infer вынесены в `smartdns_runtime_state.go`, базовый `smartdns_runtime.go` оставлен для shared runtime model/constants + status snapshot
- Step F1.11.87: [x] выполнен разнос `traffic_mode_handlers.go`: POST apply-flow вынесен в `traffic_mode_handlers_apply.go`, advanced reset endpoint вынесен в `traffic_mode_handlers_advanced.go`, базовый `traffic_mode_handlers.go` оставлен для GET/interface/test endpoint-роутинга и lock helper'а
- Step F1.11.88: [x] выполнен разнос `routes_handlers.go`: status endpoint вынесен в `routes_handlers_status.go`, policy-fix endpoint вынесен в `routes_handlers_policy_fix.go`, async update endpoint вынесен в `routes_handlers_update.go`; базовый `routes_handlers.go` оставлен для service command wrapper `makeCmdHandler`
- Step F1.11.89: [x] выполнен разнос `dns_smartdns_handlers.go`: runtime endpoint (`GET/POST /dns/smartdns/runtime`) вынесен в `dns_smartdns_handlers_runtime.go`, prewarm endpoint + helper'ы вынесены в `dns_smartdns_handlers_prewarm.go`, базовый файл оставлен как module header
- Step F1.11.90: [x] выполнен разнос `traffic_appmarks_runtime.go`: state/prune helper'ы вынесены в `traffic_appmarks_runtime_state.go`, restore sequence вынесен в `traffic_appmarks_runtime_restore.go`, базовый runtime-файл оставлен для NFT adapters/config helper'ов
- Step F1.11.91: [x] выполнен разнос `dns_settings.go`: upstream endpoints вынесены в `dns_settings_handlers_upstreams.go`, mode/status handlers вынесены в `dns_settings_handlers_mode.go`, smartdns service handlers вынесены в `dns_settings_handlers_smartdns_service.go`; базовый файл оставлен как module header
- Step F1.11.92: [x] выполнен разнос `autoloop_helpers.go`: login-state/parser helper'ы вынесены в `autoloop_helpers_login.go`, location spec/ISO resolve helper'ы вынесены в `autoloop_helpers_location.go`, базовый файл оставлен для shared `autoloopLocationSpec` и email regexp
- Step F1.11.93: [x] выполнен разнос `traffic_app_profiles.go`: store/conversion/sanitize helper'ы вынесены в `traffic_app_profiles_store.go`, базовый файл оставлен для HTTP handler `/traffic/app-profiles` и package-level store init
- Step F1.11.94: [x] выполнен разнос `routes_handlers_ops.go`: core lock/clear/status snapshot логика вынесена в `routes_handlers_ops_core.go`, базовый `routes_handlers_ops.go` оставлен для rollback/cache-restore/precheck-debug endpoint-слоя
- Step F1.11.95: [x] выполнен разнос `transport_singbox_profiles_state.go`: state load/save вынесены в `transport_singbox_profiles_state_store.go`, profile/state normalize + active-id/derive-id helper'ы вынесены в `transport_singbox_profiles_state_normalize.go`, базовый файл оставлен для shared primitive helper'ов (`mode/schema/int64/cloneMap`)
- Step F1.11.96: [x] выполнен разнос `transport_singbox_profiles_runtime.go`: client/profile selection helper'ы вынесены в `transport_singbox_profiles_runtime_select.go`, preflight render/write flow вынесен в `transport_singbox_profiles_runtime_prepare.go`, backend provision/start/restart runtime flow вынесен в `transport_singbox_profiles_runtime_apply.go`; базовый файл оставлен как module header
- Step F1.11.97: [x] выполнен разнос `transport_handlers_actions_exec.go`: health/metrics handlers вынесены в `transport_handlers_actions_exec_health.go`, provision handler вынесен в `transport_handlers_actions_exec_provision.go`, lifecycle handler (`start/stop/restart`) вынесен в `transport_handlers_actions_exec_lifecycle.go`; базовый файл оставлен как module header
- Step F1.11.98: [x] выполнен разнос `routes_update_stage_resolve.go`: доменное расширение/каппинг/wildcard trace вынесены в `routes_update_stage_resolve_domains.go`, bootstrap временных файлов вынесен в `routes_update_stage_resolve_tempfiles.go`; базовый stage-файл оставлен для resolver orchestration + runtime wildcard merge + artifact writes
- Step F1.11.99: [x] выполнен разнос `transport_backends.go`: transportcfg wrapper/helper слой вынесен в `transport_backends_config_helpers.go`, unsupported runtime backend methods вынесены в `transport_backends_runtime_unsupported.go`, базовый файл оставлен для core backend types/constants + backend selection
- Step F1.11.100: [x] выполнен разнос `transport_singbox_dns_migration.go`: migration entry/settings/path helper'ы оставлены в базовом файле, apply flow вынесен в `transport_singbox_dns_migration_apply.go`, DNS map conversion вынесен в `transport_singbox_dns_migration_map.go`
- Step F1.11.101: [x] выполнен разнос `traffic_appmarks_ops.go`: status summary helper оставлен в базовом файле, mutation-операции (`add/delete/clear`) вынесены в `traffic_appmarks_ops_mutations.go`
- Step F1.11.102: [x] выполнен разнос `routes_cache_helpers.go`: file io/helper'ы вынесены в `routes_cache_helpers_files.go`, route parse/restore в `routes_cache_helpers_routes.go`, nft snapshot/parser в `routes_cache_helpers_nft.go`, meta helpers в `routes_cache_helpers_meta.go`; базовый файл оставлен как module header
- Step F1.11.103: [x] выполнен разнос `transport_client_runtime_alloc_normalize.go`: reconcile marks/prefs allocation блок вынесен в `transport_client_runtime_alloc_reconcile.go`, базовый normalize-файл оставлен для state canonicalization и deterministic client merge
- Step F1.11.104: [x] выполнен разнос `dns_settings_benchmark.go`: HTTP benchmark endpoint вынесен в `dns_settings_benchmark_handler.go`, базовый файл оставлен для benchmark normalize/run/topN helper'ов и package aliases
- Step F1.11.105: [x] выполнен разнос `transport_singbox_profiles_card.go`: route dispatch + card method switch оставлены в базовом файле, `GET` вынесен в `transport_singbox_profiles_card_get.go`, `PATCH` в `transport_singbox_profiles_card_patch.go`, `DELETE` в `transport_singbox_profiles_card_delete.go`
- Step F1.11.106: [x] выполнен разнос `types_singbox.go`: профильные DTO вынесены в `types_singbox_profiles.go`, validate/render/apply/rollback DTO вынесены в `types_singbox_flow.go`, history DTO вынесены в `types_singbox_history.go`; базовый файл оставлен как module header
- Step F1.11.107: [x] выполнен разнос `routes_cache.go`: save snapshot flow вынесен в `routes_cache_save.go`, restore flow вынесен в `routes_cache_restore.go`, базовый файл оставлен для shared cache meta type
- Step F1.11.108: [x] выполнен разнос `transport_tokens_state.go`: clients state load/save вынесены в `transport_tokens_state_clients.go`, policy state/snapshot load/save вынесены в `transport_tokens_state_policy.go`, conflicts state load/save вынесены в `transport_tokens_state_conflicts.go`; базовый файл оставлен для token/digest helper'ов
- Step F1.11.109: [x] выполнен разнос `routes_handlers_service.go`: routes service action handler/helper'ы вынесены в `routes_handlers_service_action.go`, timer handlers/helper'ы вынесены в `routes_handlers_service_timer.go`, базовый файл оставлен как module header
- Step F1.11.110: [x] выполнен разнос `vpn_login_session_handlers.go`: login state helper вынесен в `vpn_login_session_handlers_state_helper.go`, start/state/action/stop endpoints вынесены в `vpn_login_session_handlers_{start,state,action,stop}.go`, базовый файл оставлен как module header
- Step F1.11.111: [x] выполнен разнос `resolver_bridge_pipeline.go`: planning/runtime tuning wrappers вынесены в `resolver_bridge_pipeline_planning.go`, execution/recheck/artifacts wrappers вынесены в `resolver_bridge_pipeline_exec.go`, базовый файл оставлен как module header
- Step F1.11.112: [x] выполнен разнос `transport_singbox_profiles_flow_rollback.go`: HTTP wrapper + decode body вынесены в базовый файл, rollback execution flow вынесен в `transport_singbox_profiles_flow_rollback_exec.go` без изменения кодов/контрактов endpoint `POST /api/v1/transport/singbox/profiles/{id}/rollback`
- Step F1.11.113: [x] выполнен stage-разнос `transport_singbox_profiles_flow_apply_exec.go`: preflight/validate/render и dispatch оставлены в базовом execution-файле, target-config/runtime/commit вынесены в `transport_singbox_profiles_flow_apply_exec_rendered.go` без изменения API-контрактов `POST /api/v1/transport/singbox/profiles/{id}/apply`
- Step F1.11.114: [x] выполнен stage-разнос `transport_backends_adapter_systemd_action.go`: lifecycle action orchestration оставлена в базовом файле, pre/post hooks вынесены в `transport_backends_adapter_systemd_action_hooks.go`, unit execution + auto-provision path вынесены в `transport_backends_adapter_systemd_action_exec.go` (без изменения backend/runtime контрактов)
- Step F1.11.115: [x] выполнен stage-разнос `transport_singbox_profiles_flow_rollback_exec.go`: prepare/select/restore шаг оставлен в базовом execution-файле, runtime+commit/history шаг вынесен в `transport_singbox_profiles_flow_rollback_exec_restored.go` без изменения API-контракта rollback endpoint
- Step F1.11.116: [x] выполнен разнос `transport_singbox_profiles_flow_render.go`: HTTP wrapper + body decode оставлены в базовом файле, render execution flow вынесен в `transport_singbox_profiles_flow_render_exec.go` без изменения API-контракта endpoint `POST /api/v1/transport/singbox/profiles/{id}/render`
- Step F1.11.117: [x] выполнен stage-разнос `routes_update_stage_resolve.go`: wildcard runtime merge вынесен в `routes_update_stage_resolve_wildcard_runtime.go`, запись resolver artifacts вынесена в `routes_update_stage_resolve_artifacts.go`, базовый stage-файл оставлен orchestration-слоем (без изменения routes-update поведения)
- Step F1.11.118: [x] выполнен stage-разнос `transport_backends_adapter_systemd_provision.go`: input/preflight build вынесен в `transport_backends_adapter_systemd_provision_inputs.go`, unit write + daemon-reload/enable finalize вынесены в `transport_backends_adapter_systemd_provision_finalize.go`, базовый `Provision()` оставлен orchestration-слоем без изменения runtime-контрактов
- Step F1.11.119: [x] выполнен разнос `transport_client_runtime_state.go`: client create/kind/capabilities вынесены в `transport_client_runtime_create.go`, shared helpers (`find index`, `cloneMap`) вынесены в `transport_client_runtime_helpers.go`, базовый state-файл оставлен для runtime snapshot/normalize/uptime helper'ов
- Step F1.11.120: [x] выполнен разнос `traffic_appmarks_ops_mutations.go`: mutation flow разделён на role-файлы `traffic_appmarks_ops_mutations_add.go`, `traffic_appmarks_ops_mutations_delete.go`, `traffic_appmarks_ops_mutations_clear.go`; базовый файл оставлен как module header без изменения appmarks API/runtime поведения
- Step F1.11.121: [x] выполнен stage-разнос `transport_handlers_actions_exec_lifecycle.go`: HTTP lifecycle handler оставлен в базовом файле, preflight flow вынесен в `transport_handlers_actions_exec_lifecycle_preflight.go`, locked execution/save/response flow вынесен в `transport_handlers_actions_exec_lifecycle_locked.go` без изменения lifecycle API-контракта
- Step F1.11.122: [x] выполнен stage-разнос `dns_settings_state_upstreams.go`: pool store/load/save вынесены в `dns_settings_state_upstreams_pool_store.go`, conf file load/save вынесены в `dns_settings_state_upstreams_conf_store.go`, базовый upstream-state файл оставлен для conversion/normalize/load-enabled и legacy facade методов
- Step F1.11.123: [x] выполнен stage-разнос `traffic_appmarks_handlers_post.go`: request decode/validate вынесен в `traffic_appmarks_handlers_post_parse.go`, op-specific execution вынесен в `traffic_appmarks_handlers_post_ops.go`, базовый POST handler оставлен как thin dispatch без изменения API-контракта
- Step F1.11.124: [x] выполнен stage-разнос `traffic_audit_checks.go`: duplicate-detector helper'ы вынесены в `traffic_audit_duplicates.go`, NFT appmark rule parse helper'ы вынесены в `traffic_audit_nft_parse.go`, базовый checks-файл оставлен для audit-orchestration (`auditNftAppMarks`) без изменения audit API/вывода
- Step F1.11.125: [x] зафиксирован stop-condition декомпозиции Go-ядра: max non-test файл в `selective-vpn-api/app` = `195` строк; дальнейший разнос отложен как low-priority polish
- Step F1.12: [x] выполнить пакетную декомпозицию `selective-vpn-gui/api_client.py` в `selective-vpn-gui/api/*` с сохранением legacy facade `api_client.py`
- Step F1.13: [~] выполнить декомпозицию `selective-vpn-api/app/traffic_mode.go` на подпакеты `app/trafficmode/*` без изменения policy-routing поведения
- Step F1.13.1: [x] вынесены normalize helper'ы (`tokenize/subnet/uid/cgroup`) в `app/trafficmode/normalize.go`, в `app/traffic_mode.go` оставлены фасады
- Step F1.13.2: [x] вынесен parser auto-local-bypass маршрутов в `app/trafficmode/autolocal.go` (`ParseAutoBypassRoutes` + helper'ы), в `app/traffic_mode.go` сохранены совместимые фасады для `traffic_candidates`
- Step F1.13.3: [x] вынесен cgroup->uid resolution блок в `app/trafficmode/cgroup.go` (`CgroupCandidates/ResolveCgroupPath/CollectPIDsFromCgroup/UIDRangeForPID/ResolveCgroupUIDRanges`), в `app/traffic_mode.go` сохранены фасады
- Step F1.13.4: [x] вынесен ingress-reply bypass nft-контур в `app/trafficmode/ingress.go` (`Ensure/Flush/Enable/Disable/Active`) с сохранением фасадов в `app/traffic_mode.go`
- Step F1.13.5: [x] вынесен route-rules/probe блок в `app/trafficmode/rules.go` (`ReadRules/DetectAppliedMode/ProbeMode/PrefStr`) с сохранением фасадов и `trafficRulesState` alias в `app/traffic_mode.go`
- Step F1.13.6: [x] удалены лишние внутренние wrapper-функции `traffic_mode.go` (cgroup/nftObjectMissing), `buildEffectiveOverrides` переведён на прямые вызовы `trafficmodepkg.ResolveCgroupUIDRanges`
- Step F1.13.7: [x] вынесен `ip rule` apply/remove блок в `app/trafficmode/apply.go` (`RemoveRulesForTable`, `ApplyRule`, `ApplyOverrides`) с сохранением фасадов в `app/traffic_mode.go`
- Step F1.13.8: [x] вынесен interface selection/resolve блок в `app/trafficmode/interfaces.go` (`NormalizePreferredIface`, `IfaceExists`, `ListUpIfaces`, `ListSelectableIfaces`, `ResolveTrafficIface`, `IsVPNLikeIface`), `app/traffic_mode.go` переведён на thin-facade
- Step F1.13.9: [x] вынесены HTTP/lock handlers traffic-mode в `app/traffic_mode_handlers.go` (`mode get/post`, `advanced reset`, `interfaces`, `mode test`, apply-lock); `app/traffic_mode.go` сокращён до core policy/apply/evaluate блока
- Step F1.13.10: [x] вынесены state/normalization/persistence helper'ы traffic-mode в `app/traffic_mode_state.go` (`normalize*`, `load/save/infer state`), `app/traffic_mode.go` оставлен для routing apply/evaluate orchestration
- Step F1.13.11: [x] завершена декомпозиция `traffic_mode` core: вынесены `iface/config` helper'ы в `app/traffic_mode_iface.go`, apply-orchestration в `app/traffic_mode_apply.go`, runtime-check/evaluate в `app/traffic_mode_evaluate.go`; `app/traffic_mode.go` оставлен с константами/описанием модуля
- Step F3: [ ] (deferred) выделить собственную библиотеку модулей Go-ядра (linux-first), чтобы backend собирался из переиспользуемых пакетов
- Step F3.1: [ ] вынести `netns` как отдельный debug-модуль библиотеки (`pkg/netnsdebug` / `pkg/netns`), зафиксировать Linux-only scope
- Step F3.2: [ ] зафиксировать, что `netns` модуль не является целью для mobile SDK (iOS/Android out-of-scope), используется для backend/desktop test-contour
- Step F3.2.1: [ ] не реализовывать `netns` интеграцию для mobile-клиентов; для iOS/Android использовать только API control-plane без system/netns модулей
- Step F3.3: [ ] выделить стабильные библиотечные пакеты ядра (`pkg/orchestrator`, `pkg/transport`, `pkg/pbr`, `pkg/resolver`) с явными интерфейсами зависимостей
- Step F3.4: [ ] перевести backend на использование этих пакетов через thin-facade слой `app/*` без изменения внешнего API-контракта
- Step F3.5: [ ] подготовить отдельный документ архитектуры библиотеки модулей и runbook миграции (`docs/phase-f/F3_CORE_MODULE_LIBRARY_PLAN.md`)
- Step F1.14: [~] выполнить декомпозицию `selective-vpn-api/app/egress_identity.go` на подпакеты `app/egress*` без изменения egress API/SWR-поведения
- Step F1.14.1: [x] вынесены egress utility helper'ы в `app/egressutil/util.go` (parse ip/geo, endpoint lists, curl/wget path, socks url parse, URL host resolve, generic any/string helpers)
- Step F1.14.2: [x] удалены лишние локальные egress helper-функции в `app/egress_identity.go` (dead wrappers), оставлены только необходимые facade entrypoints
- Step F1.14.3: [x] вынесены helper'ы scope/identity/probe-loop в `app/egressutil` (`ParseScope`, `IdentityChanged`, `ProbeFirstSuccess`) с сохранением app-facade функций
- Step F1.14.4: [x] вынесен HTTP fetch helper в `app/egressutil/http.go` (`HTTPGetBody`), egress probe/geo вызовы в `app/egress_identity.go` переведены на пакетный вызов
- Step F1.14.5: [x] удалены чистые egress wrapper-функции в `app/egress_identity.go` (parse geo/ip, country normalize, curl/wget/timeout/resolve helpers); app-тесты переведены на прямые вызовы `app/egressutil`
- Step F1.15: [~] выполнить декомпозицию `selective-vpn-api/app/dns_settings.go` на подпакеты `app/dnscfg/*` без изменения DNS API/SmartDNS поведения
- Step F1.15.1: [x] вынесены SmartDNS addr/upstream normalizers в `app/dnscfg/smartdns.go` (`ResolveDefaultSmartDNSAddr`, `SmartDNSAddrFromConfig`, `NormalizeDNSUpstream`, `NormalizeSmartDNSAddr`) с сохранением фасадов в `app/dns_settings.go`
- Step F1.15.2: [x] вынесен SmartDNS systemd helper-блок в `app/dnscfg/systemd.go` (`UnitState`, `RunUnitAction`) с сохранением фасада `runSmartdnsUnitAction` и прежнего success-message формата
- Step F1.15.3: [x] вынесен DNS benchmark engine в `app/dnscfg/benchmark.go` (`NormalizeProfile/Options/Upstreams/Domains`, `BenchmarkDNSUpstream`, `DNSLookupAOnce`, `BenchmarkTopN`) с сохранением HTTP/API-контракта в `app/dns_settings.go`
- Step F1.15.4: [x] вынесены pool helper'ы в `app/dnscfg/pool.go` (`NormalizeUpstreamPoolItems`, `UpstreamPoolFromLegacy`, `UpstreamPoolToLegacy`, `EnabledPool`) с сохранением фасадов в `app/dns_settings.go`
- Step F1.15.5: [x] вынесен mode-state storage в `app/dnscfg/mode.go` (`ModeConfig`, `ModeState`, `LoadMode`, `SaveMode`) с сохранением JSON-контракта `dns_mode.json`
- Step F1.15.6: [x] очищен `app/dns_settings.go` от лишних benchmark-wrapper'ов (прямые вызовы `dnscfg`), без изменения endpoint/DTO
- Step F1.15.7: [x] вынесены parse/normalize/render helper'ы `dns-upstreams.conf` в `app/dnscfg/upstreams.go` (`ParseUpstreamsConf`, `NormalizeUpstreams`, `RenderUpstreamsConf`) с сохранением файлового/JSON mirror поведения
- Step F1.15.8: [x] вынесен SmartDNS prewarm engine в `app/dnscfg/prewarm.go` (`RunPrewarm` + metrics/domain expansion/manual merge/log pipeline) через dependency-injection callbacks
- Step F1.15.9: [x] runtime/prewarm HTTP handlers вынесены из `app/dns_settings.go` в `app/dns_smartdns_handlers.go``dns_settings.go` оставлен DNS settings facade слой)
- Step F2.1: [x] выполнен production-upgrade `SingBox` runtime на шаблонный `systemd` unit `singbox@.service` (instance-per-profile без генерации отдельного unit-файла на каждый профиль)
- Step F2.2: [x] после `F2.1` внедрён миграционный и cleanup-контур (`old per-profile units -> template instances`, idempotent remove/disable/reset-failed, сохранение совместимости API)
- Step F2.1.1: [x] добавлен template unit `singbox@.service` + drop-in env strategy (`SVPN_TRANSPORT_ID`, `SVPN_CONFIG_PATH`, `SVPN_NETNS_*`) и зафиксированы hardening/tuning defaults
- Step F2.1.2: [x] backend renderer/provision переведён на instance model (`singbox@<profile_id>.service`) без изменения API-контракта `transport/clients/*`
- Step F2.1.3: [x] lifecycle/health/metrics path переведён на instance model (`start/stop/restart/is-active/reset-failed`) с сохранением runtime-code/ошибок
- Step F2.1.4: [x] добавлены e2e/smoke проверки instance model (`create/start/switch/stop/delete`) включая netns-enabled профили и auto-provision fallback
- Step F2.2.1: [x] реализован one-shot migrator `legacy unit -> template instance` (deterministic mapping + marker/ownership check + dry-run)
- Step F2.2.2: [x] реализован cleanup legacy artifacts (`disable/stop/reset-failed/remove unit file + daemon-reload`) с idempotent повторным запуском
- Step F2.2.3: [x] добавлен rollback-план миграции (`template -> legacy`) для инцидентов деплоя и зафиксирован runbook
- Step F2.2.4: [x] обновлены docs/scripts деплоя и preflight-check для новой модели (`singbox@.service` обязательный runtime dependency)
- Step F1.3: [x] выполнить декомпозицию `selective-vpn-gui/dashboard_controller.py` на domain-контроллеры с сохранением фасада `DashboardController`
- Step F1.4: [x] выполнить декомпозицию `selective-vpn-gui/vpn_dashboard_qt.py` на `main_window/*` mixin-модули (без изменения поведения)
- Step Z1: [ ] (experimental tail) глобальный L7 orchestration-layer поверх текущего ядра для multi-engine (`AdGuardVPN` / `SingBox` / future transports) с единым policy-adapter слоем
- Step Z1.1: [~] ранняя подготовка разрешена до старта Z1 runtime: единая policy-модель/intent ownership, scope-нормализация, telemetry schema и compatibility matrix; без включения L7 data-plane в прод-путь
## Что уже сделано
- 2026-03-20: закрыт F2.2.1 (one-shot migrator legacy unit -> template instance) для SingBox systemd path:
- в pre-action hook (start/restart) добавлен best-effort мигратор legacy unit singbox-<id>.service -> template instance singbox@<id>.service;
- миграция использует deterministic candidate mapping (instance-id/unit + client-id sanitize), ownership-check по marker SVPN_TRANSPORT_ID и не трогает foreign unit-файлы;
- добавлен dry-run режим через config.singbox_legacy_unit_migrate_dry_run=true (без stop/disable/remove), плюс флаг отключения config.singbox_legacy_unit_migrate=false;
- после фактической миграции выполняются daemon-reload + reset-failed для legacy unit (best-effort, idempotent).
- cleanup-path Cleanup() для template-instance расширен: удаляются и owned legacy unit-файлы singbox-<id>.service (stop/disable/remove + daemon-reload/reset-failed), template unit singbox@.service сохраняется.
- добавлены тесты transport_systemd_singbox_template_test.go: migrate owned legacy, dry-run, ownership mismatch; локально go test ./... (ok).
- 2026-03-20: закрыт E3.3 (interface orchestrator + M3 owner-scope):
- compile-plan перешёл на owner-scoped nft naming: `owner_scope=iface+client`, `nft_set=agvpn_pi_<owner_scope>_<selector>`;
- для длинных `iface/client` добавлен deterministic hash fallback, длина set name ограничена 63 символами;
- `TransportPolicyCompileRule/Set` расширены полем `owner_scope` для observability/runtime debug;
- добавлены тесты: `TestCompileTransportPolicyPlanUsesOwnerScopedNftSets`, `TestTransportPolicyNftSetNameDeterministicAndBounded`; локально `go test ./...` (ok).
- 2026-03-20: закрыт E3.5 domain destination-lock coverage:
- `detectTransportDestinationLockConflicts` расширен на `domain` selector (включая `*.domain`), теперь owner-switch проверяет sticky-lock по `domain-cache` (`direct+wildcard`) без сетевых запросов;
- добавлен `domain-cache` bridge hook в policy guard (`transportPolicyLoadDomainCacheState`, path: `/var/lib/selective-vpn/domain-cache.json`), с lazy-load и per-selector memoization;
- добавлены тесты: `TestDetectTransportDestinationLockConflictsDomainFromCache`, `TestDetectTransportDestinationLockConflictsWildcardDomainFromCache`, `TestDetectTransportDestinationLockConflictsSkipsDomainWithoutCacheHit`; локально `go test ./...` (ok).
- 2026-03-13: уменьшен trace-spam в transport-контуре:
- добавлен rate-limited trace helper (`appendTraceLineRateLimited`) с дедупом повторяющихся строк в окне времени;
- на throttled-логирование переведены шумные ветки (`singbox dns migrate warning`, `bootstrap bypass failed`, `netns setup/cleanup failed`, `systemctl unit missing/reset-failed`);
- повторяющиеся сообщения теперь схлопываются с компактной `trace dedup: suppressed=...` строкой.
- 2026-03-13: стартовал `F2.1` (SingBox instance-model foundation):
- в `transportcfg.BackendUnit` для `kind=singbox` добавлен резолв unit в `singbox@<client_id>.service`;
- в прод-коде отключён auto-map legacy unit-имён `sing-box.service` / `singbox-*.service`;
- для `kind=singbox` включена жёсткая нормализация `config.unit=singbox@.service` на create/patch/state-save;
- добавлены/обновлены тесты `app/transportcfg/runtime_helpers_test.go` (default/template/custom/sanitize).
- 2026-03-13: продолжен `F2.1` (template/drop-in provisioning для SingBox):
- `systemd` provision-path для `singbox@<id>.service` переведён на модель `template + per-instance drop-in`;
- добавлена генерация/обновление `singbox@.service` (managed template) и drop-in `singbox@<id>.service.d/10-selective-vpn.conf`;
- в drop-in пробрасываются env-поля `SVPN_TRANSPORT_ID`, `SVPN_TRANSPORT_KIND`, `SVPN_CONFIG_PATH`, `SVPN_NETNS_*`;
- cleanup для template-instance теперь удаляет клиентский drop-in (с ownership-check) и не удаляет общий template unit;
- добавлены e2e-like unit tests: `transport_systemd_singbox_template_test.go` (provision, auto-provision-on-missing, cleanup-keep-template);
- обновлены runtime dependency docs/script: добавлен check `singbox@.service` (legacy `sing-box.service` сохранён как compat).
- 2026-03-13: завершён cleanup legacy unit-контур для SingBox:
- state `/var/lib/selective-vpn/transport-clients.json` мигрирован на `config.unit=singbox@.service` для всех `kind=singbox` профилей;
- legacy unit-файлы `singbox-*.service` удалены из `/etc/systemd/system`, оставлен только `singbox@.service` + instance drop-in;
- после сборки/рестарта API прогнаны проверки: `go test ./...` (ok), `systemctl list-unit-files singbox*` (только template), runtime start через `singbox@<id>.service` (ok).
- 2026-03-13: расширены smoke/e2e проверки под instance-model:
- `tests/transport_systemd_real_e2e.py` обновлён: SingBox проверяется через default instance unit (`singbox@<client_id>.service`) + ownership в drop-in + сохранение `singbox@.service` после delete;
- `tests/transport_production_like_e2e.py` обновлён: production-like SingBox-кейс переведён на instance model (проверка template/drop-in артефактов и cleanup);
- добавлен unit-test на netns-instance drop-in env (`SVPN_NETNS_ENABLED`, `SVPN_NETNS_NAME`) в `transport_systemd_singbox_template_test.go`;
- прогон локальных проверок: `go test ./...` (ok), `python3 -m py_compile` для обновлённых e2e-скриптов (ok).
- 2026-03-12: продолжен рефакторинг `F1.11/F1.13` без изменения API-контрактов:
- в `app/transportcfg` добавлен `secrets_helpers.go` (нормализация/маскирование/сравнение secret-map + файловые JSON helper'ы);
- `app/transport_singbox_profiles.go` переведён на `transportcfg` secrets-helper'ы; удалены локальные дубли `normalizeSingBoxSecretUpdates`, `maskSingBoxSecrets`, `cloneStringMap`, `equalStringMap`;
- `app/transport_singbox_profiles_flow.go` дополнительно очищен от локальных wrapper-функций `config io/file optional/diff`; вызовы переведены на прямой `transportcfg`;
- state/normalization функции `singbox profiles` вынесены из `app/transport_singbox_profiles.go` в `app/transport_singbox_profiles_state.go` (декомпозиция без изменения DTO/handlers);
- функции secrets patch/store + error helpers `singbox profiles` вынесены в отдельные файлы (`transport_singbox_profiles_secrets.go`, `transport_singbox_profiles_errors.go`);
- create/patch mutate-логика `singbox profiles` вынесена в `transport_singbox_profiles_mutate.go`, а основной `transport_singbox_profiles.go` сокращён до `413` строк;
- runtime/history helper'ы `singbox profile flow` вынесены в `transport_singbox_profiles_runtime.go` и `transport_singbox_profiles_history.go`;
- eval/render helper'ы `singbox profile flow` вынесены в `transport_singbox_profiles_eval.go`;
- state/normalization helper'ы `transport client runtime` вынесены в `transport_client_runtime_state.go`, а `transport_client_runtime.go` сокращён до orchestration/lifecycle части;
- health/netns helper'ы `transport client runtime` вынесены в `transport_client_runtime_health.go` и `transport_client_runtime_netns.go`;
- `transport_handlers_clients.go` декомпозирован: netns-toggle и action-dispatch вынесены в отдельные файлы `transport_handlers_netns.go`/`transport_handlers_actions.go` без изменения endpoint-контрактов;
- helper-блок `routes_update.go` вынесен в `routes_update_helpers.go` (table/list/io/counter/fs/readiness), основной `routes_update.go` оставлен как orchestration-пайплайн;
- helper/state блок `dns_settings.go` вынесен в `dns_settings_state.go` (upstream pool + mode/smartdns helpers + dnscfg adapters), handler-файл оставлен как control-plane API фасад;
- DNS/SmartDNS типы вынесены из `types.go` в `types_dns.go` (контракты API/JSON не менялись);
- `types.go` дополнительно разрезан на доменные файлы (`types_traffic.go`, `types_egress.go`, `types_transport.go`, `types_singbox.go`) с сохранением совместимости по полям/тегам;
- `transport_handlers_policy.go` декомпозирован: мутационные endpoints (`validate/apply/rollback`) вынесены в `transport_handlers_policy_mutations.go`;
- `handleTransportHealthRefresh` вынесен в `transport_handlers_health_refresh.go`, а `transport_health_refresh.go` оставлен runtime-блоком фоновых health probe;
- из `egress_identity.go` вынесен probe/helper блок в `egress_identity_probe.go` (system/iface/netns/proxy probes + endpoint helpers + socks/iface bind helpers);
- из `routes_handlers.go` вынесен service/timer блок в `routes_handlers_service.go` (systemd service action, timer get/toggle/set);
- из `routes_handlers.go` вынесен clear/precheck/op-lock блок в `routes_handlers_ops.go` (clear/cache-restore/precheck-debug/status-snapshot/op-lock helpers);
- `egress_identity` endpoint handlers (`GET/refresh`) вынесены в `egress_identity_handlers.go`, основной файл оставлен как service/runtime orchestration;
- монолит `traffic_appmarks.go` разнесён по ролям на `traffic_appmarks_handlers.go` (HTTP), `traffic_appmarks_ops.go` (add/del/clear/status) и `traffic_appmarks_runtime.go` (nft/cgroup/state/restore), базовый файл оставлен с core-константами и alias-типами;
- `transport_singbox_profiles_flow.go` сокращён с `1336` до `893` строк без изменения API-контрактов;
- в `app/trafficmode` добавлен `interfaces.go` (iface detect/select/resolve), а `app/traffic_mode.go` переведён на thin-facade для этого блока;
- handlers/apply-lock блок `traffic mode` вынесен в `app/traffic_mode_handlers.go` без изменения endpoint-контрактов (`/api/v1/traffic/mode`, `/traffic/interfaces`, `/traffic/mode/test`, `/traffic/advanced/reset`);
- state/normalization/persistence блок `traffic mode` вынесен в `app/traffic_mode_state.go`, основной `traffic_mode.go` очищен до apply/evaluate orchestration;
- завершён разнос remaining-core `traffic mode`: `traffic_mode_iface.go` (iface/table/config helpers), `traffic_mode_apply.go` (build/apply overrides + route base + apply mode), `traffic_mode_evaluate.go` (rules read/probe/evaluate health);
- handlers/ops/runtime блок `traffic app marks` разнесён на отдельные файлы (`traffic_appmarks_handlers.go`, `traffic_appmarks_ops.go`, `traffic_appmarks_runtime.go`), core-файл `traffic_appmarks.go` сокращён до констант/типов (`34` строки);
- размеры крупных файлов дополнительно снижены: `traffic_mode.go` `820 -> 35`;
- helper-блок `routes cache` вынесен в `routes_cache_helpers.go`, основной `routes_cache.go` сокращён до orchestration restore/save пайплайна;
- `egress identity` разнесён на файлы ролей: `providers`, `refresh`, `geo`, `scope`; базовый `egress_identity.go` сокращён до констант/типов/bootstrap;
- `vpn_handlers.go` (`531` строка) разложен на отдельные роли (`auth/status/locations`) с сохранением endpoint-path и прежнего поведения location switch / autoconnect;
- `autoloop.go` очищен от внутренних helper closure-блоков; helper-логика вынесена в `autoloop_helpers.go`, loop orchestration оставлен неизменным по таймингам и reconnect-пайплайну;
- `transport_backends_adapter_systemd.go` разложен на role-файлы (`action/health/provision/cleanup`) без изменения lifecycle/runtime контрактов backend `systemd`;
- выполнен stage-разнос `routes_update.go`: preflight/policy/nft/resolve/artifacts вынесены в `routes_update_stage_*.go`, основной файл оставлен orchestration-фасадом;
- `vpn_login_session.go` разложен на manager/PTY и HTTP handlers: endpoint-обработчики вынесены в `vpn_login_session_handlers.go` без изменения API login session;
- `vpn_locations_cache.go` разложен на role-файлы (`refresh`, `parse`, `store`) без изменения SWR-контракта (`stale/refresh_in_progress/next_retry_at`) и endpoint поведения;
- `transport_health_refresh.go` разложен на role-файлы (`state`, `queue`, `probe`, `compare`) без изменения фонового health-refresh контракта (`fresh TTL`, `backoff`, `max parallel probes`, `persist min age`);
- `transport_client_runtime_alloc.go` разложен на role-файлы (`normalize/reconcile`, `table/mark/pref slots`) без изменения allocation-контракта (`MarkHex`, `PriorityBase`, `RoutingTable`, duplicate-id winner);
- `transport_netns.go` разложен на role-файлы (`spec`, `rules`, `run`) без изменения netns lifecycle-контракта (`ensure`, `cleanup`, nft NAT comment tags, policy-route update);
- `transport_bootstrap_bypass.go` разложен на role-файлы (`candidates`, `resolve`, `routes`, `state`) без изменения bypass-контракта (`start/restart sync`, `stop cleanup`, strict mode, bootstrap state persistence);
- `transport_singbox_profiles_flow.go` разложен на handler role-файлы (`validate`, `render`, `apply`, `rollback`, `history`) с сохранением endpoint-контрактов `/api/v1/transport/singbox/profiles/{id}/{action}`;
- `transport_singbox_profiles.go` разложен на role-файлы (`list`, `card`, `features`) с сохранением endpoint-контрактов `/api/v1/transport/singbox/profiles*`;
- benchmark-контур DNS вынесен из `dns_settings.go` в `dns_settings_benchmark.go` с сохранением API-контракта `POST /api/v1/dns/benchmark` и профилей `quick/load`;
- `resolver_bridge.go` разложен на role-файлы (`utils`, `cache`, `dns`, `pipeline`) без изменения bridge-контракта с подпакетом `app/resolver`;
- `transport_backends.go` разложен на role-файлы (`selection/config facade`, `probe adapters`, `runtime-mode backends`) без изменения backend action/health/provision/cleanup контрактов;
- `vpn_login_session.go` разложен на role-файлы (`models/manager`, `state methods`, `pty lifecycle`, `http handlers`) без изменения login-session endpoint-контрактов;
- размеры крупных файлов снижены: `traffic_mode.go` `923 -> 820`, `transport_singbox_profiles.go` `979 -> 897`, `transport_singbox_profiles_flow.go` `1372 -> 1336`;
- после изменений выполнены `go test ./...` и `go build ./...` — успешно.
- 2026-03-11: продолжен шаг `F1.11` (поэтапный вынос transport в подпакеты):
- добавлен новый подпакет `selective-vpn-api/app/transportcfg` и файл `runtime_helpers.go`;
- в подпакет перенесены runtime/config helper'ы transport backend:
- `ConfigString`, `ConfigBool`;
- `RuntimeMode`;
- `BackendUnit`/`DefaultBackendUnit`;
- `DNSTTSSHTunnelEnabled`, `DNSTTSSHUnit`;
- `SystemdActionUnits`, `SystemdHealthUnits`;
- добавлен `selective-vpn-api/app/transportcfg/systemd_helpers.go` и в подпакет перенесены systemd helper'ы transport backend:
- unit-name/path/ownership/file-write helper'ы;
- systemd unit renderer'ы (`primary` + `ssh-overlay`);
- service tuning/hardening нормализация и prefix-fallback parser'ы;
- добавлен `selective-vpn-api/app/transportcfg/exec_helpers.go` и в подпакет перенесены exec/binary helper'ы transport backend:
- template command builders (`singbox`, `dnstt`, `phoenix`, `ssh-overlay`);
- binary resolution/validation helpers (`ResolveBinary`, `FindBinaryPath`, `BinaryExists`, packaging profile);
- shell/config helpers (`ShellJoinArgs`, `ShellQuoteArg`, `ConfigInt`, `DefaultConfigPath`);
- добавлен `selective-vpn-api/app/transportcfg/probe_helpers.go` и в подпакет перенесены probe helper'ы transport backend:
- latency probing pipeline (`ProbeClientLatency`, host/netns endpoint probe);
- endpoint extraction/parsing/dedupe helpers для config/singbox-json;
- shared parse helpers (`ParseInt`, `SplitCSV`).
- app-facade сохранён внутри существующих transport-файлов (`transport_backends.go`, `transport_backends_adapter_systemd.go`), а отдельные файлы `transport_backends_runtime_helpers.go` и `transport_backends_systemd_helpers.go` удалены для снижения шума в корне `app/`;
- отдельный файл `transport_backends_exec_helpers.go` удалён; совместимые фасады сохранены в `transport_backends.go`;
- отдельный файл `transport_backends_probe_helpers.go` удалён; runtime dial/netns фасады и backward-compatible symbols для тестов собраны в `transport_backends.go`;
- добавлен подпакет `selective-vpn-api/app/eventsbus` (`Bus`, `Push`, `Since`); `app/events_bus.go` переведён в thin-adapter/facade с сохранением контракта `events.push/events.since` и `envInt`;
- добавлен подпакет `selective-vpn-api/app/apiroutes` (`Register` + доменные route-registrars), `app/api_routes.go` переведён на dependency-assembly facade;
- удалены файлы route-registry из корня `app`: `api_routes_core.go`, `api_routes_dns.go`, `api_routes_trace.go`, `api_routes_traffic.go`, `api_routes_transport.go`, `api_routes_vpn.go`;
- удалён файл `transport_backends_adapters_misc.go`; типы `transportUnsupportedRuntimeBackend` и `transportMockBackend` перенесены в `transport_backends.go` (единая точка transport-facade);
- добавлен подпакет `selective-vpn-api/app/syscmd` и перенесены низкоуровневые command helper'ы (`RunCommand`, `RunCommandTimeout`, `CheckPolicyRoute`); `app/shell.go` оставлен как thin-facade;
- добавлен подпакет `selective-vpn-api/app/httpx` и перенесены общие HTTP helper'ы (`LogRequests`, `WriteJSON`, `HandleHealthz`); `app/http_helpers.go` переведён в thin-facade;
- добавлен подпакет `selective-vpn-api/app/eventstream` и перенесён SSE polling/heartbeat loop (`ParseSinceID`, `Serve`); `app/events_handlers.go` оставлен как adapter к `events.since`;
- добавлен подпакет `selective-vpn-api/app/refreshcoord` и перенесена SWR/backoff state-machine (`Coordinator`, `Snapshot`); `app/refresh_coordinator.go` оставлен как thin-facade с прежними методами (`beginRefresh/finish*/snapshot`) для совместимости;
- в `egress_identity.go` убран прямой доступ к внутренним полям SWR (`refreshInProgress`, `nextRetryAt`) — заменено на фасадные методы (`refreshInProgress()`, `nextRetryAt()`, `clearBackoff()`) для безопасной декомпозиции;
- добавлен подпакет `selective-vpn-api/app/nftupdate` и перенесён алгоритм обновления nft-set (`interval compression`, `atomic transaction`, `chunked fallback`);
- `app/nft_update.go` переведён в thin-facade с прежним публичным контрактом (`nftUpdateIPsSmart`, `nftUpdateSetIPsSmart`) и прежним trace-логированием через `appendTraceLine("routes", ...)`;
- добавлен подпакет `selective-vpn-api/app/trafficcandidates` и перенесён сбор candidates (`subnets`, `units`, `uids`) в DI-модуль;
- `app/traffic_candidates.go` переведён в thin-adapter (маппинг pkg DTO -> API DTO без изменения endpoint path/поля ответа);
- для визуальной очистки корня `app/` удалены 16 отдельных resolver bridge-файлов:
- `resolver_artifacts_bridge.go`
- `resolver_dns_config_bridge.go`
- `resolver_domain_cache_bridge.go`
- `resolver_host_lookup_bridge.go`
- `resolver_mode_runtime_bridge.go`
- `resolver_planning_bridge.go`
- `resolver_precheck_finalize_bridge.go`
- `resolver_precheck_types_bridge.go`
- `resolver_resolve_batch_bridge.go`
- `resolver_runtime_tuning_bridge.go`
- `resolver_start_log_bridge.go`
- `resolver_static_labels_bridge.go`
- `resolver_summary_log_bridge.go`
- `resolver_timeout_recheck_bridge.go`
- `resolver_types_bridge.go`
- `resolver_wildcard_bridge.go`
- вместо них добавлен единый `selective-vpn-api/app/resolver_bridge.go` (консолидированный bridge-слой resolver-пайплайна);
- добавлен подпакет `selective-vpn-api/app/trafficprofiles` и вынесена state/store-логика профилей приложений (`list/upsert/delete/dedupe`, json persistence, id-derive);
- `app/traffic_app_profiles.go` переведён в thin-adapter: HTTP decode/encode + mapping DTO `app <-> trafficprofiles`;
- функция `sanitizeID` сохранена в `app` (runtime-совместимость с transport/egress/singbox контуром);
- добавлен `selective-vpn-api/app/trafficprofiles/appkey.go` и в него вынесены `CanonicalizeAppKey` + `SplitCommandTokens` + wrapper parser helpers;
- удалён root-файл `selective-vpn-api/app/traffic_appkey.go`; в `app/traffic_app_profiles.go` оставлены совместимые фасады `canonicalizeAppKey`/`splitCommandTokens` для существующих вызовов и тестов;
- добавлен подпакет `selective-vpn-api/app/trafficappmarks` и вынесены хелперы состояния app-marks:
- `LoadState` / `SaveState` (json persistence),
- `DedupeItems` / `UpsertItem`,
- `IsAllDigits`;
- в `app/traffic_appmarks.go` сохранены совместимые фасады (`loadAppMarksState`, `saveAppMarksState`, `dedupeAppMarkItems`, `upsertAppMarkItem`, `isAllDigits`) и type-alias на пакетные модели (`appMarksState`, `appMarkItem`);
- `traffic_appmarks.go` сокращён с `1140` до `1002` строк, без изменения endpoint-поведения и nft runtime-логики;
- добавлен файл `selective-vpn-api/app/trafficappmarks/cgroup.go` и вынесены `ResolveCgroupV2PathForNft`, `NormalizeCgroupRelOnly`, `CgroupDirInode`;
- в `app/traffic_appmarks.go` оставлены одноимённые фасады для совместимости текущих вызовов;
- добавлен файл `selective-vpn-api/app/trafficappmarks/nft.go` и вынесены `InsertAppMarkRule/DeleteAppMarkRule/HasAppMarkRule`, `UpdateLocalBypassSet`, `ParseNftHandle`, `CompactIPv4IntervalElements`;
- туда же вынесены cleanup helper'ы `CleanupLegacyRules` и `ClearManagedRules`, а в `app/traffic_appmarks.go` оставлены фасады `cleanupLegacyAppMarksRules`/`clearManagedAppMarkRules`;
- в `selective-vpn-api/app/trafficappmarks/store.go` добавлен `PruneExpired` (TTL prune + safe handling corrupted `expires_at` + callback на удаление nft-rule);
- в `selective-vpn-api/app/trafficappmarks/nft.go` добавлен `EnsureBase` (best-effort bootstrap table/chains/set + jump to app chain), фасад `ensureAppMarksNft` в `app` сохранён;
- в `app/traffic_appmarks.go` сохранены совместимые фасады `nftInsertAppMarkRule/nftDeleteAppMarkRule/nftHasAppMarkRule/updateAppMarkLocalBypassSet/parseNftHandle`;
- `traffic_appmarks.go` дополнительно сокращён до `723` строк;
- добавлен подпакет `selective-vpn-api/app/trafficmode` и файл `normalize.go` (вынесены `TokenizeList`, `NormalizeSubnetList`, `NormalizeUIDToken`, `NormalizeUIDList`, `NormalizeCgroupList`);
- `app/traffic_mode.go` переведён на фасады к `trafficmode` для normalize-контура (без изменения API/маршрутной логики);
- добавлен `selective-vpn-api/app/trafficmode/autolocal.go` (вынесен parser `ip route show table main` для auto-local-bypass);
- в `app/traffic_mode.go` оставлены фасады `parseRouteDevice/isContainerIface/routeLineIsLinkDown/isAutoBypassDestination` для backward-compatible вызовов из `traffic_candidates.go`;
- добавлен `selective-vpn-api/app/trafficmode/cgroup.go` (вынесен cgroup scan/resolve и derive uidrange из `/proc/<pid>/status`);
- добавлен `selective-vpn-api/app/trafficmode/ingress.go` (вынесен ingress-reply bypass nft lifecycle и state-check);
- добавлен `selective-vpn-api/app/trafficmode/rules.go` (вынесен parsing `ip rule show` + route-probe `ip route get`);
- добавлен `selective-vpn-api/app/trafficmode/apply.go` (вынесен apply/remove управляемых `ip rule` и overlay overrides);
- удалены промежуточные wrapper-функции cgroup/nftObjectMissing из `traffic_mode.go`; `buildEffectiveOverrides` использует пакетные вызовы напрямую;
- `traffic_mode.go` сокращён с `1449` до `923` строк;
- добавлен подпакет `selective-vpn-api/app/egressutil` и файл `util.go`;
- в `app/egress_identity.go` переведены на фасады: `parseEgressIPFromBody`, `egressParseGeoResponse`, `normalizeCountryCode`, `egressIPEndpoints`, `egressGeoEndpointsForIP`, `egressLimitEndpointsForNetns`, `egressJoinErrorsCompact`, `egressParseSingBoxSOCKSProxyURL`, `egressResolvedHostForURL`, `resolveEgressCurlPath`, `resolveEgressWgetPath`, `egressTimeoutSec`;
- удалены устаревшие внутренние переменные/функции egress (`egressCurlPathOnce/egressWgetPathOnce`, локальные any/url helpers);
- удалены мёртвые egress wrappers (неиспользуемые after-extract), сохранён публичный runtime-контракт для активных вызовов;
- `egress_identity.go` сокращён с `1208` до `932` строк;
- добавлен подпакет `selective-vpn-api/app/dnscfg` и файл `smartdns.go`;
- `app/dns_settings.go` переведён на фасады `dnscfg` для SmartDNS default/config parsing и DNS upstream normalization;
- добавлен `selective-vpn-api/app/dnscfg/systemd.go` и вынесены helper'ы `smartdns` unit state/action;
- добавлен `selective-vpn-api/app/dnscfg/benchmark.go` и вынесен benchmark-контур DNS (core scoring/probe/normalize) с callback-зависимостями (`splitDNS`, `classifyDNSError`, `isPrivateIPv4`);
- `app/dns_settings.go` оставлен как facade HTTP/DTO слой для benchmark-endpoint, без изменения JSON-контракта;
- добавлен `selective-vpn-api/app/dnscfg/pool.go` и вынесены helper'ы pool/legacy-конвертации;
- `app/dns_settings.go` использует фасады `dnscfg` для pool нормализации/конвертации, сохраняя прежние JSON-модели и endpoint-поведение;
- в `selective-vpn-api/app/dnscfg/mode.go` добавлены `ModeConfig/ModeState/LoadMode/SaveMode`; `app/dns_settings.go` переведён на тонкий фасад для чтения/сохранения `dns_mode.json`;
- `dns_settings.go` дополнительно очищен от лишних benchmark-wrapper-функций; benchmark-контур работает через прямые вызовы `dnscfg` без изменения API-контракта;
- добавлен `selective-vpn-api/app/dnscfg/upstreams.go` и вынесены parse/normalize/render helper'ы конфига `dns-upstreams.conf`;
- `loadDNSUpstreamsConfFile/saveDNSUpstreamsConfFile` переведены на фасады `dnscfg` без изменения формата `dns-upstreams.conf` и legacy JSON mirror;
- добавлен `selective-vpn-api/app/dnscfg/prewarm.go`; логика SmartDNS prewarm (domain expansion, parallel dig, runtime/manual nft merge, summary/per-upstream log) вынесена из `app/dns_settings.go` в пакет `dnscfg` через callback-deps;
- добавлен `selective-vpn-api/app/dns_smartdns_handlers.go`; туда вынесены `handleSmartdnsRuntime`, `handleSmartdnsPrewarm`, `runSmartdnsPrewarm` (тонкий app-adapter к `dnscfg.RunPrewarm`);
- `dns_settings.go` дополнительно сокращён до `749` строк (было `1073` до этого шага), без изменения endpoint-path/JSON-контрактов;
- в `app/egressutil` добавлены `scope.go`/`identity.go`/`probe.go`/`http.go` (scope parser, identity diff, endpoint probe loop, HTTP body fetch helper);
- в `app/egress_identity.go` сохранены совместимые фасады (`parseEgressScope`, `egressIdentityChanged`) и перевод вызовов probe/geo на пакетные helper'ы;
- удалены локальные pure-wrapper'ы egress; `egress_identity_test.go` использует `egressutil` напрямую;
- `egress_identity.go` сокращён до `854` строк (было `932` на старте блока F1.14);
- добавлен `selective-vpn-api/app/transportcfg/singbox_helpers.go` и в него вынесены повторно используемые helper'ы профилей singbox (digest/diff/path/io/validators/stamp/messages);
- `transport_singbox_profiles_flow.go` переведён на пакетные helper-вызовы `transportcfg` с сохранением API/flow-поведения; файл сокращён до `1624` строк;
- добавлен `selective-vpn-api/app/transportcfg/history_helpers.go`; вынос history io/selection helpers из `transport_singbox_profiles_flow.go` выполнен через пакетные фасады;
- удалены локальные helper'ы `findSingBoxBinary`/`sanitizeHistoryStamp` из `transport_singbox_profiles_flow.go` (замена на package helpers);
- `transport_singbox_profiles_flow.go` дополнительно сокращён до `1563` строк;
- `dns_settings.go` сокращён с `1521` до `1196` строк;
- валидация: `go test ./...` и `go build ./...` в `selective-vpn-api` — успешно.
- 2026-03-10: продолжен шаг `F1.11` (декомпозиция `transport_backends.go` без смены поведения):
- вынесен systemd helper-блок в `selective-vpn-api/app/transport_backends_systemd_helpers.go`:
- unit-name/ownership/file-write helpers;
- unit renderer + ssh-overlay renderer;
- tuning/hardening normalizers и prefix-fallback config helpers.
- вынесен probe/latency helper-блок в `selective-vpn-api/app/transport_backends_probe_helpers.go`:
- endpoint collector/parser/dedupe;
- host/netns probe и latency sampling.
- вынесен exec/binary helper-блок в `selective-vpn-api/app/transport_backends_exec_helpers.go`:
- `resolveTransportPrimaryExecStart` + `buildTransport*Command`;
- packaging/binary resolution helpers (`resolveTransportBinary`, `validateRequiredBinary`, `transportPackagingProfile`);
- shell/config int helpers (`shellJoinArgs`, `shellQuoteArg`, `transportConfigInt`).
- вынесен runtime helper-блок в `selective-vpn-api/app/transport_backends_runtime_helpers.go`:
- `transportBackendUnit/defaultTransportBackendUnit`;
- `transportRuntimeMode`;
- `transportSystemdActionUnits/transportSystemdHealthUnits`;
- `transportConfigString/transportConfigBool`.
- вынесены adapter-реализации `unsupported/mock` в `selective-vpn-api/app/transport_backends_adapters_misc.go`.
- вынесен `systemd` adapter в `selective-vpn-api/app/transport_backends_adapter_systemd.go` (действия/health/provision/cleanup + missing-unit helpers).
- итоговая декомпозиция: `transport_backends.go` сокращён с `1954` до `73` строк (тонкий core/facade).
- валидация: `go test ./app -run TestTransportSystemdBackendHealth -count=1`, `go test ./...`, `go build ./...` — успешно.
- 2026-03-10: продолжен `F1.11` по resolver-контурy (введена отдельная папка `selective-vpn-api/app/resolver/`):
- вынесен wildcard matcher в подпакет `selective-vpn-api/app/resolver/wildcard_matcher.go` (`NormalizeWildcardDomain/NewWildcardMatcher/Match/IsExact/Count`);
- вынесены общие resolver helper'ы в `selective-vpn-api/app/resolver/common.go` (`UniqueStrings`, `PickDNSStartIndex`, `StripANSI`, `IsPrivateIPv4`);
- вынесены DNS метрики/типы в `selective-vpn-api/app/resolver/dns_metrics.go` (`DNSErrorKind`, `DNSMetrics`, `DNSUpstreamMetrics`);
- вынесены JSON/IO helper'ы в `selective-vpn-api/app/resolver/io_helpers.go` (`ReadLinesAllowMissing`, `LoadJSONMap`, `SaveJSON`, `LoadResolverPrecheck*`, `LoadResolverLiveBatch*`);
- вынесены DNS parser/helper'ы в `selective-vpn-api/app/resolver/dns_helpers.go` (`SplitDNS`, `ClassifyDNSError`);
- вынесен adaptive live-batch policy-калькулятор в `selective-vpn-api/app/resolver/live_batch.go` (`ComputeNextLiveBatchTarget`, `ComputeNextLiveBatchNXHeavyPct`) с сохранением прежней формулы;
- вынесен live-batch selector в `selective-vpn-api/app/resolver/live_batch_select.go` (`ClassifyLiveBatchHost`, `SplitLiveBatchCandidates`, `PickAdaptiveLiveBatch`);
- вынесен upstream-pool helper в `selective-vpn-api/app/resolver/dns_upstreams.go` (`BuildResolverFallbackPool`, `MergeDNSUpstreamPools`);
- вынесен error-policy helper в `selective-vpn-api/app/resolver/error_policy.go` (`SmartDNSFallbackForTimeoutEnabled`, `ShouldFallbackToSmartDNS`, `ClassifyHostErrorKind`, `ShouldUseStaleOnError`);
- полностью вынесен `domain cache` блок в `selective-vpn-api/app/resolver/domain_cache.go` (state/model + normalize/migrate + get/set/quarantine/stale + score/state policy + json render/summary);
- вынесены precheck/live-batch типы в `selective-vpn-api/app/resolver/precheck_types.go` + bridge `selective-vpn-api/app/resolver_precheck_types_bridge.go`;
- вынесено сохранение precheck-state в `selective-vpn-api/app/resolver/precheck_state.go` (`SaveResolverPrecheckState`) с bridge-вызовом из `app`;
- вынесен static/PTR блок в `selective-vpn-api/app/resolver/static_labels.go` (`ParseStaticEntries`, `ResolveStaticLabels`, `DigPTR`) + bridge `selective-vpn-api/app/resolver_static_labels_bridge.go`;
- вынесен timeout-quarantine recheck блок в `selective-vpn-api/app/resolver/timeout_recheck.go` (`RunTimeoutQuarantineRecheck`) + bridge `selective-vpn-api/app/resolver_timeout_recheck_bridge.go`;
- вынесен загрузчик DNS-конфига в `selective-vpn-api/app/resolver/dns_config.go` (`LoadDNSConfig`) + bridge `selective-vpn-api/app/resolver_dns_config_bridge.go`;
- вынесен host-lookup блок в `selective-vpn-api/app/resolver/host_lookup.go` (`ResolveHost`, `DigAWithPolicy`) + bridge `selective-vpn-api/app/resolver_host_lookup_bridge.go` (`resolveHostGo`, `digA`, `digAWithPolicy`);
- типы DNS policy/cooldown (`dnsAttemptPolicy`, `dnsRunCooldown`, `dnsCooldownState`) перенесены из `resolver.go` в `selective-vpn-api/app/resolver_dns_policy.go` для очистки монолита;
- вынесен runtime-tuning блок (`TTL/workers/dns timeout/precheck/live-batch/negative TTL`) в `selective-vpn-api/app/resolver/runtime_tuning.go` (`BuildResolverRuntimeTuning`) + bridge `selective-vpn-api/app/resolver_runtime_tuning_bridge.go`;
- вынесен artifact-builder блок (`IPs/IPMap/Direct/Wildcard`) в `selective-vpn-api/app/resolver/artifacts.go` (`BuildResolverArtifacts`) + bridge `selective-vpn-api/app/resolver_artifacts_bridge.go`;
- вынесен planning блок (`fresh/toResolve/cache_negative/quarantine/stale/precheck_scheduled`) в `selective-vpn-api/app/resolver/planning.go` (`BuildResolvePlanning`) + bridge `selective-vpn-api/app/resolver_planning_bridge.go`;
- вынесен precheck finalize/save блок (next live-batch calc + state persist + force-file consume) в `selective-vpn-api/app/resolver/precheck_finalize.go` (`FinalizeResolverPrecheck`) + bridge `selective-vpn-api/app/resolver_precheck_finalize_bridge.go`;
- вынесен concurrent resolve-блок (`workers/jobs/results` + stale-on-error apply) в `selective-vpn-api/app/resolver/resolve_batch.go` (`ExecuteResolveBatch`) + bridge `selective-vpn-api/app/resolver_resolve_batch_bridge.go`;
- вынесен summary/breakdown/precheck-log блок в `selective-vpn-api/app/resolver/summary_log.go` (`LogResolverSummary`) + bridge `selective-vpn-api/app/resolver_summary_log_bridge.go`;
- вынесен runtime DNS mode apply/log блок в `selective-vpn-api/app/resolver/mode_runtime.go` (`ApplyDNSModeRuntime`, `LogDNSMode`) + bridge `selective-vpn-api/app/resolver_mode_runtime_bridge.go`;
- вынесен start/policy log блок в `selective-vpn-api/app/resolver/start_log.go` (`LogResolverStart`) + bridge `selective-vpn-api/app/resolver_start_log_bridge.go`;
- финальный orchestration split: добавлен pipeline-файл `selective-vpn-api/app/resolver_pipeline.go` (`buildResolverJobContext`, `runResolverPipeline`), а `runResolverJob` оставлен тонким фасадом;
- добавлен bridge `selective-vpn-api/app/resolver_domain_cache_bridge.go` (сохранены старые имена/методы `domainCache*` в пакете `app` через wrapper-слой);
- добавлен совместимый bridge `selective-vpn-api/app/resolver_wildcard_bridge.go` (сохранены старые вызовы `normalizeWildcardDomain/newWildcardMatcher` в пакете `app`);
- добавлен type bridge `selective-vpn-api/app/resolver_types_bridge.go` (alias-константы/типы для `dnsErrorKind`/`dnsMetrics` без смены API внутри `app`);
- вынесен DNS policy/cooldown блок из монолита в `selective-vpn-api/app/resolver_dns_policy.go`;
- `resolver.go` сокращён с `2983` до `41` строк без изменения API-контрактов;
- валидация: `go test ./...` и `go build ./...` в `selective-vpn-api` — успешно.
- 2026-03-10: закрыт шаг `F1.4` (декомпозиция `vpn_dashboard_qt.py`):
- добавлен пакет `selective-vpn-gui/main_window/` и вынесены базовые модули: `constants.py` (shared UI constants), `workers.py` (`EventThread`, `LocationsThread`);
- вынесен UI shell-контур (`build tabs + helpers + locations/egress`) в `main_window/ui_shell_mixin.py`;
- вынесен крупный SingBox-контур в подпакет `main_window/singbox/*` (`editor`, `cards`, `links`, `runtime`) и сохранён фасад `main_window/singbox_mixin.py`;
- вынесен runtime/refresh/actions контур в `main_window/runtime_actions_mixin.py` (events stream, refresh, login/auth, routes/dns/domains actions, close lifecycle);
- добавлена вторичная декомпозиция mixin-слоёв: `ui_tabs_*`, `ui_helpers`, `ui_location_runtime`, `runtime_{state,refresh,auth,ops}`, `singbox/{links_*,runtime_*}`; фасады `ui_tabs_mixin.py`, `ui_shell_mixin.py`, `runtime_actions_mixin.py`, `singbox_mixin.py` сохранены;
- отдельный split `ui_tabs_singbox_mixin.py` на `ui_tabs_singbox_{layout,editor}_mixin.py` (убран последний UI-файл >700 строк);
- `selective-vpn-gui/vpn_dashboard_qt.py` сокращён с `6103` до `116` строк, при этом сохранён как thin-bootstrap+UI wiring;
- валидация: `python3 -m py_compile vpn_dashboard_qt.py main_window/*.py main_window/singbox/*.py` и import-проверка `MainWindow` проходят.
- 2026-03-10: hotfix `systemd stop` idempotency для transport lifecycle:
- в `transportSystemdBackend.Action` добавлен no-op режим для `action=stop`, если `systemctl` возвращает `Unit ... not loaded/not found/unknown unit` (чтобы отсутствующий unit не валил switch-пайплайн);
- добавлен unit-тест `TestTransportSystemdBackendStopMissingUnitIsNoop`;
- runtime-валидация на живом API: `POST /api/v1/transport/clients/sg-finland-yur/start` и `.../sg-realnetns/start` проходят, `.../sg-torjantest-ylu9ja7f/stop` возвращает `ok=true` с warning вместо `TRANSPORT_BACKEND_ACTION_FAILED`.
- 2026-03-10: hotfix `systemd start/restart` auto-provision для новых SingBox-профилей:
- в `transportSystemdBackend.Action` добавлен общий fallback для `SingBox`: при `start/restart` и ошибке `unit not found/not loaded` backend автоматически выполняет `Provision` и повторяет `systemctl start/restart` один раз;
- добавлен unit-тест `TestTransportSystemdBackendStartAutoProvisionOnMissingUnit`;
- runtime-валидация: `POST /api/v1/transport/clients/sg-torjantest-ylu9ja7f/start` вернул `ok=true`, `status_after=up` без ручного `Prepare`.
- 2026-03-10: закрыт шаг `F1.6` по `cmd/*` entrypoints в `selective-vpn-api`:
- добавлены отдельные main-пакеты: `cmd/selective-vpn-api`, `cmd/selective-vpn-routes-update`, `cmd/selective-vpn-routes-clear`, `cmd/selective-vpn-autoloop`;
- в `app/server.go` выделены явные entrypoint-функции `RunAPIServer`, `RunRoutesUpdateCLI`, `RunRoutesClearCLI`, `RunAutoloopCLI`, route registration вынесена в `registerAPIRoutes`;
- legacy `main.go` сохранён для обратной совместимости (старые unit/скрипты продолжают работать через `app.Run()`).
- 2026-03-10: выполнена повторная валидация `F1.6` (контрольный прогон перед следующим рефактором):
- `go test ./...` и `go build ./...` в `selective-vpn-api` прошли успешно;
- собраны отдельные бинарники из `cmd/*` в `/tmp` (`selective-vpn-api`, `selective-vpn-routes-update`, `selective-vpn-routes-clear`, `selective-vpn-autoloop`).
- 2026-03-10: закрыт шаг `F1.7` (декомпозиция API bootstrap/роутера):
- удалён монолит `selective-vpn-api/app/server.go` и введены файлы `entrypoints.go`, `api_bootstrap.go`, `api_routes.go`;
- сигнатуры и поведение сохранены: `Run*CLI`, `RunAPIServer`, `runAPIServerAtAddr`, `registerAPIRoutes`;
- `registerAPIRoutes` дополнительно разложен на доменные helper'ы (`registerCoreRoutes`, `registerRoutesControlRoutes`, `registerTrafficRoutes`, `registerTransportRoutes`, `registerDNSRoutes`, `registerVPNRoutes`) без изменения endpoint-path;
- контрольная валидация `go test ./...` и `go build ./...` прошла успешно.
- 2026-03-10: закрыт шаг `F1.8` (физическая декомпозиция route-registry по доменам):
- `register*Routes` вынесены из общего файла в `app/api_routes_core.go`, `api_routes_traffic.go`, `api_routes_transport.go`, `api_routes_trace.go`, `api_routes_dns.go`, `api_routes_vpn.go`;
- `app/api_routes.go` оставлен как thin-facade (`registerAPIRoutes`), который только собирает доменные registrars;
- endpoint-path и handlers сохранены 1:1; контрольная валидация `go test ./...` и `go build ./...` прошла успешно.
- 2026-03-10: закрыт шаг `F1.9` (переход от "всё в app" к подпапкам runtime-слоя):
- добавлен подпакет `app/cli` с раннерами `routes-update`, `routes-clear`, `autoloop` и dependency-injection через callbacks;
- добавлен подпакет `app/bootstrap` с HTTP server runner (`Run(ctx, Config)`), `app/api_bootstrap.go` переведён на thin-wrapper + `prepareAPIRuntime()`;
- фасады в `app` сохранены (`RunRoutesUpdateCLI`, `RunRoutesClearCLI`, `RunAutoloopCLI`, `runAPIServerAtAddr`), поэтому внешний контракт и поведение не изменились;
- контрольная валидация: `go test ./...` и `go build ./...` успешны.
- 2026-03-10: закрыт шаг `F1.10` (декомпозиция transport handler-монолита):
- удалён монолит `app/transport_handlers.go` (`~2440` строк), код разложен на модули: `transport_shared.go`, `transport_handlers_clients.go`, `transport_handlers_policy.go`, `transport_policy_validate.go`, `transport_client_runtime.go`, `transport_tokens_state.go`;
- все endpoint-handler сигнатуры и policy/lifecycle поведение сохранены (роуты `transport/*` без изменений);
- контрольная валидация: `go test ./...` и `go build ./...` успешны.
- 2026-03-10: начат шаг `F1.11` (подпакеты без циклов):
- вынесен token-store в `app/transporttoken/store.go` (issue/consume/ttl cleanup + token generation);
- `transport_tokens_state.go` переведён на новый store через facade (`issueTransportConfirmToken`, `consumeTransportConfirmToken`, `newTransportToken`);
- обновлены unit-тесты confirm-token lifecycle под новый store (`TestTransportConfirmStoreExpiresToken`), `go test ./...` и `go build ./...` успешны.
- 2026-03-10: начат шаг `F1.12` (декомпозиция `api_client.py` по папкам):
- добавлен пакет `selective-vpn-gui/api/` (`models.py`, `errors.py`, `utils.py`, `client.py`, `__init__.py`);
- `api_client.py` переведён в backward-compatible facade (реэкспорт `ApiClient/ApiError/models/strip_ansi`);
- сохранена обратная совместимость импортов (`from api_client import ...`) и добавлен новый путь (`from api import ...`);
- проверка: `py_compile` + импорты `dashboard_controller.py`, `vpn_dashboard_qt.py`, `dns_benchmark_dialog.py` проходят.
- 2026-03-10: закрыт шаг `F1.12` (полная доменная декомпозиция Python API-клиента):
- `selective-vpn-gui/api/client.py` сокращён до base HTTP/SSE + shared helpers (`_request/_json/_to_int/_parse_cmd_result`);
- доменные методы вынесены в mixin-модули: `api/status.py`, `api/routes.py`, `api/traffic.py`, `api/dns.py`, `api/domains.py`, `api/vpn.py`, `api/trace.py`;
- дополнительный разрез transport-домена: `api/transport_clients.py`, `api/transport_policy.py`, `api/transport_singbox.py` + facade `api/transport.py`;
- backward compatibility сохранена (`api_client.py` facade и `api.transport.TransportApiMixin`), `py_compile` и импорты GUI-модулей проходят.
- 2026-03-10: закрыт шаг `F1.3` (декомпозиция `dashboard_controller.py`):
- добавлен пакет `selective-vpn-gui/controllers/` с domain mixin-модулями: `status`, `vpn`, `routes`, `traffic`, `transport`, `dns`, `domains`, `trace`, плюс общий `core` и `views`;
- `dashboard_controller.py` сокращён до thin-facade (`DashboardController`) с прежней точкой импорта и совместимыми экспортами (`TraceMode`, view-models);
- проверка: `py_compile` + импорты `vpn_dashboard_qt.py`, `traffic_mode_dialog.py`, `dns_benchmark_dialog.py` проходят.
- 2026-03-10: зафиксировано разделение зависимостей `go.mod` vs runtime services:
- выполнен `go mod tidy` для `selective-vpn-api` (изменений не потребовалось; Go-модули актуальны);
- добавлен preflight-check `scripts/check_runtime_dependencies.sh` (required/optional + `--strict`);
- добавлен документ `docs/phase-b/B4_RUNTIME_DEPENDENCIES_AND_PREFLIGHT.md` с реестром бинарей/unit-зависимостей до начала крупного рефакторинга.
- 2026-03-10: стабилизирован egress refresh для `AdGuardVPN` после смены локации:
- в Go API (`/api/v1/vpn/location`) добавлен backend `egress refresh burst` для `adguardvpn` (force refresh сразу + отложенные итерации), чтобы egress IP/geo догонял фактическое переключение туннеля;
- в `egress identity` force-refresh теперь обходит negative geo-cache (ошибка geo не блокирует повторную попытку при `force=true`), а TTL negative geo-cache снижен до `30s`;
- в GUI (`refresh_vpn_tab`) добавлены дополнительные post-switch trigger'ы egress-refresh (на смену desired location и на завершение switching), а polling расширен до стабилизации `IP + country` без ранней остановки.
- 2026-03-10: доведён anti-regression контур `SingBox netns + multi-profile switch`:
- в backend lifecycle `POST /api/v1/transport/clients/{id}/start|restart` добавлен обязательный preflight для `SingBox` (render/validate + materialize `config_path`) до `systemctl`, чтобы старт не обходил профильную проверку;
- при `start/restart` выбранного `SingBox` в `netns` backend теперь автоматически останавливает другие `SingBox`-клиенты в этом же namespace перед запуском (убран конфликт `listen 127.0.0.1:10808: address already in use` при switch между профилями);
- в `systemd` backend добавлен best-effort `systemctl reset-failed <unit>` перед `start/restart`, чтобы switch не упирался в `start-limit-hit` после прошлых неуспешных стартов;
- закрыт legacy кейс `packet_encoding: none` (SingBox panic `unknown value`): нормализация выполняется в Go-render pipeline, а в GUI-editor/link-import `none` больше не пишется в raw config;
- для `egress identity` при `transport:<id>` и `netns_enabled=true` у `SingBox` direct-netns fallback полностью отключён: probe идёт только через локальный SOCKS inbound, иначе возвращается ошибка (без ложного AdGuard/system IP);
- runtime-проверка: `sg-finland-yur` стартует даже после удаления `/etc/selective-vpn/transports/sg-finland-yur/singbox.json` (файл воссоздаётся preflight); при последовательном switch подтверждён ожидаемый egress: `sg-finland-yur -> FI (92.42.102.181)`, `sg-realnetns -> NL (46.17.97.149)`.
- 2026-03-10: добавлен хвостовой экспериментальный этап `Z1` (глобальный L7 orchestration-layer) и отдельно зафиксировано правило: подготовку можно делать заранее, но runtime-включение только в конце планов.
- 2026-03-10: зафиксирован отложенный этап `E7` (без реализации в текущем спринте):
- целевое разделение `System selective PBR` (L3/L4 transport guardrails) и `SingBox L7 routing` (policy-engine);
- зафиксирован полный список будущих работ по L7 правилам, renderer, pipeline `validate/apply/rollback`, DNS-стратегии и observability.
- 2026-03-10: исправлен приоритет egress-проверки для `transport:<id>` в `netns`:
- для `SingBox` с локальным `socks` inbound probe теперь сначала идёт через `socks5h://127.0.0.1:<port>` (реальный tunnel egress), а direct-netns probe оставлен только как fallback;
- устранён ложный кейс, когда `transport:sg-realnetns` показывал системный/AdGuard IP вместо IP tunnel-профиля.
- 2026-03-10: дополнили фиксы `SingBox + netns` и старт нового профиля:
- для `transport:<id>` при `netns + socks-inbound` direct-netns fallback отключён; при ошибке proxy probe возвращается ошибка, чтобы UI не показывал ложный AdGuard/system IP как egress transport-клиента;
- в GUI `Run/Start` добавлен preflight `singbox profile apply` (`skip_runtime=true`) перед switch/start, чтобы materialize `config_path` и исключить падение нового клиента с ошибкой `open .../singbox.json: no such file or directory`.
- 2026-03-10: исправлен конфликт `netns`/`agvpn` маршрутов для `SingBox Real NetNS`:
- устранена причина флапов `lookup n3.elmprod.tech ... deadline exceeded` при живом профиле;
- в `traffic_mode` авто-bypass больше не подхватывает `linkdown` маршруты и `svh*/svn*` интерфейсы;
- в `transport_netns` добавлена принудительная синхронизация policy-route (`table agvpn`) для `netns_subnet` на актуальный host-veth при `start/restart` + cleanup при удалении namespace;
- выполнена одноразовая очистка сиротского legacy namespace (`svpn-realnetns`) и проверен стабильный egress (`transport:sg-realnetns` -> `stale=false`, IP/geo обновляется).
- 2026-03-10: доведён `E6` netns runtime contour для `transport:<id>`:
- netns-probe больше не зависит от DNS внутри namespace (`curl --resolve` с host-side резолвом endpoint хоста);
- добавлен fallback probe через локальный `SingBox` SOCKS inbound (`socks5h://127.0.0.1:<port>`) для изолированных netns;
- ограничена длительность netns-probe (по умолчанию `SVPN_EGRESS_NETNS_MAX_ENDPOINTS=1`) и уплотнены сообщения ошибок (`last_error`) для UI.
- 2026-03-10: закрыт E6.2E6.5 (egress identity implementation):
- добавлены endpoint'ы `GET /api/v1/egress/identity` и `POST /api/v1/egress/identity/refresh` в Go API;
- реализованы scope-providerы `adguardvpn|system|transport:<id>` с netns-aware probe для transport;
- добавлены SWR/single-flight/backoff метаданные (`updated_at/stale/refresh_in_progress/last_error/next_retry_at`) и SSE `egress_identity_changed`;
- добавлен GeoIP lookup (country_code/country_name) с кэшем;
- desktop GUI подключён к новому API: `AdGuardVPN` и `SingBox` карточки показывают `IP + country`, флаг строится в UI из `country_code`.
- 2026-03-10: закрыт E6.1 requirements freeze для общего `egress identity`:
- добавлен контракт `docs/phase-e/E6_EGRESS_IDENTITY_API_CONTRACT.md`;
- зафиксированы scope (`adguardvpn|transport:<id>|system`), endpoint'ы `GET /api/v1/egress/identity` и `POST /api/v1/egress/identity/refresh`;
- зафиксированы SWR/SSE поля и правило рендера флага в UI из `country_code`.
- 2026-03-10: ускорен live health-probe для `SingBox`:
- `transport` latency probe timeout снижен до `900ms`;
- устранён lock-contention в background health refresh: долгий `backend.Health()` вынесен из-под `transportMu`, чтобы запросы `GET /transport/clients/{id}/health` не ждали второй probe под mutex.
- 2026-03-09: полировка `SingBox` runtime UX и live latency:
- удалён отдельный блок `Runtime details` (убран дублирующий UI-слой в `SingBox` вкладке);
- `last update` перенесён в верхнюю карточку `Profile` (`id + updated timestamp`);
- GUI переведён на live health для выбранного профиля через `GET /api/v1/transport/clients/{id}/health` (через `ApiClient`/`DashboardController`), поэтому `latency_ms` обновляется сразу после refresh/SSE, а не только по snapshot `/transport/clients`.
- 2026-03-09: закрыт baseline по latency в transport health:
- `systemd` backend теперь пишет `health.latency_ms` для активных клиентов через TCP probe endpoint'ов из runtime-конфига (`singbox outbounds`);
- для `netns_enabled=true` probe выполняется через тот же netns (`ip netns exec/nsenter`), чтобы latency считался по фактическому runtime-контуру клиента;
- при неуспешном probe статус не деградирует (остаётся `up`, latency остаётся пустым), чтобы избежать ложных падений lifecycle;
- добавлены unit-тесты на endpoint extraction и latency sampling (`transport_backends_test.go`).
- 2026-03-09: завершён перенос netns-toggle orchestration из GUI в Go-ядро:
- добавлен и подключён endpoint `POST /api/v1/transport/netns/toggle` (config patch + provision + restart running clients);
- `ApiClient`/`DashboardController` получили отдельный метод `transport_netns_toggle`;
- `netns_debug.py` больше не выполняет локальную orchestration-цепочку и использует только backend call;
- добавлены unit-тесты `applyTransportNetnsToggleLocked` (success / partial-failure / no-targets).
- 2026-03-09: начата унификация SWR-механики для переиспользования:
- добавлен общий модуль `selective-vpn-api/app/refresh_coordinator.go` (`freshTTL`, single-flight refresh gate, exponential backoff with cap, snapshot metadata);
- `vpn_locations_cache.go` переведён на coordinator (без изменения endpoint `/api/v1/vpn/locations` и SSE `vpn_locations_changed`);
- добавлены unit-тесты coordinator (`refresh_coordinator_test.go`).
- 2026-03-09: SWR-схема расширена на transport health:
- подключён endpoint `POST /api/v1/transport/health/refresh` (manual trigger/force queue, без блокировки UI);
- `GET /api/v1/transport/clients` теперь ставит background health-refresh по eligible клиентам (`up|starting|degraded`) через shared coordinator (single-flight + backoff);
- GUI подписан на SSE `transport_client_health_changed`, кнопка refresh сначала триггерит backend health-refresh (best-effort), затем обновляет карточки.
- 2026-03-09: стабилизирован `SingBox + netns` runtime после регрессии:
- backend переведён на adaptive `nsenter` (default) с fallback на `ip netns exec`;
- netns setup/runtime используют единый exec selector (без дублирования режимов);
- исправлен `nft` comment-tag для NAT-правил (совместимо с текущим синтаксисом nft);
- добавлен документ ready-case: `docs/phase-d/D5_NETNS_RUNTIME_CASE.md`.
- 2026-03-09: выполнен рефакторинг netns-модулей:
- GUI логика переключения вынесена в `selective-vpn-gui/netns_debug.py` (state/button/toggle pipeline);
- API exec-логика вынесена в `selective-vpn-api/app/transport_netns_exec.go`;
- `vpn_dashboard_qt.py` и `transport_netns.go` оставлены как orchestrator/UI-обвязка.
- Созданы фазы AD в `docs/phase-{a,b,c,d}` с описанием целей, критериев и задач.
- Задокументированы ожидания от Go API (routes, traffic, DNS, SmartDNS, VPN).
- Подготовлен первоначальный план для проверки GUI+API разделения.
- Закрыт этап E5.2 в Go API для `SingBox profiles`:
- добавлены endpoint'ы `GET/POST /api/v1/transport/singbox/profiles`, `GET/PATCH/DELETE /api/v1/transport/singbox/profiles/{id}`, `GET /api/v1/transport/singbox/features`;
- добавлен persistent state с ревизиями профилей (`profile_revision`) и active profile resolution;
- добавлен secrets-store (`/var/lib/selective-vpn/transport/secrets/singbox/*.json`, `0600`) с masked выдачей в API и без утечки plaintext в state.
- Добавлены unit-тесты E5.2 (`selective-vpn-api/app/transport_singbox_profiles_test.go`): CRUD+revision conflict, secrets persistence/masking, features endpoint.
- Закрыт этап E5.3 в Go API для `SingBox profiles`:
- добавлены endpoint'ы `POST /api/v1/transport/singbox/profiles/{id}/validate|render|apply|rollback` и `GET /api/v1/transport/singbox/profiles/{id}/history`;
- реализован flow `validate -> render -> apply` с binary-check (`sing-box check`), persisted rendered config и runtime apply через существующий transport backend;
- реализован rollback через history snapshots (включая restore предыдущего target config) + SSE события `singbox_profile_validated|rendered|applied|rollback|failed`.
- Добавлены unit-тесты E5.3 (`selective-vpn-api/app/transport_singbox_profiles_flow_test.go`): render/apply/rollback/history сценарий и негативная валидация.
- Усилены smoke-скрипты в `tests/`:
- `api_sanity.sh` (валидация JSON ключей, проверка method guard, проверка SSE headers),
- `events_stream.py` (живой SSE + триггер `trace_append`),
- `vpn_login_flow.py` (start/state/action/stop),
- `trace_append.sh` (append + readback через plain и json),
- `transport_flow_smoke.py` (state-machine smoke: draft/validate/confirm/apply/rollback).
- Добавлен общий раннер `tests/run_all.sh`.
- Скрипты прогнаны локально с `API_URL=http://127.0.0.1:8080`: все 5 smoke-тестов прошли успешно.
- Phase D дополнен endpoint-level матрицей (`53` legacy ручки) со статусами `ready-read/ready-write/ready-async/ready-interactive`.
- По состоянию на 2026-03-07 в ядре `61` mux-маршрут (`53 legacy + 8 transport base`), плюс action-subpath `GET /api/v1/transport/clients/{id}/metrics` внутри `clients/{id}/*`.
- В Phase C/Phase D добавлены архитектурные требования для переиспользования API на `web + iOS + Android` (единый `/api/v1`, TLS/token auth, mobile fallback/retry/idempotency).
- Зафиксирован frontend-стек для web prototype: `Vite + React + TypeScript` (SPA). `Next.js` отложен как опция только при требованиях SSR/edge/BFF.
- Запущен web prototype foundation в `selective-vpn-web/`:
- App shell (sidebar/header/navigation) на `React Router`,
- query-слой на `TanStack Query` с read-only snapshot (`healthz`, `status`, `vpn/status`, `vpn/login-state`),
- SSE connectivity-hook для `/api/v1/events/stream`,
- placeholder-страницы `VPN/Routes/DNS/Transport/Trace` для поэтапного подключения бизнес-логики.
- Зафиксирован целевой набор внешних transport-клиентов: `sing-box` (клиент), `dnstt-client`, `phoenix` (подключение к `slipstream` серверу).
- Подготовлен отдельный дизайн multi-client маршрутизации поверх единого PBR-ядра: per-client `fwmark/table/pref`, ownership-lock для доменов/IP, conntrack stickiness, UX предупреждения о конфликтных сценариях.
- Подготовлен API-контракт `transport/*` с JSON-примерами, dry-run валидацией и apply-моделью (`docs/phase-e/E2_TRANSPORT_API_CONTRACT.md`).
- В Go-ядре добавлены endpoint'ы `/api/v1/transport/*` (clients/policies/validate/apply/rollback/conflicts/capabilities + `clients/{id}/metrics`).
- Реализованы `validate` и `apply` c anti-conflict guardrails, optimistic revision lock, confirm token для force override, snapshot предыдущей policy и SSE события.
- Добавлены unit-тесты для transport validator/token lifecycle (`selective-vpn-api/app/transport_handlers_test.go`), `go test ./...` проходит.
- Реализован allocator policy v2 для transport clients: резервные диапазоны `mark/pref`, автонормализация состояния, детерминированное восстановление и auto re-balance при коллизиях.
- Реализован `POST /api/v1/transport/policies/rollback`: откат из snapshot с проверкой ревизии и валидацией перед применением.
- Подготовлен документ UX предупреждений и подтверждения конфликтного apply (`docs/phase-e/E4_VALIDATE_CONFIRM_APPLY_UX.md`).
- В `E1`/`E4` добавлены подпункты UX для переключения/подключения engine (`singbox|dnstt|phoenix`): `desired_engine vs active_engine`, switch states, guardrails и rollback action.
- Усилен transport backend-контракт (D4.1) в Go API:
- `GET /api/v1/transport/clients/{id}/metrics`,
- унифицированные DTO `TransportClientLifecycleResponse`, `TransportClientHealthResponse`, `TransportClientMetricsResponse`,
- runtime-поля клиента (`backend`, `allowed_actions`, counters/uptime, last_error с code/message),
- lifecycle/health ответы теперь возвращают кодируемые backend-ошибки и метрики в одном формате.
- Добавлен foundation backend-адаптеров (D4.2) в Go:
- выбор backend по конфигу клиента (`runner=mock|systemd`, fallback в `mock`),
- `provision/lifecycle/health` действия идут через единый adapter слой (без дублирования логики в UI),
- добавлен `POST /api/v1/transport/clients/{id}/provision` для backend-side подготовки runner (systemd units),
- для `dnstt` добавлен режим `ssh_tunnel/ssh_overlay` с orchestration двух unit (`ssh_unit` + `unit`) как единой системы.
- Запущен пункт D4.2.1 (production templates):
- `config.exec_start` переведён в optional override;
- если override не задан, Go-ядро формирует `ExecStart` по шаблонам:
- `singbox`: `<bin> run -c <config_path>`,
- `dnstt`: resolver/pubkey/domain/local_addr,
- `phoenix`: `<bin> -config <config_path>`;
- добавлены unit-тесты `transport_backends_test.go` для manual override, template build и DNSTT required fields.
- Запущен пункт D4.2.2 (systemd restart/watchdog tuning):
- для transport unit добавлены настраиваемые `Restart/RestartSec`, `StartLimitIntervalSec/StartLimitBurst`, `TimeoutStartSec/TimeoutStopSec`, `WatchdogSec`;
- для `dnstt + ssh overlay` добавлены отдельные `ssh_*` overrides tuning-полей для SSH unit;
- добавлены unit-тесты рендера tuning-полей (`transport_backends_test.go`).
- Запущен пункт D4.2.3 (unit hardening):
- для systemd unit включён baseline hardening по умолчанию (`NoNewPrivileges`, `ProtectSystem`, `ProtectHome`, `RestrictSUIDSGID`, `UMask` и др.);
- добавлен профиль `strict` и режим `off` через `config.hardening_profile`, а также `config.hardening_enabled`;
- добавлены точечные overrides hardening-полей и `ssh_*` overrides для overlay unit;
- добавлены unit-тесты hardening рендера (`baseline`, disable, `ssh_hardening_enabled=false`).
- Запущен пункт D4.2.4 (`runtime_mode` architecture foundation):
- в `client.config` введён `runtime_mode` (`exec|embedded|sidecar`) с нормализацией alias (`external|companion -> exec`);
- `exec` использует текущую production-цепочку backend-адаптеров;
- `embedded/sidecar` пока отвечают унифицированной ошибкой `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED` (без silent fallback);
- `GET /api/v1/transport/capabilities` расширен матрицей `runtime_modes`.
- Запущен пункт D4.2.5 (exec packaging profiles):
- для template-команд добавлены профили установки бинарей: `packaging_profile=system|bundled`;
- для `bundled` добавлен `bin_root` (default `/opt/selective-vpn/bin`) и switch `packaging_system_fallback`;
- `require_binary=true` включает fail-fast проверку наличия бинаря (включая ручные `*_bin` override);
- `GET /api/v1/transport/capabilities` расширен `packaging_profiles`.
- Запущен пункт D4.2.6 (manual pinned packaging automation):
- добавлены скрипты `scripts/transport-packaging/update.sh` и `rollback.sh` (checksum verify + atomic symlink switch + history rollback);
- добавлен `manifest.example.json` для pinned версий/URL/checksum;
- добавлен `manifest.production.json` с pinned артефактами для `singbox` (`v1.13.2`) и `phoenix` (`v1.0.1`) под `linux-amd64/linux-arm64`; для `dnstt` зафиксирован prebuilt-source (checksum pinned), но компонент по умолчанию `enabled=false` до trusted-source/signature этапа;
- добавлен foundation trusted source/signature/canary policy:
- `scripts/transport-packaging/source_policy.production.json` (trusted URL-prefix + signature mode policy);
- `update.sh` поддерживает `--source-policy`, `signature.type=openssl-sha256`, `--rollout-stage stable|canary|any`, `--cohort-id`, `--force-rollout`/`--canary`;
- для `manifest.production.json` policy подхватывается автоматически (если `source_policy.production.json` рядом);
- добавлен локальный smoke `tests/transport_packaging_smoke.sh` и включён в `tests/run_all.sh`;
- добавлен дополнительный smoke `tests/transport_packaging_policy_rollout.sh` (source trust + signature verify + canary gating);
- default остаётся manual, но добавлен opt-in auto-update слой.
- Запущен пункт D4.2.7 (auto-update opt-in):
- добавлен `scripts/transport-packaging/auto_update.sh` (interval gate + jitter + flock lock + state files);
- добавлены systemd-шаблоны `scripts/transport-packaging/systemd/transport-packaging-auto-update.{service,timer}` и env-шаблон;
- добавлен smoke `tests/transport_packaging_auto_update.sh` и включён в `tests/run_all.sh`.
- Запущен пункт D4.2.8a (singbox backend e2e):
- добавлен `tests/transport_singbox_e2e.py`:
- успешный lifecycle на `runner=mock` (`provision/start/health/restart/stop/metrics`);
- negative guard `runtime_mode=embedded` -> `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED`;
- fail-fast `require_binary=true` + missing `singbox_bin` -> `TRANSPORT_BACKEND_PROVISION_CONFIG_REQUIRED`;
- тест подключён в `tests/run_all.sh`.
- Запущен пункт D4.2.8b (dnstt backend e2e):
- добавлен `tests/transport_dnstt_e2e.py`:
- успешный lifecycle на `runner=mock`;
- guard для `ssh_overlay` конфигурации (`ssh_host` обязателен, `ssh_unit` должен быть валидным) -> `TRANSPORT_BACKEND_PROVISION_CONFIG_REQUIRED`;
- guard валидации шаблона DNSTT при неполном config -> `TRANSPORT_BACKEND_PROVISION_CONFIG_REQUIRED`;
- тест подключён в `tests/run_all.sh`.
- Запущен пункт D4.2.8c (phoenix backend e2e):
- добавлен `tests/transport_phoenix_e2e.py`:
- успешный lifecycle на `runner=mock` (`provision/start/health/restart/stop/metrics`);
- guard `runtime_mode=embedded` -> `TRANSPORT_BACKEND_RUNTIME_MODE_UNSUPPORTED`;
- fail-fast `require_binary=true` + missing `phoenix_bin` -> `TRANSPORT_BACKEND_PROVISION_CONFIG_REQUIRED`;
- тест подключён в `tests/run_all.sh`.
- Верификация D4.2.8 на live backend (2026-03-07):
- `selective-vpn-api` пересобран из текущего кода и перезапущен через systemd;
- `./tests/run_all.sh` выполнен полностью без `SKIP` на `transport_singbox_e2e`, `transport_dnstt_e2e`, `transport_phoenix_e2e`;
- подтверждён рабочий контракт `provision/start/health/restart/stop/metrics` и negative guards по каждому transport-клиенту.
- Запущен пункт D4.2.9 (операционные runbook'и):
- добавлен `scripts/transport_runbook.py` для воспроизводимого lifecycle-flow через API (`capabilities/create/provision/start/health/metrics/restart/stop/delete`);
- добавлен smoke `tests/transport_runbook_cli_smoke.sh` (mock singbox lifecycle через runbook helper);
- smoke подключён в `tests/run_all.sh`.
- Запущен пункт D4.2.10 (real-systemd e2e + cleanup):
- в backend добавлен cleanup для `runner=systemd` при `DELETE /api/v1/transport/clients/{id}`:
- удаляются только unit-файлы с ownership-marker `SVPN_TRANSPORT_ID=<client_id>`;
- выполняется `stop/disable`, затем `daemon-reload` + `reset-failed` (best-effort, с предупреждением в `message`, не блокируя delete);
- добавлен тест `tests/transport_systemd_real_e2e.py`:
- lifecycle для `singbox`, `dnstt(+ssh_unit)`, `phoenix` на реальном `systemd` backend (`runner=systemd`, `exec_start=/usr/bin/sleep`);
- проверка, что unit-файлы создаются с ownership-marker и удаляются после delete cleanup;
- тест подключён в `tests/run_all.sh`.
- Запущен пункт D4.2.11 (production-like transport e2e):
- добавлен тест `tests/transport_production_like_e2e.py`;
- проверяется lifecycle `provision/start/health/metrics/restart/stop/delete` для `singbox|dnstt(+ssh)|phoenix` на `runner=systemd`;
- вместо manual `exec_start` проверяются template-команды и `packaging_profile=bundled` с `bin_root`/`require_binary=true`;
- проверяется, что в unit-файлах присутствуют ожидаемые template-аргументы (`run -c`, `-config`, `-doh`/`domain`/`local_addr`) и ownership marker;
- тест подключён в `tests/run_all.sh`.
- Запущен пункт D4.2.12 (recovery runbook):
- добавлен `scripts/transport_recovery_runbook.py`:
- проверяет health клиента, делает до `N` попыток `restart`,
- при необходимости выполняет fallback `provision -> start`,
- при нерешённой деградации пишет diagnostics (`metrics`, `health`, `client_card`, шаги recovery) и возвращает `rc=2`;
- добавлен smoke `tests/transport_recovery_runbook_smoke.sh`:
- case1: успешное восстановление (`mock+exec`) -> `rc=0`,
- case2: ожидаемый fail-path (`runtime_mode=embedded`) -> `rc=2` + diagnostics file;
- smoke подключён в `tests/run_all.sh`.
- Запущен пункт D4.3 (platform compatibility matrix):
- добавлен артефакт `docs/phase-d/D4_PLATFORM_COMPATIBILITY_MATRIX.md` с матрицей `web + iOS + Android`;
- зафиксированы ограничения runtime (`exec=true`, `embedded/sidecar=false`) и правило backend-only orchestration для `singbox/dnstt/phoenix`;
- добавлен smoke `tests/transport_platform_compatibility_smoke.py` (capabilities + policy-contract checks), тест подключён в `tests/run_all.sh`.
- Верификация D4.3 на live backend (2026-03-07):
- `env API_URL=http://127.0.0.1:8080 ./tests/transport_platform_compatibility_smoke.py` -> `passed`;
- полный `./tests/run_all.sh` выполнен успешно с включённым `transport_platform_compatibility`.
- Запущен пункт D4.2.13 (singbox bootstrap-bypass):
- добавлен backend-модуль `app/transport_bootstrap_bypass.go`;
- для `runner=systemd` в `start/restart/stop` добавлена синхронизация bypass-маршрутов endpoint'ов transport-клиента;
- `singbox` endpoint-hosts извлекаются из `client.config` и `singbox` config-файла (`outbounds[*].server/address/host`);
- перед `start/restart` backend вычисляет `main` default-route и ставит `ip -4 route replace <endpoint>/32 table agvpn ...` (избежание bootstrap через `tun0`);
- на `stop` backend удаляет привязанные bypass-маршруты клиента;
- добавлен strict-режим `config.bootstrap_bypass_strict=true` (ошибка `TRANSPORT_BACKEND_BOOTSTRAP_BYPASS_FAILED` при невозможности применить bypass);
- добавлены unit-тесты `transport_bootstrap_bypass_test.go`, `go test ./...` проходит.
- Запущен пункт D4.2.14 (netns test contour):
- добавлен backend-модуль `app/transport_netns.go`:
- `netns` setup перед `start/restart` (veth pair, `ip_forward`, `nft` masquerade),
- optional cleanup на `stop` (`config.netns_auto_cleanup=true`),
- cleanup при backend `DELETE client` через `Cleanup()` (best-effort);
- `Provision()` для systemd поддерживает запуск transport внутри namespace через adaptive exec (`nsenter` default, fallback `ip netns exec`) при `config.netns_enabled=true`;
- для безопасного fail-fast добавлен strict-режим `config.netns_setup_strict=true` (`TRANSPORT_BACKEND_NETNS_SETUP_FAILED`);
- добавлены unit-тесты `transport_netns_test.go`, `go test ./...` проходит.
- Запущен пункт D4.2.15 (sing-box DNS migration):
- добавлен модуль `app/transport_singbox_dns_migration.go` (best-effort миграция legacy DNS в typed format);
- migration trigger встроен в `Provision()` для `singbox`:
- `dns.servers[*].address` -> `dns.servers[*].type/server/server_port`,
- `address_resolver/address_strategy` -> `domain_resolver/domain_strategy`,
- удаление `detour=direct` для DNS servers (невалидно для новых версий),
- backup оригинала: `<config>.legacy-dns.bak`;
- добавлены config-флаги:
- `singbox_dns_migrate_legacy` (default `true`),
- `singbox_dns_migrate_strict` (fail-fast с `TRANSPORT_BACKEND_SINGBOX_DNS_MIGRATE_FAILED`);
- добавлены unit-тесты `transport_singbox_dns_migration_test.go`;
- проверено на live client `sg-realnetns`: warning `legacy DNS servers is deprecated` исчез, transport после миграции поднимается и трафик через SOCKS проходит.
- В `selective-vpn-gui` добавлены transport методы в `api_client.py` и transition-логика в `dashboard_controller.py`:
- `draft -> validate -> (validated|risky) -> confirm -> apply`,
- обработка `POLICY_REVISION_MISMATCH` с возвратом в `draft`,
- общий foundation для последующего web/iOS/Android UX.
- Запущен пункт E4.3.1 (GUI engine foundation):
- в `selective-vpn-gui` на вкладке `AdGuardVPN` добавлен блок `Transport engine`;
- реализованы действия `Prepare/Connect/Disconnect/Restart` для выбранного transport-клиента через Go API;
- Запущен пункт E5.4.4 (GUI protocol editor flow):
- `Save draft` теперь сохраняет VLESS raw profile через `POST/PATCH /api/v1/transport/singbox/profiles/*` (через controller/api_client);
- при смене transport engine в `SingBox` вкладке editor автоматически подгружает профиль из Go API и отключается при `API unavailable/нет выбранного клиента`;
- `Preview/Validate/Apply` перед вызовом action теперь автоматически синхронизируют текущий draft из формы (guardrail: валидация обязательных client-полей до API action).
- добавлено состояние выбранного engine (status/iface/table/latency/last_error) и авто-обновление по transport SSE-событиям;
- расширен client/controller слой: `ApiClient.transport_client_action()` + `DashboardController.transport_client_action()`.
- Запущен пункт E5.4.5 (dashboard cards + context menu):
- карточки `Connection profiles` переведены на widget-плитки со state-driven стилями;
- активный runtime-профиль (`status=up`) подсвечивается зелёным;
- добавлено правокликовое меню по карточке: `Run`, `Edit`, `Delete`;
- `Edit` открывает отдельный modal-диалог и использует тот же VLESS form-editor (inline-редактор скрыт из основной вкладки);
- для `Delete` добавлен GUI/API flow с поддержкой `force=true` при policy-ссылках.
- Запущен пункт E5.4.6 (editor usability + profile creation):
- `security/transport` в VLESS editor больше не сбрасывают введённые значения при переключении режима;
- `Flow` переведён в editable режим: preset `xtls-rprx-vision` + возможность custom/raw значения;
- в блок `Connection profiles` добавлена кнопка `Create connection` с режимами:
- `Create from clipboard` (парсинг `vless://` из буфера),
- `Create from link...` (вставка URL),
- `Create manual` (пустой профиль с последующим edit);
- для создания профиля добавлен GUI/API create-flow transport-клиента (`POST /api/v1/transport/clients`) и auto-seed editor + optional draft save.
- Запущен пункт E5.4.7 (unified protocols import):
- link-import больше не завязан на VLESS: добавлен общий dispatcher по схеме URL;
- реализованы парсеры и raw-profile build для `vless/trojan/ss/hysteria2(hy2)/tuic`;
- добавлен общий набор helper-функций (query/tls/transport/raw route/inbound builder), чтобы следующие протоколы подключались без повторения кода;
- для non-VLESS профилей сохраняется raw-конфиг и доступен lifecycle (`Run/Validate/Apply`) через тот же pipeline.
- Запущен пункт E5.4.8 (multi-protocol form editor):
- `Protocol` в editor переключается между `vless/trojan/shadowsocks/hysteria2/tuic`;
- форма стала модульной: protocol-specific поля (password/ss_method/hy2_obfs/tuic options) показываются/скрываются по выбранному протоколу;
- `Save draft` теперь пишет `protocol=<selected>` и генерирует соответствующий outbound raw-конфиг;
- сохранены guardrails по transport/security и обязательным полям каждого протокола.
- Запущен пункт E5.4.9 (wireguard in common editor):
- добавлен protocol `wireguard` в тот же form-editor, без отдельной ветки UI;
- добавлены поля `private_key`, `peer_public_key`, `pre_shared_key`, `local_address`, `reserved`, `mtu`;
- включена валидация обязательных WG полей и сохранение в raw outbound `type=wireguard`;
- расширен universal link-import parser: поддержка `wireguard://` (и alias `wg://`) в `Create connection`.
- Запущен пункт E4.3.2 (SingBox switch pipeline):
- блок engine вынесен в отдельную вкладку `SingBox` (отдельно от `AdGuardVPN`);
- `Connect/Switch` теперь выполняет policy pipeline `validate -> confirm -> apply` и только потом `start`;
- при блокирующих конфликтах UI показывает confirm-диалог с force apply;
- добавлена кнопка `Rollback policy` (через `POST /api/v1/transport/policies/rollback`).
- Запущен пункт E5.1 (requirements freeze для протоколов SingBox):
- добавлен документ `docs/phase-e/E5_SINGBOX_PROTOCOLS_REQUIREMENTS.md`;
- зафиксированы архитектурные границы (`engine` vs `policy` vs `protocol profile`);
- зафиксированы требования к новой группе API `/api/v1/transport/singbox/profiles/*` (`CRUD/validate/render/apply/rollback/history/features`);
- зафиксированы требования к `typed + raw` режимам, secrets storage, versioning и SSE событиям.
- Запущен пункт E5.1.2 (protocol field inventory):
- добавлен шаблон `docs/phase-e/E5_SINGBOX_PROTOCOL_MATRIX_TEMPLATE.md`;
- добавлена заполненная матрица `docs/phase-e/E5_SINGBOX_PROTOCOLS_MATRIX.md` (общие блоки + 6 протоколов + guardrails + MVP/Advanced/Raw-only split);
- добавлен machine-readable пример `docs/phase-e/E5_SINGBOX_PROTOCOLS_MANIFEST.example.json` для последующей генерации UI-форм.
- Запущен пункт E5.1.3 (client form baseline):
- добавлен документ `docs/phase-e/E5_SINGBOX_CLIENT_FORM_MATRIX.md`;
- зафиксированы UI-блоки клиентской формы (`Profile`, `Server/Auth`, `Transport`, `Security`, `Sniffing`, `Advanced Dial`);
- явно исключены server-only поля (billing/traffic/expiry/subscription) и добавлены guardrails для `VLESS+Reality`.
- Запущен пункт E5.4.1 (desktop dashboard foundation):
- добавлен документ `docs/phase-e/E5_2_SINGBOX_DESKTOP_DASHBOARD_SPEC.md`;
- вкладка `SingBox` перестроена на 3 зоны: runtime card, profile settings, global defaults;
- внедрён `card-based dashboard`: верхние metric cards + grid profile cards (с выбором карточки);
- включён compact UX: настройки и activity log открываются кнопками (по умолчанию скрыты);
- добавлены `Use global` override-переключатели, effective summary и локальное сохранение настроек;
- runtime pipeline `Prepare/Connect-Switch/Disconnect/Restart/Rollback` сохранён без изменения API-контракта.
- Запущен пункт E5.4.2 (GUI wiring profile actions):
- в `selective-vpn-gui/api_client.py` добавлены typed-модели и методы для `POST /api/v1/transport/singbox/profiles/{id}/validate|apply`;
- в `selective-vpn-gui/dashboard_controller.py` добавлены `singbox_profile_validate_action()` и `singbox_profile_apply_action()` с унифицированным `ActionView`;
- в `selective-vpn-gui/vpn_dashboard_qt.py` заглушки `Validate profile`/`Apply profile` заменены на реальные вызовы API с логированием в activity-log и авто-refresh runtime state.
- Запущен пункт E5.4.3 (profile flow completeness + UX hardening):
- в `selective-vpn-gui/api_client.py` добавлены методы `profiles list/get/create/patch/render/rollback/history` и соответствующие typed DTO;
- в `selective-vpn-gui/dashboard_controller.py` добавлены:
- `singbox_profile_render_preview_action()`,
- `singbox_profile_rollback_action()`,
- `singbox_profile_history_lines()`,
- `singbox_profile_ensure_linked()` с auto-create профиля из `config_path` (raw mode) и auto-link `meta.client_id`;
- в `selective-vpn-gui/vpn_dashboard_qt.py` добавлены кнопки `Preview render`, `Rollback profile`, `History`;
- добавлена авто-синхронизация `engine -> profile` при выборе/refresh engine (`_sync_selected_singbox_profile_link`);
- выполнен лёгкий рефакторинг profile handlers (`_selected_singbox_profile_context`, `_run_singbox_profile_action`) без смены backend-контракта.
- Запущен пункт B3 (resolver diff + improvement plan):
- добавлен документ `docs/phase-b/B2_RESOLVER_DIFF_AND_IMPROVEMENT_PLAN.md`;
- зафиксированы границы ролей `system resolver` (authoritative для PBR) и `singbox DNS` (transport-level resolver);
- зафиксирован приоритетный backlog улучшений resolver (`R1-R4`: надёжность, качество резолва, observability API/SSE, multi-client ownership safety).
- Запущен пункт F1.1 (план модульности):
- добавлен документ `docs/phase-f/F1_REFACTOR_MODULARITY_PLAN.md`;
- зафиксированы целевые разрезы для `vpn_dashboard_qt.py`, `api_client.py`, `dashboard_controller.py`, `transport_handlers.go`;
- зафиксирован безопасный порядок реализации и DoD без изменения API-контракта.
- Текущий статус: transport backend/API foundation и базовый desktop UI-flow включены (`SingBox` tab: lifecycle actions + switch pipeline + rollback).
- Зафиксирован план по VPN локациям:
- без большой кнопки `Refresh`/`Apply`, но с лёгким icon-trigger refresh (фоновый, неблокирующий),
- в будущем без кнопки `Apply location` (выбор в списке сразу инициирует connect/reconnect),
- список грузится асинхронно, UI не блокируется при недоступном CLI/большом latency,
- используется кеш последнего успешного списка + SWR (stale-while-revalidate) + backoff/retry + single-flight lock.
- Диагностирован кейс `eu.posthog.com` (страница "region unavailable" в браузере при включенном wildcard):
- root cause: `smartdns` runtime был с `nftset-timeout yes`, а `agvpn_dyn4` создан без `timeout`-флага (`flags interval`), из-за чего runtime-добавление IP не происходило;
- applied fix: `nftset-timeout no` в `smartdns.conf`;
- verification: после `smartdns-local` restart + DNS query через `127.0.0.1#6053` все актуальные `A`-IP `eu.posthog.com` появились в `inet/agvpn/agvpn_dyn4`.
- Диагностирован второй корень проблемы для `selective`:
- `routes/update` перезаписывал `agvpn_dyn4` только списком из resolver cache, из-за чего runtime-IP (SmartDNS nftset) периодически терялись;
- applied fix в Go (`routes_update.go`): merge `resolver wildcard IPs` + `existing agvpn_dyn4` при `runtime_nftset=true`;
- verification: после `smartdns` query и `POST /api/v1/routes/update` PostHog IP (`3.121.142.0`, `3.67.52.82`, `18.158.106.188`, `63.183.90.15`, `63.177.143.4`, `63.182.85.19`) не исчезают из `agvpn_dyn4`.
- По запросу добавлен временный static fallback для PostHog в `/etc/selective-vpn/static-ips.txt`:
- `3.121.142.0/24`, `3.67.52.0/24`, `18.158.106.0/24`, `63.177.143.0/24`, `63.182.85.0/24`, `63.183.90.0/24`;
- verification: после `routes/update` подсети присутствуют в `inet/agvpn/agvpn4`.
- Расширено покрытие PostHog-хостов для selective:
- в wildcard state добавлены `app-static.eu.posthog.com` и `internal-j.posthog.com`;
- добавлен временный static fallback для `internal-j.posthog.com`: `3.88.247.0/24`, `3.95.129.0/24`, `54.90.36.0/24`;
- verification: по packet capture (`tcpdump`) HTTPS к `internal-j.posthog.com` уходит через `tun0` (маркировка срабатывает).
- Финальная проверка: пользователь подтвердил, что `eu.posthog.com` в режиме `selective` открывается и работает штатно.
- Реализован backend cache/SWR для `/api/v1/vpn/locations`:
- мгновенный ответ из кеша (`/var/lib/selective-vpn/vpn-locations-cache.json`),
- фоновый refresh `list-locations` с single-flight lock,
- метаданные ответа: `updated_at`, `stale`, `refresh_in_progress`, `last_error`, `next_retry_at`,
- backoff на ошибках, SSE-событие `vpn_locations_changed`.
- Реализована стабилизация GUI по локациям:
- загрузка списка через отдельный `QThread` (без блокировки главного потока),
- убрана кнопка `Apply & restart loop`, включён auto-apply при выборе локации в `QComboBox`,
- добавлена status/meta строка для состояния кеша/обновления/ошибок.
- Реализован V2 "умный поиск" без отдельного поля ввода:
- typed-buffer в списке локаций с live-фильтрацией и сортировкой по релевантности,
- матч по началу ISO/строки/слова, backspace уменьшает фильтр,
- таймаут-сброс буфера.
- Исправлена точность применения VPN-локации:
- вместо `ISO` в `connect -l` теперь передаётся точный `target` локации (например `United States Los Angeles`),
- устранён кейс, когда в UI выбран один город, а autoloop подключается к другому дефолтному городу страны.
- Усилен safety flow для `set location` (L1 hardening):
- backend теперь резолвит пользовательский выбор в безопасный `connect`-аргумент (`city` или `ISO`) по каталогу локаций,
- при нераспознанной локации возвращается `422` без рестарта autoloop (текущий туннель сохраняется),
- в GUI при apply передаётся `target + iso + label`, чтобы повысить точность матчинга и убрать ложные реконнекты.
- Добавлены проверки для блока локаций:
- `tests/vpn_locations_swr.sh`,
- unit-тесты parser-а `selective-vpn-api/app/vpn_locations_cache_test.go`.
- Smoke-набор `tests/run_all.sh` обновлён и прогнан успешно (включая новый `vpn_locations_swr`).
- В GUI локаций `Sort/Refresh` оставлены снаружи списка (рядом с `Location`), popup содержит только локации.
- `Refresh` отправляет trigger `GET /api/v1/vpn/locations?refresh=1` в фоне через `LocationsThread`, UI не блокируется.
## Следующие шаги
- M1 (priority): закрыть backend multi-interface foundation:
- interface orchestrator (`E3.3`), ownership registry (`E3.4`), anti-mixing guard (`E3.5`), transaction pipeline (`E3.6`);
- критерий: минимум 2 engine работают одновременно без пересечения table/mark/pref и без ложного egress mixing.
- M2 (priority): довести runtime наблюдаемость (`E6.6`) для всех engine в едином контракте ядра.
- M3 (priority): после backend foundation включить GUI-флоу выбора/переключения engine-профилей поверх нового orchestration-слоя (тонкий UI, без бизнес-логики в GUI).
- E5.4: реализовать GUI-блок протоколов в `SingBox` вкладке (list/editor/validate/preview/apply/rollback).
- B3.1: реализовать resolver status snapshot API + SSE (`resolver_status_changed`, `resolver_refresh_completed`).
- B3.2: реализовать adaptive upstream scoring + negative cache TTL policy.
- B3.3: внедрить `domain_owner_map` и conflict guard для multi-client DNS ownership.
- D4.2 (supporting track): поддерживать backend-ready состояние `dnstt-client` и `phoenix` без UI-расширения в текущем этапе.
- P1.1: спроектировать `service profiles` (например, `PostHog`) как пресеты доменов/wildcard/static-fallback/policy.
- P1.2: добавить API для профилей (`list/apply/remove/export`) и idempotent apply в Go-ядре.
- P1.3: добавить в GUI чекбоксы/тогглы профилей с применением в 1 клик и rollback.
- P1.4: подготовить переносимый формат профилей для web + iOS + Android (единый JSON schema).
- V2.2: собрать пользовательский отчёт по UX поиска/автоприменения локаций в desktop (latency, false matches, edge-cases).
- V2.3: при подтверждении UX перенести одинаковую модель локаций (`cache/SWR + typed-buffer + auto-apply`) в web/mobile клиенты.
- Доработать Phase B (описание контроллеров, зависимости, условия запуска).
- Завершить Phase D (матрица endpoint со статусом `web-ready` и явные блокеры для веб-прототипа).
- Формализовать реализацию D3.1-D3.4 (gateway/auth, web старт, mobile transport profile, запуск iOS/Android на общем API).
- E4.4: перенести foundation state-machine в web prototype UI (`React + TypeScript` на `Vite`) без изменения API-контракта Go.
- E4.5: добавить настройки видимости protocol tabs (включать/выключать вкладки `SingBox/DNSTT/Phoenix`) через UI settings/profile.
- F1.4: выполнить декомпозицию `selective-vpn-gui/vpn_dashboard_qt.py` на tab/modules и вынести event/locations сервисы.
- F1.11: вынести переиспользуемую transport-логику в подпакеты (`app/transport/*`) для частей без циклических зависимостей (через facade+deps).
- F1.11.r1: продолжить resolver-декомпозицию в отдельной папке `app/resolver/*` через bridge-слой (`domain cache`, `adaptive live-batch`, `io/json helpers`) без смены внешнего поведения.
- F2.1 (post-refactor): перейти на `singbox@.service` как целевой production-runtime (instance-per-profile + без лавины unit-файлов).
- F2.2 (post-refactor): сделать управляемую миграцию/cleanup старых `singbox-*.service` и зафиксировать runbook для деплоя/отката.
## Дальний backlog (после завершения текущего приложения)
- L1.1: `AmneziaWG` интеграция как отдельный transport executor/backend kind (`amneziawg`) через единый Go control-plane API.
- L1.2: поддержка `AmneziaWG` в GUI как отдельный engine/profile workflow (без смешивания с текущим `wireguard`/`singbox` этапом).
- L1.3: обновление capability-матрицы, runbook/e2e и packaging-профилей для `amneziawg` после полного закрытия приоритетных задач desktop (`SingBox` + core stability).