Releases: Leadaxe/LxBox
LxBox v1.8.2
L×Box v1.8.2
Final fix дублирования версии. v1.8.0 показывал v1.7.0 (hardcoded const забыли bump'ать) → v1.8.1 добавил CI guard → v1.8.2 убирает дубль вообще. Tag теперь единственный источник правды.
Кому актуально: всем, кто на v1.8.x. Это infrastructure release — user-facing behaviour не меняется (after install версия в About показывается корректно как и в v1.8.1), но release-flow стал чище: больше нет bump-коммитов в репо при выпуске релиза.
Quick links:
🔧 Refactor ·
🛠 Release flow ·
🔬 Verified ·
🇷🇺 На русском
🔧 Refactor
Tag = single source of truth для версии
§065 spec. До этого версия дублировалась в:
app/pubspec.yamlversion: X.Y.Z+Napp/lib/screens/about_screen.dartstatic const _version = 'X.Y.Z'- git tag
vX.Y.Z
Три места → три ошибки. v1.8.0 ▼: забыл (2), UI показал v1.7.0. v1.8.1 ▼: CI consistency check валидировал, но bump всё равно ручной в 2 файлах.
Новая модель:
pubspec.yamlнавсегдаversion: 0.0.0-dev+0(placeholder, не правится)- CI перед build'ом sed'ит pubspec из
${tag#v}+git rev-list --count HEAD - Local
scripts/build-local-apk.shтоже derive'ит + revert черезtrap EXIT - About screen + UpdateChecker →
VersionInfo.I.version(loaded изPackageInfo.fromPlatform()вmain()передrunApp)
Файлы:
app/lib/services/version_info.dartNEWapp/lib/main.dart—await VersionInfo.I.init()передrunAppapp/lib/screens/about_screen.dart—_versionconst удалён;versionStringalias удалёнapp/lib/screens/home_screen.dart+app_settings_screen.dart+services/debug/handlers/action.dart— 6 occurrence'овAboutScreen.versionString→VersionInfo.I.versionapp/pubspec.yaml→ placeholder.github/workflows/ci.yml— добавлен «Inject release version» step (sed pubspec из meta.outputs.version + commits count); удалён «Version consistency check» (нечего сверять)scripts/build-local-apk.sh— sed pubspec из git describe + revert trap
🛠 Release flow
До v1.8.2
1. bump pubspec.yaml → 1.8.1+34 → 1.8.2+35
2. bump about_screen.dart → '1.8.1' → '1.8.2'
3. CHANGELOG / RELEASE_NOTES / docs/releases/vX.Y.Z.md / DEV_REPORT chronicle
4. commit «chore(release): bump 1.8.2» (5+ файлов)
5. merge develop→main
6. tag v1.8.2 + push tag
После v1.8.2
1. CHANGELOG / RELEASE_NOTES / docs/releases/vX.Y.Z.md / DEV_REPORT chronicle
2. commit «docs(release): v1.8.2 notes» (3 файла, никаких code/pubspec bump)
3. merge develop→main
4. tag v1.8.2 + push tag
Два шага меньше, ноль шанса забыть bump.
CI deltа:
- Version consistency check (sed-grep pubspec ↔ about_screen ↔ tag, fail mismatch)
+ Inject release version (sed pubspec from ${tag#v}+${git rev-list --count HEAD})Local build (scripts/build-local-apk.sh):
- Если на tagged commit'е →
versionName = X.Y.Z - Если между тегами →
versionName = X.Y.Z-dev.N(N commits since last tag) versionCode = git rev-list --count HEAD(same formula что CI)trap "git checkout -- app/pubspec.yaml" EXITдля revert после build
🔬 Verified
После tagged release v1.8.2:
- CI собрал APK с
versionName=1.8.2,versionCode=N(= commits count HEAD) adb install -rповерх v1.8.1 → successdumpsys package | grep version→ корректно- App → Settings → About →
v1.8.2 - UpdateChecker → silent (current == latest)
- Debug API
/state.app_version→1.8.2+N - Backup
source_app_version→1.8.2+N
Все surfaces consistent.
🇷🇺 LxBox v1.8.2 на русском
Что изменилось в инфраструктуре (для пользователя ничего видимого):
- Версия приложения теперь берётся только из git тега, никаких параллельных источников
- В коде убрана hardcoded константа
_version = '1.8.1'— больше не нужно её править при релизе - В
pubspec.yamlнавсегда стоит placeholder0.0.0-dev+0, CI/local build переписывает её перед сборкой - Бамп-коммитов в репо при релизе больше нет — только docs
Для разработчика:
- При
flutter runв About покажетсяv0.0.0-dev— это явный signal «это dev session, не release build». Если нужна правильная версия —scripts/build-local-apk.sh(он derive'ит изgit describe) - При release: edit CHANGELOG/RELEASE_NOTES/docs → commit → merge →
git tag vX.Y.Z→git push origin vX.Y.Z→ всё. CI остальное сделает.
В фоне есть один потенциальный quirk — на 0.0.0-dev UpdateChecker предложит «обновитесь до vX.Y.Z». Это технически корректно (0.0.0 < anything), но для dev сессий может быть слегка раздражающим. Пока не fix'ил; если будет проблема — добавим guard «if version contains '-dev' skip update check».
🔗 Refs
- Previous: v1.8.1
- Spec: §065 — Version from git tag
- Release process: docs/RELEASE_PROCESS.md §2.2
- CHANGELOG:
## [1.8.2] - CI workflow: .github/workflows/ci.yml
- Local build: scripts/build-local-apk.sh
LxBox v1.8.1
L×Box v1.8.1
Hotfix v1.8.0. UI показывал v1.7.0 на правильно собранном v1.8.0 APK — потому что static const _version в About screen не был поднят при release-bump'е. Patch фиксит UI + добавляет CI guard чтобы это не повторилось.
Что увидит юзер v1.8.0: после обновления Settings → About покажет v1.8.1, snackbar «v1.8.0 available» (некорректно выдаваемый на v1.8.0) пропадёт.
Кому актуально: всем, кто установил v1.8.0. Если ты ещё на v1.7.x — этот hotfix можно пропустить, ставь сразу v1.8.1 (включает все v1.8.0 changes).
Quick links:
🐞 Fix ·
🛡 CI guard ·
📚 Process ·
🇷🇺 На русском
🐞 Fix
About screen + UpdateChecker показывали v1.7.0 на v1.8.0 build
app/lib/screens/about_screen.dart:13.
В app'е две независимых записи версии:
pubspec.yamlversion: X.Y.Z+N— рисует AndroidversionName/versionCode, попадает вPackageInfo.fromPlatform()app/lib/screens/about_screen.dartstatic const _version = 'X.Y.Z'— sync compile-time const, читается About screen +UpdateChecker.checkForUpdate()
При release v1.8.0 поднял (1), забыл (2). Эффект:
Settings → Aboutпоказывалv1.7.0UpdateCheckerсравнивалversionString='1.7.0'с latest GitHub tagv1.8.0→ рекомендовал «обновитесь до 1.8.0» сразу после установки v1.8.0
PackageInfo корректно отдавал 1.8.0 — поэтому backup-файлы (source_app_version берётся оттуда) и Debug API /state показывали правильно. Единственный affected surface — синхронные const-зависимые места в UI.
Fix: bump _version to 1.8.1 (one-liner).
🛡 CI guard
Чтобы избежать повторения — новый step в .github/workflows/ci.yml → checks job → «Version consistency check»:
- name: Version consistency check
shell: bash
run: |
pubspec_version=$(grep -E "^version:" pubspec.yaml | sed -E 's/^version: ([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
about_version=$(grep -E "_version = '" lib/screens/about_screen.dart | sed -E "s/.*_version = '([^']+)'.*/\1/")
...
if [ "$pubspec_version" != "$about_version" ]; then
echo "::error file=...::pubspec.yaml version != about_screen.dart _version"
exit 1
fiСверяет:
pubspec.yamlversion:↔about_screen.dart_version— всегда- Git tag
vX.Y.Z↔pubspec.yaml— только на tag run
Mismatch → CI fail до build APK, релиз с расхождением не выйдет.
📚 Process
docs/RELEASE_PROCESS.md §2.2 «Bump версии» переписан — теперь явно перечислены оба места куда писать версию, объяснение почему дубль (compile-time const vs async PackageInfo), ссылка на CI guard.
🇷🇺 LxBox v1.8.1 на русском
Что случилось: v1.8.0 был выпущен с забытой константой версии в About screen → показывал v1.7.0 и предлагал обновиться до v1.8.0 сразу после установки v1.8.0. Сам APK был правильно собран (Android versionName=1.8.0), глюк был чисто в UI/UpdateChecker.
Что в hotfix'е:
- Константа поднята до
1.8.1 - В CI добавлена проверка консистентности — расхождение
pubspec.yaml/about_screen.dart/ git tag → CI fail до сборки. Больше так не вылезет. - Обновлён release process — теперь явно прописаны оба места для bump'а.
Backup-файлы пере-делать не нужно — source_app_version в них шёл через PackageInfo (корректно 1.8.0), only UI и UpdateChecker были affected.
🔗 Refs
- Previous: v1.8.0
- Release process: docs/RELEASE_PROCESS.md §2.2
- CHANGELOG:
## [1.8.1] - CI guard: .github/workflows/ci.yml
LxBox v1.8.0
L×Box v1.8.0
«Backup overhaul + routing order fix» release. Главное:
- §063 / §040 — backup format переписан под полный snapshot
lxbox_settings.json+ native VPN toggles. Старый формат silently терял
большинство user data (custom_rules,tun_apps,enabled_groups,
route_final,rule_outbounds,dns_options). Breaking: старые
backup-файлы reject'ятся при import — пере-export после обновления. - §062 — custom_rules cross-kind order fix: storage order теперь end-to-end
управляемый между preset/inline/srs (раньше builder ломал cross-kind
ordering двумя независимыми проходами). - §053 — editor split Stage 1+2+3:
custom_rule_edit_screen.dart
2060 → 456 LOC (−77%) через секции / tabs /CustomRuleEditController
(ChangeNotifier).
Quick links:
🐞 Fixes ·
✨ New ·
🔧 Changed ·
🏗 Refactor ·
📚 Docs ·
🇷🇺 На русском
🐞 Fixes
§062 — custom_rules order broken между kind-ами (preset/inline/srs)
SettingsStorage.custom_rules это один список с mixed kind, и
UI/Debug API (POST /rules/reorder) предполагают что order этого списка =
order matching в sing-box route.rules[] (first-wins сверху вниз).
Bug: builder делал 2 прохода — applyPresetBundles (только kind:preset)
→ applyCustomRules (только kind:inline|srs) — поэтому в финальном
sing-box config все preset правила оказывались перед всеми inline/srs
независимо от storage order. Юзер ставил «RU apps inline» между
«Private IPs preset» и «Russian domains preset» в Routing → Rules, но
inline всегда уезжал в самый конец route.rules[]. Reorder API «провёртывался
вхолостую» в плане эффекта на routing.
Fix: новый applyAllCustomRules обходит rules в одном цикле с
dispatch по kind. Per-rule logic вынесена в private _applyPresetSingle /
_applyInlineSingle / _applySrsSingle. Старые public
applyPresetBundles / applyCustomRules остались как shim через те же
private — backward-compat для тестов.
Cross-preset rule_set dedup переехал с mergeFragments на
RuleSetRegistry.tryRegisterRuleSet (identical-skip / first-wins warning) —
работает естественно при per-rule обходе.
Verified on device: storage [Block Ads, Private IPs, RU apps inline, Russian domains, Russia-only, BitTorrent] теперь даёт config
[ads-all, ip_is_private, RU apps, ru-domains, ru-inside, bittorrent] —
порядок 1-к-1 (за вычетом 3 system rules resolve / sniff / dns hijack
которые builder вставляет в голову).
Spec: §062.
Tests: 614 → 620, +6 в test/services/builder/apply_all_custom_rules_test.dart
покрывают cross-kind order, mixed kinds, identical-skip + cross-kind, DNS aspect.
§064 — Custom rule editor View tab показывал пустой preview для disabled rules
Юзер открывал editor disabled-правила, переходил на View → видел
{rule_set: [], rules: []} потому что applyCustomRules фильтровал по
cr.enabled. Семантика «что родит в реальном конфиге» уместна для production
pipeline, но не для editor preview — юзер открыл editor именно для inspect'а
формы.
Fix: parameter skipDisabled на applyCustomRules (default true для
backward-compat; production pipeline applyAllCustomRules поведение не меняется).
ViewTab зовёт с skipDisabled: false — preview показывает «что родит при
включении» независимо от Switch.
Spec: §064.
✨ New
Info tooltip на Allow VPN bypass toggle
VPN Settings → System → Allow VPN bypass теперь имеет info_outline icon
рядом с заголовком. Tap → tooltip на 12 секунд объясняет:
- что делает —
ConnectivityManager.bindProcessToNetwork()bypass - когда полезно — банкинг, captive portal detection, системные сервисы
которые отказываются работать через VPN - что значит off — strict tunnel (весь трафик через VPN)
- когда применяется — на следующий VPN connect
Тот же паттерн что в DNS settings (Tooltip с triggerMode: tap).
🔧 Changed
Backup format переписан — full storage snapshot
Старый формат {vars, server_lists} на корне не сохранял большую часть
пользовательских данных — custom_rules, tun_apps, enabled_groups,
enabled_rules, route_final, rule_outbounds, dns_options живут как
top-level ключи lxbox_settings.json, а export'ил только data['vars'].
Inline rule_set'ы вида «Ru Apps» (57 пакетов через CustomRule.inline)
исчезали при restore silently.
Новый wire-format:
{
"app": "lxbox",
"kind": "snapshot",
"created_at": "...",
"source_app_version": "...",
"storage": { /* lxbox_settings.json целиком */ },
"vpn_settings": {
"auto_start": ..., "keep_on_exit": ..., "background_mode": ...,
"core_logs_enabled": ..., "allow_bypass": ...
}
}versionполе убрано — single-format. Файлы старого образца reject'ятся
с message «Unsupported backup format. Re-export from a recent app version.»storageблок = deep-clone всегоlxbox_settings.jsonчерез
SettingsStorage.exportRaw(). Restore — черезreplaceRaw(map, merge:):
приmerge=falseoverwrite целиком, приmerge=truetop-level merge
с recursive vars upsert.vpn_settingsблок — отдельный native-side state изboxvpn_boot
SharedPreferences (BootReceiver читает at boot-time когда Flutter ещё
не запущен; не перенесён в Flutter storage ради simplicity).- Категории UI — 5 (было 4): Server lists, Routing, App settings,
VPN system toggles (новая), Debug API. Filter работает на уровне
keys вstoragemap — будущие top-level настройки попадают в backup
автоматически без правок allowlist'ов. - Debug API
/backup/export|importсинхронизирован с UI — symmetric
round-trip.
Spec: §040 backup.
Tests: 13 cases в backup_service_test.dart
— round-trip, selective categories, merge vs replace, legacy reject,
deep-clone semantics.
🏗 Refactor
§053 — custom_rule_edit_screen.dart split
Stage 1 / 2 / 3 поэтапно вынесли editor's 2060 LOC monolith в композицию:
- Stage 1 (v14080) — 3 wifi widgets extracted:
widgets/wifi_entry.dart,
wifi_saved_picker_sheet.dart,wifi_manual_add_dialog.dart+
validators + normalizers с unit-тестами. - Stage 2 (v14090) — 7 секций + 2 shared widgets в
screens/custom_rule_edit/sections/иwidgets/. Sections — dumb
StatelessWidgetс props (controllers + callbacks);ItemsField—
единственныйStatefulWidget(подписан на controller через
addListenerдля self-rebuild). Editor: 1795 → 1330 LOC. - Stage 3 (v14100) — выделен
CustomRuleEditController extends ChangeNotifier(edit_controller.dart):
владеет всеми 8TextEditingController-ами, флагами, коллекциями,
async state + mutator'ами +snapshot()/isDirty()+ pure async
методами. Раздаётся вниз черезCustomRuleEditScope(plain
InheritedNotifier). Tabs выделены вtabs/params_tab.dart,
tabs/preset_params_tab.dart,tabs/view_tab.dart. Editor scaffold:
1330 → 456 LOC (−65%; от исходных 2060 — −77%). Save-icon обёрнут
вAnimatedBuilderчтобы dirty-rebuild не дёргал весь AppBar.
На screen State остались только UI-actions требующие BuildContext:
save/back/delete dialog'и, cloud-menu, picker-вызовы, snackbar'ы.
Save flow unchanged.
Spec: §053.
Tests: 620 pass; analyzer clean.
📚 Docs
§054 — Spec reorg: features vs tasks classification audit
docs/spec/features/ теперь содержит только живые продуктовые /
архитектурные концепции. Семь демотированных в docs/spec/tasks/:
| Был | Стал | Reason |
|---|---|---|
055 |
Historical architectural decision | |
056 |
Historical milestone | |
057 |
Superseded by §026 | |
058 |
Superseded by §026 | |
059 |
Superseded by §030 | |
060 |
One-shot migration (Done) | |
061 |
Refactor; live spec — §014 |
Освобождённые номера (001 / 002 / 004 / 005 / 013 / 039 / 041) не
переиспользуются — archive-ссылки сохраняются. Все cross-refs обновлены
в docs/**/*.md, CHANGELOG.md, app/lib/**/*.dart, app/test/**/*.dart;
grep на retired numbers — 0 hits.
Spec: §054.
ARCHITECTURE Feature Specs map + CHANGELOG order audit
Пост-реорг audit нашёл два расхождения:
docs/ARCHITECTURE.md→ Feature Specs всё ещё перечисляла 7
демотированных как live features → синхронизирована с
docs/spec/features/README.md(демотированные в отдельной секции).- CHANGELOG.md — блок
[1.2.0] — 2026-04-18стоял между
[1.4.0]и[1.3.1](chronologically wrong) → переставлен в
правильный newest-first порядок.
§047 — Public Intent API spec расширен
Добавлены outgoing events (broadcast intents от LxBox: VPN_STATE_CHANGED,
CONFIG_RELOAD, опционально RULE_FIRED) + 2 incoming actions
(SET_RULE_ENABLED, SWITCH_PRESET_GROUP) + symmetric input/output
pattern. Статус остаётся Draft — не имплементировано.
Spec: §047.
📦 Install
CI APK: LxBox-v1.8.0-arm64-v8a.apk (после tag'а).
adb install -r LxBox-v1.8.0-arm64-v8a.apk по...
LxBox v1.7.0
L×Box v1.7.0
«Observability» release. Главное — Per-app traffic profiler: pick app → ▶ Record → see every domain, IP, and routing decision в real-time. Бонусом — расширение ru-direct preset'а 4-слойной защитой (TLD + service-CDN suffixes + GeoIP-ranges) — закрывает категории багов вида «X не открывается через VPN» из v1.6.x.
Quick links:
✨ Highlights ·
🐞 Fixes ·
🏗 Under the hood ·
📦 Install ·
🇷🇺 На русском
✨ Highlights
Per-app traffic profiler
Inline-инструмент диагностики «куда конкретное приложение ходит и как роутится» — без packet capture, без root, без ручного matching'а conn-id'ов между логами.
Workflow:
Statistics → Per-app(или с HomeScreen tap по traffic bar'у когда recording active)- Pick app → выбрать приложение из picker'а
- ▶ START — recording пошёл
- Походить по приложению, дать трафик пройти
- ⏹ STOP — финализирует session (сохраняется в ring-buffer'е последних 5)
4 sub-tab'а:
| Tab | Что показывает |
|---|---|
| Live | Streaming events newest-first: DNS resolves с CNAME chain'ом, TCP/UDP open/close с outbound chain'ом. ⚠ icon на anomalies, ↗ chip рядом с IP — jump to Domains tab с автоподстановкой этого IP в search |
| Domains | Aggregated unique domains, sorted by bytes. Expanded view: CNAME targets, all resolved IPs, outbound, anomalies. Search-поле матчит по domain ‖ ip ‖ cname target (cross-domain CDN audit) |
| IPs | Aggregated by destination IP. Полезен для hostless connections (TCP без SNI sniffing) и suspect-IP debug'а |
| Connections | Per-connection timeline. Tap header → inline-expand: CNAME chain, all IPs, rule, anomalies, button [View in Domains →] фокусит aggregated row |
Connection-issue detection (2 locale-агностичных типа):
dnsTimeout— прямой engine-сигнал изdns: exchange failedлога sing-box'аtcpReset— heuristic «TCP closed <1s с 0 bytes» (firewall RST / unreachable)
Process inference fallback — когда sing-box find_process мисс'нул conn (rare, для WebView/system processes), profiler атрибутирует по prior DNS resolved IP в окне 10s; rows помечены 〽 inferred from prior DNS. Решает known-issue с Tinkoff WebView CDN-запросами из v1.6.1 диагностики.
Recording indicators:
- HomeScreen
_buildTrafficBarchip⚡ <pkg>рядом с traffic stats; tap всей строки →StatsScreen(initialTab: perApp) - В Stats у
Per-apptab title красная иконка⚡пока идёт запись
Overflow menu (⋮) в Per-app tab'е: Verbose core logs (debug-level, для глубокой DNS-диагностики; battery/CPU impact), Copy session JSON, Share, Clear all sessions, Help.
Debug API (Bearer-auth, port 9269): POST /profiler/start {package, verbose?} · POST /profiler/stop · GET /profiler/active · GET /profiler/sessions · GET /profiler/session/<id>?include=events,domains,ips · DELETE /profiler/session/<id> · DELETE /profiler/sessions · GET /profiler/stream (Server-Sent Events, fire-and-forget).
In-memory only — последние 5 finished sessions + 1 active. 3h sliding window + 50k events fallback cap. force-stop / app kill стирает всё (persist'а нет by design — diagnostic-only).
📚 User guide с use cases & curl recipes · §044 spec
Differentiator vs альтернативы
| Tool | Per-app traffic | Routing chain | DNS chain (CNAME) | Block in 1 click | Open-source |
|---|---|---|---|---|---|
| PCAPDroid | ✓ (hex) | ✗ | ✗ | ✗ | ✓ |
| NetGuard | ✓ (block/allow log) | ✗ | ✗ | ✓ block-only | ✓ |
| AdGuard Pro | iOS-only | ✗ | DNS-side | ✓ DNS-side | ✗ |
| L×Box §044 | ✓ structured | ✓ outbound chain | ✓ CNAME tracking | ✓ direct/block/preset | ✓ |
Sing-box-router внутри даёт нам уникальный data-source — outbound chain виден изнутри. Никто на mobile рынке этого не делает.
ru-direct preset — 4-слойная защита (§045)
В v1.6.x при tinkoff-incident'е (см. v1.6.1 release notes) выяснилось что ru.tinkoff.investing мог уходить через route.final = vpn-1 = 🇵🇱Польша в случаях когда ни TLD-domain (rule [4] ru-domains), ни package-name (rule [8] Ru Apps) не сматчились — sniff race и package detection race. Польский backend Tinkoff'а отвечал FIN → CDN-домены не доходили.
ru-direct preset теперь имеет четыре независимых rule_set с symmetric structure:
| Rule_set | Тип | Что ловит | Когда полезен |
|---|---|---|---|
ru-domains |
inline TLD-suffix | .ru / .su / IDN / .moscow / .tatar |
RU-домены — основной layer |
ru-services (новый) |
inline domain-suffix | 18 RU-сервисов на не-RU TLD: userapi.com (VK), avito.st, yandex.{net,com}, 2gis.com, okko.tv, premier.one, lenta.com, vk.com, gismeteo.com, mradx.net, wbstatic.net, trbcdn.net (Tinkoff+Sber CDN), sberbank.com, и ещё |
RU-родные сервисы с CDN на .com/.net/.tv |
Ru Apps |
package_name | ru.tinkoff.investing, ru.yandex.*, ru.kinopoisk, ~57 RU-app packages |
Ru-приложения у которых трафик может идти на любые TLD |
geoip-ru (новый remote .srs) |
IP-range | Russian-AS IP'шники (geoip от runetfreedom, ~150KB, обновление 168h) | Catch-all для CDN/QUIC/ECH/short-lived TCP когда первые три слоя промахнулись |
Гейт через preset var geoip_enabled (default true). Existing юзеры на v1.6.1 → автоматически получают новый layer на ребилде, без миграции storage.
📚 §045 spec с полным анализом race-conditions и failure modes.
Diagnostics tooling — scripts/lxbox-diag.sh + DIAGNOSTICS.md
One-command snapshot всего runtime-state'а на тестовом устройстве (Debug API + Clash API + adb-state) в /tmp/lxbox-debug-<datetime>/ за 2-3 секунды. ~23 файла: state.json, storage.json, config.json, core/app logs, clash connections+proxies+rules, ss tcp/udp output, ip route (включая policy-based), ip rule, addrs, props, logcat. Authored для post-mortem'ов и pre-destructive-op baseline'ов.
docs/DIAGNOSTICS.md (262 строки) — playbook: Debug API endpoints, Clash API endpoints, adb-команды, conn-id correlation, TCP-states meanings (LAST-ACK со unack send-q vs FIN-WAIT-1 vs SYN-SENT stuck), DNS error patterns, common diagnostic flows, что НЕ делать (destructive ops table).
🐞 Fixes
- Per-app log attribution edge cases (нашлись во время имплементации §044, 7 пунктов в implementation log):
- UID suffix strip в
metadata.process(ru.tinkoff.investing (10364)→ru.tinkoff.investing) — без этого ноль conn'ов атрибутировались к target session'у. - AppLog ring-buffer overflow → timestamp-based diff. Раньше length-diff работал пока буфер не заполнен (cap=500), потом freeze'ился. Now monotonic.
dns: cachedregex добавлен (помимоdns: exchanged) — у занятых apps cache-hit рулит, без этого DNS-уровневая видимость была разорванная.- DNS event attribution на оригинальный domain (не на финальный CNAME-target). Domains tab теперь показывает
cdn.t-bank-app.ruс раскрытиемCNAME → cl-ead2c819.edgecdn.ru → A 193.17.93.194, а неcl-ead2c819.edgecdn.ruкак top-level domain.
- UID suffix strip в
🏗 Under the hood
AppInfoCacheунификация icon-cache между Custom Rules и Per-app picker'ами.loadAllApps()(lightweight installed-apps list, без иконок) + smartensure(pkg)(догружает только icon если в cache был AppInfo без него). Раньше иконки грузились дважды на повторных открытиях picker'ов.SseResponseвdebug/transport/response.dart— primitive для Server-Sent Events через Debug API.expandPreset: поддерживаетenabled: "@var"гейтинг для rule_set entries иList<String>форму дляrouting_rule.rule_setс downgrade до single-string при одном expanded tag'е и drop+warning при empty filtered list.
📦 Install
Большинству юзеров нужен LxBox-v1.7.0-arm64-v8a.apk (~32 MB) — подходит 95%+ современных Android-устройств.
| Variant | Size | Когда выбирать |
|---|---|---|
LxBox-v1.7.0-arm64-v8a.apk |
~32 MB | Default — современные Android'ы |
LxBox-v1.7.0-armeabi-v7a.apk |
~30 MB | Старые / Android Go (Itel A48, Tecno Pop 5) |
LxBox-v1.7.0-x86_64.apk |
~34 MB | Эмуляторы / Chromebook'и |
LxBox-v1.7.0-universal.apk |
~93 MB | Fat fallback — если не уверены в архитектуре |
APK подписан upload-keystore'ом; устанавливается поверх v1.6.1 без переустановки. Никаких storage migration'ов — всё backward-compatible.
🧪 Tests
flutter test — 510 tests passed, flutter analyze чистый. Новое покрытие:
app/test/services/traffic_profiler_test.dart— session lifecycle, log-stream parsing (UID strip, CNAME chain, dns:cached + dns:exchanged), aggregation, process inference (10s window), connection-issue detection.app/test/services/builder/preset_expand_test.dart— расширен под §045: 4 case'а дляgeoip_enabled×geoip-rucache state, List form дляrule_set, dangling-rule_set guard.
🇷🇺 L×Box v1.7.0 на русском
«Observability» — релиз про видимость происходящего в сети.
Что увидит юзер
- Per-app traffic profiler — третий tab в Statistics. Выбираешь приложение, тапаешь ▶ Record, ходишь по нему — видишь все домены, IP-адреса, через какой outbound идёт трафик, какой rule сматчился. ⚠ icon отмечает connection issues (DNS timeout / RST early).
- Live — стрим events в реальном времени newest-first. DNS resolves с CNAME цепочками, TCP/UDP open/close.
- Domains — агрегаты по доменам, отсортированы по объёму трафика. Раскрываются с показом CNAME chain, всех IP, через какой outbound уходило.
- IPs — то же но с другой стороны (по destination IP). Полезно для hostless connections (TCP без SNI).
- Connections — timeline всех соединений с inline-expand.
- **Recording indicator ...
LxBox v1.6.1
L×Box v1.6.1
«DNS servers clean schema» polish-релиз поверх v1.6.0. Storage-схема dns_options.servers переведена на kind-discriminated refs (симметрия с DNS rules §041), что фиксит четыре live-бага выявленных на v1.6.0 в эксплуатации. Бонусом — кликабельные GitHub-ссылки в About и первый релиз публикующий per-ABI APK (closes #4).
Quick links:
✨ Highlights ·
🐞 Fixes ·
🏗 Under the hood ·
📦 Install ·
🇷🇺 На русском
✨ Highlights
DNS servers — clean schema (§043 + §044)
dns_options.servers[i] теперь хранится как kind-discriminated ref — точно по образцу dns_options.rules[] (§041):
{
"kind": "template" | "preset" | "inline",
"enabled": <bool>,
"tag": "<string>", // single source of truth, не дублируется в body
"description": "<string>"?, // optional override для template/preset, primary для inline
"body": { … }? // только inline; partial sing-box body БЕЗ tag/description/enabled
}Что это даёт:
- Override =
kind: inlineс tag совпадающим с canonical. Тривиальная classification по kind — без shape-comparison черезjsonEncode(раньше order-sensitive фрагильно). - Body для template/preset берётся из canonical at render/build time → tag rename'ы из §039 (
direct_dns_resolver→google_udp) теперь подхватываются automatically. - Auto-discovery + orphan cleanup — для каждого template/active-preset server'а tag которого нет в storage append'ится ref; ref'ы с несуществующими tag'ами удаляются. Симметрично
resolveDnsRulesList. - Edit dialog: 3 явных input'а —
Tag/Description/Enabledсверху, body JSON внизу (только sing-box-relevant поля). Раньше юзер видел «магические» meta-поля среди sing-box-полей. - Короткие односложные badge'ы — Template / Preset / User / Overridden вместо длинных «User (overrides template)» / «Preset · Russian domains direct» (которые ломали title-wrap, см. live-баг ниже).
- Builder синтезирует tag в body на build-time; в storage tag живёт только на ref-level. Запротоколированная магия в
resolveDnsServersBodies— нет двух источников правды.
Lossless one-shot migration для existing v1.6.0 юзеров. Auto-detect по presence/absence of kind field на entries:
| Состояние storage | Migration |
|---|---|
| Pre-§043 (legacy full-body snapshot) | classify по canonical match → kind-ref + peeled description на ref-level + partial body |
| §043 inline (tag/description в body, intermediate) | peel body.description → ref.description; drop body.tag / body.enabled / UI-annotations |
| §044 (already-migrated) | no-op |
Debug API PUT /settings/dns_options/servers принимает любой из трёх форматов; legacy конвертируются на ближайший resolver tick.
About screen — GitHub-tile clickable
Source Code, VPN core, и singbox-launcher (Credits) теперь по тапу открывают соответствующий GitHub-репозиторий в браузере, а не копируют URL в clipboard. Trailing ↗ иконка для visual cue.
| Tile | URL |
|---|---|
| Source Code | https://github.com/Leadaxe/LxBox |
| VPN core | https://github.com/SagerNet/sing-box |
| singbox-launcher (Credits) | https://github.com/Leadaxe/singbox-launcher |
Per-ABI + universal APKs (closes #4)
Релиз публикует 4 APK вместо одного fat-APK:
| Variant | Size | Когда выбирать |
|---|---|---|
LxBox-v1.6.1-arm64-v8a.apk |
~32 MB | Default — 95%+ современных Android-устройств |
LxBox-v1.6.1-armeabi-v7a.apk |
~30 MB | Старые устройства / Android Go (бюджетные Itel, Tecno Pop, etc) |
LxBox-v1.6.1-x86_64.apk |
~32 MB | Эмуляторы / Chromebook'и с Android-runtime'ом |
LxBox-v1.6.1-universal.apk |
~95 MB | Fat fallback — содержит native libs для всех 3 ABI |
CI-инфраструктура запилена в v1.6.0-cycle; v1.6.1 — первый релиз, который реально публикует все 4. Уменьшает скачиваемый размер для большинства юзеров на ~3×.
🐞 Fixes
- JSON viewer protocol leak —
_showServerBodyDialogпоказывал underscore-поля (_kind/_overrides/_preset_label/_origin) у одних tile'ов, не у других. ТеперьResolvedServer.bodyфизически не содержит underscore-полей — компилятор гарантирует. - DNS Settings — Yandex/long-name preset tile разрывался на 4 строки (live-баг). Длинный badge
Preset · Russian domains directломал title-wrap. Теперь badge — короткийPreset, имя preset'а живёт в subtitle. - Toggle enabled на template-сервере помечал его как Overridden (live-баг). Storage хранил copy-on-write template-shape с изменённым
enabled, override-detection через shape compare ошибочно классифицировала это как override. С refs-by-kind: toggle меняет толькоenabledref'а, kind остаётсяtemplate. - После tag rename из §039 (
direct_dns_resolver→google_udp) existing-юзеры не видели нового tag'а в DNS Final dropdown'е (storage XOR template). Auto-discovery теперь подтягивает новый tag, orphan cleanup убирает старый.
🏗 Under the hood
applyCustomDnsиспользуетresolveDnsServersList(refs) +resolveDnsServersBodies(refs → final bodies для sing-box config). Старая XOR-логика «userServers OR templateServers» удалена.ResolvedServertyped-class вdns_settings_screen.dartзаменил underscore-аннотированныеMap<String, dynamic>. Render layer стал явно typed, accessors через named fields.- Render order в UI —
template→preset→inline(сорт поServerKind.index, stable внутри группы). docs/STORAGE.md(новый) — единый источник правды по схемеlxbox_settings.json: top-level shape, per-key семантика, migration history (proxy_sources → server_lists, app_rules → custom_rules, dns_options.rules_json → rules[], pre-§043 → §043 → §044), SharedPreferences boot flags, Debug API exposure allow-list.ARCHITECTURE.md§5 свёрнут до tldr со ссылкой.
📦 Install
Большинству юзеров нужен LxBox-v1.6.1-arm64-v8a.apk — он подходит для 95%+ современных Android-устройств. Размер ~32 MB.
Если не уверены в архитектуре своего устройства — берите LxBox-v1.6.1-universal.apk (fat, ~95 MB, содержит libs для всех ABI). Для очень старых / бюджетных устройств — armeabi-v7a. Для эмуляторов / Chromebook'ов — x86_64.
APK подписан upload-keystore'ом; устанавливается поверх v1.6.0 без переустановки. Migration storage'а происходит автоматически на первом запуске.
🧪 Tests
flutter test — все тесты проходят. Новое покрытие:
test/services/builder/dns_servers_resolver_test.dart—resolveDnsServersList(orphan cleanup, auto-discovery, three-way migration);resolveDnsServersBodies(refs → bodies, enabled filter, tag synthesis).
🇷🇺 L×Box v1.6.1 на русском
«DNS servers clean schema» — polish-релиз поверх v1.6.0. Хранилище dns_options.servers переведено на kind-discriminated refs (симметрия с DNS rules из v1.6.0); фиксит четыре live-бага выявленных в эксплуатации. Бонус — кликабельные GitHub-ссылки в About и первый релиз с per-ABI APK.
Что увидит юзер
- DNS Settings — короткие односложные badge'и:
Template/Preset/User/Overriddenвместо длинных «User (overrides template)» которые ломали layout (Yandex UDP-tile разрывался на 4 строки). - Edit dialog у DNS-сервера — три явных input'а сверху (
Tag/Description/Enabled), body JSON внизу содержит только sing-box-relevant поля. Раньше meta-поля были смешаны с sing-box-полями. - About → 3 GitHub-ссылки кликабельны (Source Code → LxBox, VPN core → sing-box, singbox-launcher в Credits → upstream). Раньше копировали URL в буфер обмена.
- После update'а template'а (rename DNS-tag'а или change body) — изменения автоматически подхватываются в существующих юзер-конфигурациях. Раньше storage хранил snapshot и stale fields ломали поведение.
- Toggle enabled на template-сервере не меняет badge на
Overridden(live-баг v1.6.0).
Под капотом
- Storage: kind-refs + clean schema —
dns_options.servers[i]хранит{kind, enabled, tag, description?, body?}. Tag — single source of truth на ref-level. Builder синтезируетbody.tagна build-time. Override-detection — тривиальная classification поkind == 'inline'(без shape comparison). - One-shot migration для existing v1.6.0 юзеров: pre-§043 / §043 / §044 — все три формата детектятся автоматически и конвертятся к §044 lossless'но.
docs/STORAGE.md(новый) — полная схемаlxbox_settings.json, migration history всего legacy (proxy_sources, app_rules, dns_options.rules_json), SharedPreferences boot flags. Единый источник правды для shape'а файлов на диске.
📦 Установка
Большинству нужен LxBox-v1.6.1-arm64-v8a.apk (~32 MB) — подходит 95%+ современных Android'ов. Не уверены в архитектуре — берите universal (~95 MB).
Поверх v1.6.0 встаёт без переустановки; миграция storage происходит на первом запуске.
Предыдущий релиз: v1.6.0.
LxBox v1.6.0
L×Box v1.6.0
«Diagnostics + recovery + DNS-cleanup» release. libbox 1.13.x migration with native VPN service rewrite under the hood; user-visible — light-recovery (Reload button + reset-network), per-group ping/test settings, human-readable error banners, fixed RU DNS routing via ru-direct preset, backup/restore UI, and more.
Quick links:
✨ Highlights ·
🐞 Fixes ·
🏗 Under the hood ·
⚠ Breaking ·
🇷🇺 На русском
✨ Highlights
Light-recovery without losing the tunnel
- Reload button in the AppBar. Default tap = light reload of the sing-box core (
commandServer.startOrReloadService) — no TUN teardown, no full reconnect. In-place restart with the same config in <1s. Long-press menu also lists Reload as the first item. POST /action/reset-networkDebug API. Calls sing-boxcommandServer.resetNetwork(): closes all active connections + flushes DNS cache + resets DoH/DoT transports + refreshes inbound/outbound dialers. No box runtime / Service / TUN recreate. Use it when DoH connection pool went stale after long idle (≈ what Reload does, but addressable from adb).
Per-group ping/test settings
Each VPN group can now have its own test endpoint (URL + timeout) for ping / mass-URLTest / group URLTest. Useful when groups have different routing semantics:
| Group | Routing | Recommended endpoint |
|---|---|---|
| VPN-1 (foreign-routed) | through bypass-VPN | https://www.gstatic.com/generate_204 |
| VPN-2 (РФ-direct) | direct path / WG router in Russia | https://ya.ru/ |
| VPN-3 (China-direct, hypothetical) | direct path in CN | https://baidu.com/ |
Earlier the single global URL would false-negative for half the groups. Now: storage shape ping_options: {url?, timeout_ms?, groups: {<groupTag>: {url?, timeout_ms?}}} mirrors the template; resolve chain is per-group → global → template default.
UI: in Ping Settings dialog there's now a SegmentedButton «All groups | », plus a Reset-to-global button when an override exists.
Debug API: GET/PUT /settings/ping_options (full structure), GET/PUT/DELETE /settings/ping_options/groups/{tag} (scoped).
Bonus fix: the previous global pingUrl/pingTimeout lived only in HomeController memory — not persisted. On every app restart the UI showed a misleading template default. Now persists to SettingsStorage.
Backup & restore UI
New screen — export/import user data (server lists / routing rules / app settings / debug config) as a single JSON. 4 toggleable categories, dry-run preview before apply, merge vs replace mode. Export through share_plus, import through file_picker. Same surface available via Debug API: GET /backup/export?include=..., POST /backup/import?merge=....
Human-readable error banners
Replaced raw Dart exception toString'ы in user-visible places with a unified formatUserError(e) helper. No more leaked technical jargon (Future not completed, errno = N, long address = ... tuples, PlatformException(code, msg, null, null)):
| Error | Before | After |
|---|---|---|
| Ping timed out | Ping: TimeoutException after 0:00:10.000000: Future not completed |
direct-out → ya.ru — timeout 5.8s |
| Native VPN start failed | PlatformException(start_failed, vpn_service.prepare returned false, null, null) |
vpn_service.prepare returned false |
| Clash API unreachable | Clash API: SocketException: Connection refused (OS Error: Connection refused, errno = 61), address = ... |
Clash API: Connection refused |
| File pick — missing file | Failed to read file: FileSystemException: Cannot open file, path = '/p' (OS Error: No such file or directory, errno = 2) |
Failed to read file: No such file or directory |
| Invalid JSON paste | Invalid JSON: FormatException: Unexpected character (at character 5)\n{...}\n^ |
Invalid JSON: Unexpected character |
Applied in home_controller.dart (file pick, start/stop/reconnect VPN, Clash API refresh, switch node) and snackbar'ах 6 экранов (config / backup / dns_settings / node_settings / debug / speed_test).
ru-direct preset works in Russia again
Default DNS for the ru-direct preset switched from DoH/Safe-tier (yandex_doh @ 77.88.8.88, HTTPS/443) to UDP/Base-tier (yandex_udp @ 77.88.8.8, UDP/53). Reason — for users where outbound = direct-out or a WG router in RU, the Yandex DoH endpoint on :443 was DPI-blocked: TLS handshake to safe.dot.dns.yandex.net hangs, while ICMP/UDP to the same IP works fine. All .ru lookups via ru-direct failed → ERR_CONNECTION_REFUSED in the browser and Tinkoff/etc mobile apps. UDP/53 on 77.88.8.8 is universally permitted.
Tooltip on dns_server shortened: «Recommended: Base/UDP — most stable. DoH/DoT may be filtered by ISPs.». Options reordered — Base/UDP first.
Existing installs unaffected: explicit vars_values in SettingsStorage take precedence over the template default; new default fires only for users who never picked a value (or reset).
Empty template DNS catch-all
Removed the template-level catch-all {name: "Default → Google DoH", server: google_doh} from wizard_template.json. Anything not matched by preset/inline DNS rules now flows through dns.final (= variable @dns_final, default local_dns_resolver = system resolver via PlatformInterface; user can override in the wizard).
Why: google_doh (HTTPS/443) was degrading on long idle — DoH connection pool went stale → re-dial failed → entire fall-through DNS died (observed twice this week, recovery via resetNetwork()/Reload). System resolver is state-less and not subject to this.
Existing users with a Default → Google DoH storage entry — orphan cleanup in resolveDnsRulesList removes it on the next config rebuild. Tooltip on dns_final updated. Also, the direct_dns_resolver server tag was renamed to google_udp for symmetry with cloudflare_udp naming.
Sing-box internal logs in Debug API + AppLog
GET /logs/core exposes router/dns/inbound/outbound events from sing-box itself. Source delivery: PlatformInterface.writeDebugMessage → EventChannel("lxbox/coreLog") → new ClashLogPump Dart subscriber → AppLog as DebugSource.core. Level parsed via regex (\bWARN\b/\bERROR\b etc.); TRACE/DEBUG dropped on the native side for volume reduction; ANSI escape codes stripped.
Toggle: PUT /settings/core_logs_enabled {"enabled":true} (default false; storage in SharedPreferences boxvpn_boot.core_logs_enabled because Libbox.setup reads it before Flutter engine; takes effect after force-stop & restart of the app).
UI toggle: App Settings → Diagnostics → "Forward sing-box logs". Shortcut from DebugScreen ⋮ menu → "Diagnostics settings".
AppLog per-source quotas + persistent split
The single 500-entry ring-buffer was outgrown by sing-box's verbosity (hundreds of lines/min on busy traffic) — app-level messages would get evicted within minutes. Now: Map<DebugSource, List> with independent caps app=300, core=500. K-way merge on read (insert is O(1) amortized), entriesForSource(s) direct lookup without merge for filtered queries. Persistent split: applog.txt (app warn/error) + corelog.txt (core warn/error), 200 lines / 64KB each — initPersistent() loads both.
Debug API: GET /logs/app, GET /logs/core (aliases for /logs?source=...); POST /logs/clear?source=app|core for per-source clear.
Debug API: PUT /config + lockable rebuild
Two new endpoints for testing sing-box features that our parser/builder doesn't yet understand (e.g., Tailscale outbound):
PUT /configwith raw sing-box JSON — sing-box reloads with that config.PUT /settings/config_locked {"locked": true}— pins the config from UI rebuilds (SubscriptionController.generateConfig()returns null silently while locked).GET /state/config_locked— current state.
Storage: config_locked_for_debug, default false.
Misc UI
- Core version visible in About dialog (
commandServer.coreVersion()next to the app version) — quick glance to see which libbox is shipped.
🐞 Fixes
- Clash delay endpoint hang after ~27 min uptime (root-cause §039 libbox migration). Symptom: every node in the server list showed "err" in the UI after 28-30 minutes of active VPN session, even though traffic through the selected node kept flowing. Hours-long sessions silently broken for anyone trying to compare nodes or do a manual switch. Root cause — DNS cache dedup-lock goroutine leak in sing-box
dns/client.go:144-164: the per-question wait channel blocked withoutctx.Done()-awareness; the first time an upstream DNS-transport froze, all subsequent waiters parked forever, including the probe-mechanism goroutines. Fix — upstream commitaba8346b("Fix DNS cache lock goroutine leak"), shipped in sing-boxv1.12.21+andv1.13.0+. - Mass ping cancel actually cancels (§034). Tapping Stop during a mass-ping previously left three side-effects:
- Spinner indicators kept hanging until timeout for nodes that hadn't responded yet (worker
breakwithout cleaning up pingBusy state). _runAllUrltestGroupskept iterating the auto-group regardless of cancel.- In-flight HTTP delay/groupDelay requests kept executing (Dart
http.Clienthas no per-request cancel).
Fix:cancelMassPingnow (1) clearspingBusyentirely; (2)_runAllUrltestGroups(epoch)checks epoch on every iteration; (3)ClashApiClienthas a separate_delayHttpclient for delay/groupDelay —cancelDelays()closes it, in-flight HTTP sockets drop, workers get an exception and break.
- Spinner indicators kept hanging until timeout for nodes that hadn't responded yet (worker
- Clash delay/groupDelay timeout sync (§040). The Dart-side wrapper used a hardcoded
_timeout = 10sregardless of thetimeoutMsquery-param sent to cl...
LxBox v1.5.0
L×Box v1.5.0
Reliability + UX + introspection iteration. Critical fix for Android 9-11 startup. Two new user-facing protocols / shortcuts. Full crash diagnostics.
Quick links:
✨ Highlights ·
🐞 Fixes ·
⚠ Breaking ·
🇷🇺 На русском
✨ Highlights
Protocols & connectivity
- NaïveProxy (§037, #2) — parser for
naive+https://URIs (DuckSoft format), generator for sing-boxtype: "naive"outbound, share-URI round-trip. 10th typed protocol in Parser v2. Cronet (with_naive_outbound) is already bundled in ourlibbox.aar, no APK-size impact. - Quick Connect: QS tile + home-screen shortcut (§032, #1) — toggle VPN without opening the app. Tile in the notification shade syncs with
BoxVpnService.currentStatus; long-press on the launcher icon → Toggle VPN. First tap briefly opens the app for the system VPN consent dialog (Android API requirement); subsequent taps go straight to the service.
Diagnostics
- Crash diagnostics (§038) — four independent post-mortem channels available through one
Share dumpbutton (⤴ in Debug AppBar) orGET /diag/dump:- A. stderr-redirect — Go panic stacktrace from libbox/sing-box; written to
filesDir/stderr.logbefore SIGABRT, survives the process. New conditionalstderrtab in Debug screen. - B. ApplicationExitInfo (Android 11+) —
getHistoricalProcessExitReasonslazy-read in DumpBuilder. Reason + tombstone for NATIVE_CRASH or Java stacktrace for CRASH. - C. Persistent AppLog —
warning+errorlevels written tofilesDir/applog.txt(ring-buffer, 200 entries / 64 KB cap). Loaded onmain()withfromPreviousSession=true, visually marked in UI; survives process restart. - D. Logcat tail —
Runtime.exec("logcat", "-d", "-t", 1000, "*:E")viaProcessBuilder(noREAD_LOGSpermission needed; logd UID-filters automatically). CatchesAndroidRuntime FATAL EXCEPTION,libc/DEBUG/tombstoned,art/linker. Especially useful when AEI didn't attach trace (Samsung One UI quirk on REASON_CRASH).
- A. stderr-redirect — Go panic stacktrace from libbox/sing-box; written to
- Debug API
/diag/*group (§031):/diag/dump,/diag/exit-info,/diag/logcat,/diag/stderr,/diag/applog. Everything available via UI is also accessible over HTTP for adb-driven flows. - Debug API
/backup/*group —GET /backup/export+POST /backup/importдля бэкапа/восстановления{config, vars, server_lists}. Без диагностического шума и без кешей; совместим с форматом/diag/dump. Опции?merge=и?rebuild=для гибкости restore. POST /action/preview-empty-state?on=true|false— UI-only override empty-state без потери данных, для скриншотов/regression-теста UX.
Home screen polish
- First-run empty-state guide (task 024) — на первом запуске (нет конфига) главный экран показывает «Add a server» с крупной круглой
+-кнопкой →SubscriptionsScreen. Никаких disabled-кнопок и догадок куда нажимать. - Tap-to-connect zone — когда серверы есть но VPN не запущен, центр экрана показывает крупную кликабельную зону «Tap to connect» (play-icon 64dp). Тап стартует VPN — равноценно нажатию Start в верхней панели.
UX & reliability (carryover from earlier dev cycles)
- Tunnel sleep mode (3-way) — Settings → Background → «Tunnel sleep mode»:
never(default; pushes/SIP stay alive at the cost of ~1–3% battery overnight),lazy(pause on deep Doze only — old default),always(pause on every screen-off, max battery). - Tabbed App Settings — three tabs: General (appearance/behaviour/subscriptions/updates), Background (battery/notifications/OEM/sleep mode), Diagnostics (permissions summary, Debug API).
- Update check on launch (§036) — daily ping to GitHub Releases; SnackBar on a newer tag with View / Not now. Manual
Check nowfrom About / Settings bypasses the cap. - Battery-optimization prompt at startup if not whitelisted (rate-limited to once per 24h).
- Notifications status indicator in Settings → Background (matters for
POST_NOTIFICATIONSon Android 13+).
🐞 Fixes
CHANGE_NETWORK_STATEpermission for Android 9-11 (task 023) —DefaultNetworkListeneron API 28-30 callsConnectivityManager.requestNetwork(...), which requiresCHANGE_NETWORK_STATE. Without it:SecurityException→REASON_CRASHimmediately after VPN consent OK on A50/A10/Y9. On API 31+ a different code path is used (registerBestMatchingNetworkCallback) which is why the regression only appeared on Android 9-11 while Android 12+ kept working. Permission isnormal-level, no runtime prompt, silent migration.- VLESS
packetEncodingallow-list (task 012) — xray-style subscriptions encodepacketEncoding=nonein their URIs, which produced"packet_encoding": "none"in outbound JSON; sing-boxvless.NewOutboundonly acceptsxudp/packetaddr/omitted and calledE.New("unknown packet encoding: …"), which crashed libbox via an upstreamformat.ToStringbug. Parser now normalises on input:xudp/XUDP→xudp,PacketAddr→packetaddr,nonesilently dropped, anything else → warning + drop. - Race:
Libbox.newServicebeforeLibbox.setupfinishes (task 027) —BoxApplication.libboxReady: CompletableDeferred<Unit>barrier;BoxVpnServiceserviceScope.launchwaits for it before any libbox call. Bonus: libboxworkingDirmoved from external (getExternalFilesDir(null)) to internal (context.filesDir) — same place whereSettingsStorageand subscriptions already live; eliminates Knox/SELinux edge cases on Samsung One UI 3.x and EMUI. - Quick Connect class-verification on Android 9-11 (task 015) —
Tile.subtitle(API 29+) extracted into a@RequiresApi(Q)helper;LxBoxTileService.refreshTileandQuickShortcuts.refreshgated on API 30+ with outertry { Throwable }; all callsites insetStatus/onDestroy/initializewrapped inrunCatching.FOREGROUND_SERVICE_SPECIAL_USEpermission gated tominSdkVersion="34"; typedstartForegroundon API 34+.
Reliability internals
Libbox.newService/svc.start/serviceScope.launchcatchThrowable(task 016) — not justException;Errorsubclasses (OOM, NoClassDefFoundError, VerifyError) now surface throughstopAndAlert(...)instead of vanishing the process./files/localDebug API alias for/files/external(legacy). Internal app-scoped storage.
⚠ Breaking
- Tunnel sleep mode default flipped:
lazy→never. Old default paused the tunnel on deep Doze, which broke long-lived TCP sockets and push notifications. New default keeps the tunnel always active (+1–3% battery overnight). Want the old behaviour — Settings → Background → Tunnel sleep mode → Lazy sleep.
📦 Install
APK is signed with the upload keystore; install over previous L×Box versions in place.
🇷🇺 L×Box v1.5.0 на русском
Релиз с критическим фиксом запуска на Android 9-11, новым 10-м протоколом (NaïveProxy), Quick Connect (плитка в шторке + ярлык на иконке), и встроенной диагностикой крашей через 4 канала + HTTP API.
Основные фиксы
- Android 9-11 / VPN не запускался (Samsung A50/A10, Huawei Y9 2018) — в манифесте не хватало
CHANGE_NETWORK_STATE, который требуетConnectivityManager.requestNetwork(...)на API 28-30. На Android 12+ используется другой код-путь, поэтому регрессия проявлялась только на 9-11. Permissionnormal-уровня, без runtime-промпта, миграция silent. - Crash при VLESS-подписке с
packetEncoding=none— парсер теперь нормализует на входе по allow-list:noneтихо дропается, неизвестные значения → warning + дроп. - Race в init libbox — добавлен барьер
libboxReady: CompletableDeferred<Unit>, VPN не стартует до готовности sing-box. Заодно libbox перенесён из external в internal storage — там же где подписки и настройки. - Quick Connect class-verification на Android 9-11 —
Tile.subtitleв@RequiresApi-helper, всё gated на API 30+; FGS_SPECIAL_USE permission гейтнутminSdkVersion="34".
Новые фичи
- NaïveProxy (§037) — Cronet TLS fingerprint,
naive+https://-URIs парсятся в подписках в типизированный outbound. Полезно когда DPI ловит uTLS-имитации. - Quick Connect (§032) — плитка в шторке + ярлык на лаунчер-иконке. Toggle VPN без открытия app'а.
- Crash diagnostics (§038) — четыре канала (stderr-redirect / ApplicationExitInfo / persistent AppLog / logcat tail) собираются ...
LxBox v1.4.2
L×Box v1.4.2
Patch-release поверх 1.4.1 — новая иконка приложения.
🎨 Design
- Новая иконка приложения — W1 "routing cross" вместо generic Flutter-иконки. Концепт — пересечение двух путей под прямым углом, формирующее стилизованную «L»; метафора маршрутизации по правилам (direct / via proxy). Читаемо в 48×48 launcher size.
- Все платформы обновлены: Android (adaptive foreground/background + themed mono для Android 13+), iOS, macOS, web favicon, Windows.
- Источники SVG хранятся в
docs/design/icon/W1_pack/для будущих ревизий. - Детали: spec 034.
🧹 Cleanup
- Удалён
docs/design/icon-exploration/— прочие отклонённые концепты (W2 Lx-monogram, W3 iso-cube + 10 черновых SVG). История в git, winner перемещён вdocs/design/icon/W1_pack/.
Всё остальное содержимое — см. RELEASE_NOTES для v1.4.1.
LxBox v1.4.1
L×Box v1.4.1
Iterative release on top of 1.4.0. Two major internal changes + reworked templated routing rules.
✨ Highlights
- Preset bundles (spec 033) —
CustomRule(kind: preset)is now a thin reference to a template preset; content is expanded fromwizard_template.jsonon every build. Template updates automatically propagate to every user without data migrations. Example:Russian domains directis parametric — pick Outbound + DNS Transport (DoH/DoT/UDP) + UDP server IP (Safe/Base/Family). CustomRulesealed split (spec 030 §v1.4.1, task 011) — the single class is split intoCustomRuleInline/CustomRuleSrs/CustomRulePreset. Exhaustive pattern-match in builder/UI, compile-time checks instead of runtime ifs. Legacy JSON reads without migrations.- SRS cache for preset bundles — bundles with a remote
rule_set(Block Ads, Russia-only services) now go through the localRuleSetDownloadercache instead of sing-box auto-download. Spec 011 invariant restored. (UI ☁ buttons are in 1.4.2.) - Rename
target → outbound,out → outbound— everywhere, matching the sing-box JSON schema and the UI label. - Universal outbound override for presets —
OutboundPickerв tile работает для любого bundle'а, даже если его template использует shorthandrule.action: "reject"(Block Ads) или hardcodedrule.outbound: "<tag>"(Russia-only services). Override пишется вvarsValues['outbound']и применяется вpreset_expandнезависимо от формы template'а. Shorthandaction: "reject"теперь означает "default", не "lock": юзер может сменить Block Ads с reject на vpn-1 и обратно. См. spec 033 §Expansion §5. - Initial insertion sort для Custom Rules — при добавлении reject-правила оно ложится в самый верх списка, direct — сразу за начальным блоком reject'ов. Proxy-правила (vpn-1, auto, прочие) — в хвост. First-match-wins sing-box-семантика соблюдается из коробки. Drag-handle остаётся свободным: юзер может переставить как хочет, автосортировка — только на вставке. См. spec 030 §Initial insertion sort.
- Debug API refresh —
/state/rulesserializer переписан на sealed-dispatch: inline/srs/preset правила отдают только релевантные поля. Preset-правила теперь экспозятpreset_id,vars_values, nestedpreset.remote_rule_sets[]с per-rule_set SRS-cache статусом (cached,path,mtime), флагready. Debug API честно отражает sealed-иерархию task 011. - SRS cache prune —
RuleSetDownloader.pruneOrphans()при каждом_refreshSrsCacheсметает.srs-файлы без живого референса. Orphan'ы после миграций схемы (pre-bundle → bundle) больше не копятся. .онлайн(xn--80asehdb) в Russian domains direct — добавлен в inline rule_setru-domainsи в DNS-правило черезyandex_doh.
🛡 Reliability & Safety
- Retry + exponential backoff для subscription fetch и rule_set download — 3 попытки (1s → 3s).
4xx— permanent (без ретраев),5xx/ timeout /SocketException— retry. Снимает львиную долю жалоб "подписка не обновляется" у юзеров с нестабильной сетью. - Top-level error boundary (
FlutterError.onError) — uncaught Flutter-ошибки попадают вAppLogи видны на Debug → Logs. Красный экран заменён на компактныйErrorBoundaryfallback-widget. - Auto-updater coverage — §027 spam-gate покрыт тестами (
consecutiveFails,minRetryInterval,maxFailsPerSession,inProgresscrash-safe reset на старте). Никаких скрытых background-refresh не добавлено.
🔐 Security
- URL masking audit — subscription URL больше не течёт в
AppLogцеликом. Везде используетсяmaskSubscriptionUrl(scheme://host/***). Полный URL доступен только в Debug API сreveal=trueпараметром. Закрыты 4 leak-сайта (hydrate-fail, inProgress-skip, shortUrl truncation,addFromInput).
🎨 UX
- Human-readable errors (
humanizeError) — все user-visible сообщения приведены к человеческому виду. Было:Exception: HTTP 503 for https://…. Стало:Server error (503) — provider is down, try later. Timeout сообщает длительность. Покрыто топ-5 сценариев: subscription fetch, rule-set download, parse, config build, VPN start. - Parse hints — когда подписка загружена, но распарсилась в 0 нод, показываем причину: HTML-страница провайдера, Clash YAML, full sing-box config, plain-text error. Юзер понимает "что именно не так", а не "unknown parse error".
- Pull-to-refresh на Subscriptions screen — стандартный Android-gesture запускает
updateAll. - Getting Started card — при пустом списке подписок показывается пояснительная карточка: как добавить URL / paste clipboard / file pick.
- Unsaved-input guard — на Add Subscription, введённый в поле текст без сохранения → back → диалог "Discard input?".
- Relative time —
2h ago / yesterday / 3d ago / 2w ago / 2mo ago / 2y agoвместо абсолютных timestamp'ов в узких местах. - Debug log search —
/logsподдерживаетq=substring search иlevel=multi-filter (level=error,warn). - Reset fail-count & retry (long-press на подписке) — ручной reset
consecutiveFailscounter'а, размораживает подписку из session-cap и сразу пытается обновить. - Share URL (masked / full) — long-press → "Share URL…" → диалог с выбором: замаскированный или полный URL (для приватной передачи).
🧪 Testing
- +97 tests (262 → 359). Новые модули покрыты полностью:
error_humanize,url_mask,parse_hints,relative_time,input_helpers,http_cache,rule_set_downloader,auto_updater,body_decoder, validator edge cases, preset-expand.
🧹 Cleanup
flutter analyze: 20 info/warning → 0.@overrideаннотации на subclass fields, удалены избыточные!.- Dispose / dead-code audit — чисто, без правок.
🔖 Preset bundles (spec 033)
Before 1.4.1, a preset was copied into CustomRule at "Add to Rules" time; template updates never reached existing rules.
Now CustomRule(kind: preset) stores only {presetId, varsValues}:
{
"id": "...", "name": "Russian domains direct",
"kind": "preset", "presetId": "ru-direct",
"varsValues": {"outbound": "direct-out", "dns_server": "yandex_doh", "dns_ip": "77.88.8.88"}
}On every build buildConfig expands via expandPreset(rule, preset):
- substitute
@var→ user values or template defaults. - rule_set / dns_rule / routing_rule / dns_servers get injected into the sing-box config.
Preset-local var types: outbound (outbound-group picker), dns_servers (picker of preset.dns_servers[].tag). WizardOption {title, value} syntax — dropdowns show human-readable titles (e.g. "77.88.8.88 · Safe").
Russian domains direct exposes 3 vars:
- Outbound (default
direct-out). - Transport (DoH/DoT/UDP, default
yandex_doh). - UDP server IP — 10 Safe/Base/Family × IPv4/IPv6 primary/alt options, used only when Transport = UDP. DoH/DoT are hardcoded to
77.88.8.88+ SNIsafe.dot.dns.yandex.net.
Existing user rules (created via + Add rule with match fields) stay as CustomRuleInline / CustomRuleSrs — spec 033 does not touch them.
🧬 Sealed CustomRule (task 011)
Previously a single class with enum discriminator, unused fields per kind, runtime ifs.
Now:
sealed class CustomRule { id, name, enabled, kind, toJson, fromJson-dispatch }
├── CustomRuleInline { domains/suffixes/keywords/cidrs/ports/packages/protocols/ipIsPrivate + outbound }
├── CustomRuleSrs { srsUrl + routing-level extra filters + outbound }
└── CustomRulePreset { presetId, varsValues } // outbound lives in varsValues['outbound']
switch (cr)in builder / UI is exhaustive-checked by the compiler.- Type-preserving
withEnabled/withName/withOutboundmutators on the base class — UI can writerule.withEnabled(v)without manual pattern-matching. - Convenience read-only getters (
domains/srsUrl/presetId/ …) for code that doesn't care about the concrete subclass. CustomRule.fromJsondispatches onkind; legacytargetfield is accepted as fallback foroutbound.
🗄️ SRS cache for preset
Spec 011 requires "sing-box never downloads — always type: local, path: <cache>". In 1.4.0-beta preset bundles with type: remote broke this — sing-box fetched rule_set files on its own at startup.
1.4.1 restores the invariant:
RuleSetDownloadergets apreset__<presetId>__<tag>namespace for preset-owned cache.expandPresetcheckssrsPaths[tag]for remote rule_sets — hit → replace with{type: "local", path: "..."}; miss → skip rule_set + warning.buildConfigpre-resolves cache paths beforeapplyPresetBundles.
UI side is in 1.4.1: preset-rules with remote rule_set'ы show the same ☁ button as srs rules in the Rules list. Tap downloads every remote rule_set of the preset into the cache. Long-press → Refresh / Clear menu. The Switch auto-downloads on toggle-on if the cache is missing and enables the rule on success. "Cached" means every remote rule_set of the preset has a local .srs file.
🛠️ Fixes (task 011 polish)
Failed to start service: rule-set not found—expandPresetnow drops arouting_rulewhoserule_setreferences a tag that was skipped (missing cache), instead of leaving a dangling reference that crashed sing-box at startup.- Preset
☁icon now reacts to tap — the oldGestureDetectorwithHitTestBehavior.opaquewas swallowing taps beforeIconButton.onPressed. Replaced withInkWell(onTap+onLongPress). - Adding a preset with remote rule_set'ами via "Add to Rules" creates it disabled (same policy as
CustomRuleSrs). Users tap ☁ to download, then flip the switch — or toggle-on triggers auto-download + enable. - On load, any rule whose SRS cache is missing is auto-disabled (
_refreshSrsCachenow runs after_templateis assigned, so preset lookup actually resolves).
🏷 Rename target → outbound, out → outbound
CustomRule.target is renamed ...
LxBox v1.4.0
L×Box v1.4.0
Android VPN client powered by sing-box.
Major release: unified routing rules model, local-only SRS, Stats tabs + Top apps, Debug API, VPN reliability overhaul, per-server detour toggles, performance pass, correctness fixes.
✨ Highlights
- Unified routing rules —
AppRule+SelectableRuletoggles +CustomRuleслиты в одинCustomRule(spec 030). Один редактор, все поля (domain/IP/port/package/protocol/private-IP/srs) в одной форме. - SRS — только локально — sing-box больше ничего не качает сам. Ручной download через ☁ в UI, никаких скрытых auto-update (spec 011).
- Presets = каталог — вкладка Presets превратилась в read-only каталог пресетов, кнопка "Copy to Rules" клонирует пресет в твой реестр.
- Reorder / context menus — правила перетаскиваются за drag-handle, long-press → Delete с подтверждением, long-press на ☁ в редакторе → Refresh SRS / Clear cached file.
- JSON preview — в редакторе правила вкладка View показывает готовый sing-box фрагмент конфига (rule_set + routing rule) + warnings.
- Stats redesign — Statistics-экран получил табы
Overview/Connections(больше не нужен отдельный navigate), карточка Top apps с иконкой + display name + packageName + byte counters, карточка By routing rule, чип sing-box memory. - Template vars UX — формы в Settings / Routing перерисованы: label сверху, описание во всю ширину, поле — тоже.
Test URL/Test interval/Tolerance (ms)получили preset-дропдауны. URLTest interval default поднят с1mдо5mпод invariant spam-avoidance. - Debug API — локальный HTTP-сервер для dev-introspection/control (spec 031):
/state,/clash/*(proxy с auto-auth),/action/*(триггеры пинга/urltest/rebuild'а),/logs,/config,/files/*,/device. Включается runtime-toggle'ом в App Settings → Developer. - Status sync on reattach — при возврате в приложение со свёрнутого/кильнутого процесса (keep-on-exit + VPN активен) UI больше не застревает в Disconnected.
BoxVpnClient.getVpnStatuspull'ит текущее состояние у native-сервиса вHomeController.init. - Auto-update subscriptions toggle — глобальный выключатель в App Settings → Subscriptions + дубль в
SubscriptionsScreenPopupMenu (три точки). Default ON. Off → автоматические триггеры (appStart / vpnConnected / periodic / vpnStopped) скипаются; ручное ⟳ работает всегда (spec 027). - Clash API reference — docs/api/clash-api-reference.md — полный разбор эндпоинтов, полей connections[].metadata, выключенных/недокументированных возможностей sing-box 1.12.12.
- Background / Battery — App Settings → Battery optimization whitelist status + App info (OEM toggles) с hint-диалогом (spec 022).
- Auto-ping after connect — через 5s после подключения VPN пингуем ноды активной группы автоматом (по умолчанию ON).
- ✨auto rename —
auto-proxy-outтег переименован в✨autoс Icons.speed в UI (единая константаkAutoOutboundTag).
🔧 Unified CustomRule model
Три параллельных механизма слиты в один. До v1.4.0:
AppRule— только per-package.SelectableRule— template пресеты, toggle'ятся в Routing → Presets.CustomRule(v1.3.x) — enum type per rule, один matcher.
Теперь один CustomRule имеет все match-поля сразу:
class CustomRule {
CustomRuleKind kind; // inline | srs
List<String> domains, domainSuffixes, domainKeywords, ipCidrs;
List<String> ports, portRanges;
List<String> packages; // package_name (AND с headless match)
List<String> protocols; // routing-rule level (tls/quic/bittorrent/…)
bool ipIsPrivate; // routing-rule level (ORed with rule_set)
String srsUrl; // kind=srs only
String target; // outbound tag OR "reject"
}Semantics per sing-box default rule formula — OR within category (domain-family, port-family), AND between categories. protocol и ip_is_private эмитятся на routing-rule level (headless rule их не поддерживает).
Один редактор с табами Params / View. Params сгруппирован: APPS (над Source) → Source (inline/srs) → MATCH / RULE-SET URL → PORT → PROTOCOL → Delete. Dirty-aware save (подсветка), unsaved back → "Discard changes?" диалог.
🔧 SRS local-only
Убран type:"remote" + update_interval:"24h" из генерации конфига. Все .srs rule_set'ы хранятся локально в $documents/rule_sets/<rule.id>.srs, эмитятся как type:"local", path:…. Скачивание — только ручное:
- Tile cloud icon — ☁ (not cached) / ✅ (cached, green) / ❌ (failed) / spinner. Tap = download/retry.
- Enable gate — switch правила disabled пока нет cached файла.
- Cleanup — Delete rule удаляет cached файл, URL change на save стирает старый кэш.
- Long-press на ☁ в editor → menu: Refresh SRS / Clear cached file.
Причина: провайдеры банят за бездумные авто-запросы, юзер должен контролировать когда что обновляется (см. invariant feedback_no_unplanned_autoupdates).
🔧 Migration (one-shot)
- AppRule → CustomRule.packages —
SettingsStorage._absorbLegacyAppRulesподхватываетapp_rulesключ при первомgetCustomRules, конвертит, удаляет legacy key. - enabled_rules + rule_outbounds → CustomRule —
RoutingScreen._migrateLegacyPresetsпри первой load'е (флагpresets_migrated) конвертит enabled SelectableRule'ы черезselectableRuleToCustom. Fresh installs получают seed изtemplate.selectableRules.where(r => r.defaultEnabled).
Конвертер поддерживает все формы preset'ов:
rule_set:[remote SRS]→kind=srsrule.rule_set:"<tag>"— разворачивает template inline rule_set в match-поля- inline поля прямо в
rule(включаяip_is_private,protocol) — копируются as-is
✨ Routing UI overhaul
3 табы вместо 4
Routing → Channels | Presets | Rules
- Channels — proxy groups +
route.finalselector (без изменений). - Presets — read-only каталог пресетов с кнопкой "Copy to Rules". Для srs-пресетов копия создаётся в disabled-состоянии (юзер должен скачать файл сначала).
- Rules — реестр
CustomRule. ReorderableListView с drag-handle слева, tile edge-to-edge, long-press → Delete, OutboundPicker inline.
Tile дизайн
┌────────────────────────────────────────────────┐
│ ║ ⬤─ Firefox RU domains ☁ ⌄ direct │
│ ║ 2 suffix · 1 app │
└────────────────────────────────────────────────┘
║— drag handle (reorder).- Switch — включатель (disabled для srs без кэша).
- Имя + summary (2 строки) — tap = edit.
- ☁ — для srs only (download status + tap = refresh).
- OutboundPicker справа — inline смена target'а без открытия editor'а.
Editor — Params / View tabs
Params — все match-поля сразу, грамматически OR/AND per sing-box формуле.
View — готовый JSON preview (rule_set + routing rule) через тот же applyCustomRules, что реально применяется при build'е конфига. Copy button + warnings (например "SRS rule X skipped: no cached file").
🔧 Background / Battery
Новая секция в App Settings:
- Battery optimization — status tile (зелёный/красный), tap открывает системную страницу battery-optimization. Fallback на direct-prompt
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONSесли OEM не поддерживает primary. - App info (OEM power settings) — hint-dialog перечисляет что искать (Autostart, Background activity, Battery, Saver exceptions), потом открывает
ACTION_APPLICATION_DETAILS_SETTINGSс package URI. - Permission
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSдобавлен в AndroidManifest.
🔧 Auto-ping after connect
Через 5s после перехода в connected, HomeController пингует ноды активной группы однократно. Дефолт ON. Отменяется pending timer при disconnect/revoked чтобы не стрельнуть в уже отключённом состоянии. Toggle в App Settings → Feedback.
✨ Stats redesign
StatsScreenтеперьDefaultTabController(length: 2)с вкладкамиOverview/Connections. Вкладка Connections —ConnectionsView, тот же самый компонент что раньше был отдельным экраном, теперь embeddable (без Scaffold/AppBar).- Top apps card — топ-10 приложений по суммарному трафику с иконкой + display name + packageName (монospace subtitle) +
N conns ↑↑ ↓↓. Lazy-cacheAppInfoCacheдёргает nativegetAppInfoper-package,AnimatedBuilderперерисовывает строку когда ответ пришёл. - By routing rule card — распределение активных соединений по rule+rulePayload с процентной полоской.
- Memory chip —
sing-boxрасход RAM в 4-чиповом топ-карде (из/connections.memoryполя Clash API). - Удалена dead навигация: Connections-чип больше не кликабельный (вместо этого — вкладка).
✨ Template vars UX
Формы шаблонных переменных (routing screen Auto Proxy, settings_screen core vars) перерисованы:
- Label сверху — больше нет
ListTileс label/subtitle слева и узким input'ом справа. На узких экранахTest URLраньше ломался наTest/URL(две строки), описание сжималось до 13 символов в строку. - Поле на всю ширину —
Column(crossAxisAlignment: stretch), field растягивается, suffix ▾ прижат справа. EnumDropdownButtonполучилisExpanded: true. - Presets для URLTest —
urltest_intervalдропдаун:30s / 1m / 3m / 5m / 10m / 30m.urltest_tolerance:10 / 30 / 50 / 100 / 200. - Interval default
1m→5m— согласуется с invariantfeedback_subscription_no_spam/feedback_no_unplanned_autoupdates. Юзеры без ручного override получают 5m (меньше фонового трафика).
✨ Debug API (spec 031)
Локальный HTTP-сервер, bind'ится на 127.0.0.1:9269, авторизация Bearer-токеном из App Settings → Developer. Позволяет с хоста по adb forward tcp:9269 tcp:9269 читать state и триг...