Skip to content

Releases: Leadaxe/LxBox

LxBox v1.8.2

12 May 01:21

Choose a tag to compare

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. До этого версия дублировалась в:

  1. app/pubspec.yaml version: X.Y.Z+N
  2. app/lib/screens/about_screen.dart static const _version = 'X.Y.Z'
  3. 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.dart NEW
  • app/lib/main.dartawait VersionInfo.I.init() перед runApp
  • app/lib/screens/about_screen.dart_version const удалён; versionString alias удалён
  • app/lib/screens/home_screen.dart + app_settings_screen.dart + services/debug/handlers/action.dart — 6 occurrence'ов AboutScreen.versionStringVersionInfo.I.version
  • app/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 → success
  • dumpsys package | grep version → корректно
  • App → Settings → About → v1.8.2
  • UpdateChecker → silent (current == latest)
  • Debug API /state.app_version1.8.2+N
  • Backup source_app_version1.8.2+N

Все surfaces consistent.


🇷🇺 LxBox v1.8.2 на русском

Что изменилось в инфраструктуре (для пользователя ничего видимого):

  • Версия приложения теперь берётся только из git тега, никаких параллельных источников
  • В коде убрана hardcoded константа _version = '1.8.1' — больше не нужно её править при релизе
  • В pubspec.yaml навсегда стоит placeholder 0.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.Zgit 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

LxBox v1.8.1

12 May 00:49

Choose a tag to compare

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'е две независимых записи версии:

  1. pubspec.yaml version: X.Y.Z+N — рисует Android versionName/versionCode, попадает в PackageInfo.fromPlatform()
  2. app/lib/screens/about_screen.dart static const _version = 'X.Y.Z' — sync compile-time const, читается About screen + UpdateChecker.checkForUpdate()

При release v1.8.0 поднял (1), забыл (2). Эффект:

  • Settings → About показывал v1.7.0
  • UpdateChecker сравнивал versionString='1.7.0' с latest GitHub tag v1.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.ymlchecks 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.yaml version:about_screen.dart _versionвсегда
  • Git tag vX.Y.Zpubspec.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

LxBox v1.8.0

11 May 11:17

Choose a tag to compare

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=false overwrite целиком, при merge=true top-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 в storage map — будущие 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):
    владеет всеми 8 TextEditingController-ами, флагами, коллекциями,
    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
001 mobile stack 055 Historical architectural decision
002 MVP scope 056 Historical milestone
004x subscription parser 057 Superseded by §026
005x config generator 058 Superseded by §026
013 routing 059 Superseded by §030
039 libbox 1.13 migration 060 One-shot migration (Done)
041 DNS rules refactor 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 нашёл два расхождения:

  1. docs/ARCHITECTURE.md → Feature Specs всё ещё перечисляла 7
    демотированных как live features → синхронизирована с
    docs/spec/features/README.md (демотированные в отдельной секции).
  2. 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 по...

Read more

LxBox v1.7.0

08 May 17:50

Choose a tag to compare

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:

  1. Statistics → Per-app (или с HomeScreen tap по traffic bar'у когда recording active)
  2. Pick app → выбрать приложение из picker'а
  3. ▶ START — recording пошёл
  4. Походить по приложению, дать трафик пройти
  5. ⏹ 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-поле матчит по domainipcname 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 _buildTrafficBar chip ⚡ <pkg> рядом с traffic stats; tap всей строки → StatsScreen(initialTab: perApp)
  • В Stats у Per-app tab 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: cached regex добавлен (помимо 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.

🏗 Under the hood

  • AppInfoCache унификация icon-cache между Custom Rules и Per-app picker'ами. loadAllApps() (lightweight installed-apps list, без иконок) + smart ensure(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

Latest release on GitHub →

Большинству юзеров нужен 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 test510 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-ru cache 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 ...
Read more

LxBox v1.6.1

07 May 23:14

Choose a tag to compare

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_resolvergoogle_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.descriptionref.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 меняет только enabled ref'а, kind остаётся template.
  • После tag rename из §039 (direct_dns_resolvergoogle_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» удалена.
  • ResolvedServer typed-class в dns_settings_screen.dart заменил underscore-аннотированные Map<String, dynamic>. Render layer стал явно typed, accessors через named fields.
  • Render order в UItemplatepresetinline (сорт по 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

Latest release on GitHub →

Большинству юзеров нужен 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.dartresolveDnsServersList (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 schemadns_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'а файлов на диске.

📦 Установка

Последний релиз на GitHub →

Большинству нужен 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

06 May 23:17

Choose a tag to compare

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-network Debug API. Calls sing-box commandServer.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.writeDebugMessageEventChannel("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 /config with 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 without ctx.Done()-awareness; the first time an upstream DNS-transport froze, all subsequent waiters parked forever, including the probe-mechanism goroutines. Fix — upstream commit aba8346b ("Fix DNS cache lock goroutine leak"), shipped in sing-box v1.12.21+ and v1.13.0+.
  • Mass ping cancel actually cancels (§034). Tapping Stop during a mass-ping previously left three side-effects:
    1. Spinner indicators kept hanging until timeout for nodes that hadn't responded yet (worker break without cleaning up pingBusy state).
    2. _runAllUrltestGroups kept iterating the auto-group regardless of cancel.
    3. In-flight HTTP delay/groupDelay requests kept executing (Dart http.Client has no per-request cancel).
      Fix: cancelMassPing now (1) clears pingBusy entirely; (2) _runAllUrltestGroups(epoch) checks epoch on every iteration; (3) ClashApiClient has a separate _delayHttp client for delay/groupDelay — cancelDelays() closes it, in-flight HTTP sockets drop, workers get an exception and break.
  • Clash delay/groupDelay timeout sync (§040). The Dart-side wrapper used a hardcoded _timeout = 10s regardless of the timeoutMs query-param sent to cl...
Read more

LxBox v1.5.0

29 Apr 22:37

Choose a tag to compare

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-box type: "naive" outbound, share-URI round-trip. 10th typed protocol in Parser v2. Cronet (with_naive_outbound) is already bundled in our libbox.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 dump button (⤴ in Debug AppBar) or GET /diag/dump:
    • A. stderr-redirect — Go panic stacktrace from libbox/sing-box; written to filesDir/stderr.log before SIGABRT, survives the process. New conditional stderr tab in Debug screen.
    • B. ApplicationExitInfo (Android 11+) — getHistoricalProcessExitReasons lazy-read in DumpBuilder. Reason + tombstone for NATIVE_CRASH or Java stacktrace for CRASH.
    • C. Persistent AppLogwarning + error levels written to filesDir/applog.txt (ring-buffer, 200 entries / 64 KB cap). Loaded on main() with fromPreviousSession=true, visually marked in UI; survives process restart.
    • D. Logcat tailRuntime.exec("logcat", "-d", "-t", 1000, "*:E") via ProcessBuilder (no READ_LOGS permission needed; logd UID-filters automatically). Catches AndroidRuntime FATAL EXCEPTION, libc/DEBUG/tombstoned, art/linker. Especially useful when AEI didn't attach trace (Samsung One UI quirk on REASON_CRASH).
  • 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/* groupGET /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 now from 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_NOTIFICATIONS on Android 13+).

🐞 Fixes

  • CHANGE_NETWORK_STATE permission for Android 9-11 (task 023) — DefaultNetworkListener on API 28-30 calls ConnectivityManager.requestNetwork(...), which requires CHANGE_NETWORK_STATE. Without it: SecurityExceptionREASON_CRASH immediately 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 is normal-level, no runtime prompt, silent migration.
  • VLESS packetEncoding allow-list (task 012) — xray-style subscriptions encode packetEncoding=none in their URIs, which produced "packet_encoding": "none" in outbound JSON; sing-box vless.NewOutbound only accepts xudp/packetaddr/omitted and called E.New("unknown packet encoding: …"), which crashed libbox via an upstream format.ToString bug. Parser now normalises on input: xudp/XUDPxudp, PacketAddrpacketaddr, none silently dropped, anything else → warning + drop.
  • Race: Libbox.newService before Libbox.setup finishes (task 027) — BoxApplication.libboxReady: CompletableDeferred<Unit> barrier; BoxVpnService serviceScope.launch waits for it before any libbox call. Bonus: libbox workingDir moved from external (getExternalFilesDir(null)) to internal (context.filesDir) — same place where SettingsStorage and 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.refreshTile and QuickShortcuts.refresh gated on API 30+ with outer try { Throwable }; all callsites in setStatus/onDestroy/initialize wrapped in runCatching. FOREGROUND_SERVICE_SPECIAL_USE permission gated to minSdkVersion="34"; typed startForeground on API 34+.

Reliability internals

  • Libbox.newService / svc.start / serviceScope.launch catch Throwable (task 016) — not just Exception; Error subclasses (OOM, NoClassDefFoundError, VerifyError) now surface through stopAndAlert(...) instead of vanishing the process.
  • /files/local Debug API alias for /files/external (legacy). Internal app-scoped storage.

⚠ Breaking

  • Tunnel sleep mode default flipped: lazynever. 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

Latest release on GitHub →

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. Permission normal-уровня, без 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-11Tile.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) собираются ...
Read more

LxBox v1.4.2

22 Apr 20:54

Choose a tag to compare

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

22 Apr 18:53

Choose a tag to compare

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 from wizard_template.json on every build. Template updates automatically propagate to every user without data migrations. Example: Russian domains direct is parametric — pick Outbound + DNS Transport (DoH/DoT/UDP) + UDP server IP (Safe/Base/Family).
  • CustomRule sealed split (spec 030 §v1.4.1, task 011) — the single class is split into CustomRuleInline / 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 local RuleSetDownloader cache 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 presetsOutboundPicker в tile работает для любого bundle'а, даже если его template использует shorthand rule.action: "reject" (Block Ads) или hardcoded rule.outbound: "<tag>" (Russia-only services). Override пишется в varsValues['outbound'] и применяется в preset_expand независимо от формы template'а. Shorthand action: "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/rules serializer переписан на sealed-dispatch: inline/srs/preset правила отдают только релевантные поля. Preset-правила теперь экспозят preset_id, vars_values, nested preset.remote_rule_sets[] с per-rule_set SRS-cache статусом (cached, path, mtime), флаг ready. Debug API честно отражает sealed-иерархию task 011.
  • SRS cache pruneRuleSetDownloader.pruneOrphans() при каждом _refreshSrsCache сметает .srs-файлы без живого референса. Orphan'ы после миграций схемы (pre-bundle → bundle) больше не копятся.
  • .онлайн (xn--80asehdb) в Russian domains direct — добавлен в inline rule_set ru-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. Красный экран заменён на компактный ErrorBoundary fallback-widget.
  • Auto-updater coverage — §027 spam-gate покрыт тестами (consecutiveFails, minRetryInterval, maxFailsPerSession, inProgress crash-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 time2h 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 consecutiveFails counter'а, размораживает подписку из 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 + SNI safe.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 / withOutbound mutators on the base class — UI can write rule.withEnabled(v) without manual pattern-matching.
  • Convenience read-only getters (domains / srsUrl / presetId / …) for code that doesn't care about the concrete subclass.
  • CustomRule.fromJson dispatches on kind; legacy target field is accepted as fallback for outbound.

🗄️ 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:

  • RuleSetDownloader gets a preset__<presetId>__<tag> namespace for preset-owned cache.
  • expandPreset checks srsPaths[tag] for remote rule_sets — hit → replace with {type: "local", path: "..."}; miss → skip rule_set + warning.
  • buildConfig pre-resolves cache paths before applyPresetBundles.

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 foundexpandPreset now drops a routing_rule whose rule_set references 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 old GestureDetector with HitTestBehavior.opaque was swallowing taps before IconButton.onPressed. Replaced with InkWell(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 (_refreshSrsCache now runs after _template is assigned, so preset lookup actually resolves).

🏷 Rename target → outbound, out → outbound

CustomRule.target is renamed ...

Read more

LxBox v1.4.0

20 Apr 21:29

Choose a tag to compare

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 rulesAppRule + SelectableRule toggles + 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.getVpnStatus pull'ит текущее состояние у native-сервиса в HomeController.init.
  • Auto-update subscriptions toggle — глобальный выключатель в App Settings → Subscriptions + дубль в SubscriptionsScreen PopupMenu (три точки). Default ON. Off → автоматические триггеры (appStart / vpnConnected / periodic / vpnStopped) скипаются; ручное ⟳ работает всегда (spec 027).
  • Clash API referencedocs/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 renameauto-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.packagesSettingsStorage._absorbLegacyAppRules подхватывает app_rules ключ при первом getCustomRules, конвертит, удаляет legacy key.
  • enabled_rules + rule_outbounds → CustomRuleRoutingScreen._migrateLegacyPresets при первой load'е (флаг presets_migrated) конвертит enabled SelectableRule'ы через selectableRuleToCustom. Fresh installs получают seed из template.selectableRules.where(r => r.defaultEnabled).

Конвертер поддерживает все формы preset'ов:

  1. rule_set:[remote SRS]kind=srs
  2. rule.rule_set:"<tag>" — разворачивает template inline rule_set в match-поля
  3. inline поля прямо в rule (включая ip_is_private, protocol) — копируются as-is

✨ Routing UI overhaul

3 табы вместо 4

Routing → Channels | Presets | Rules
  • Channels — proxy groups + route.final selector (без изменений).
  • 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-cache AppInfoCache дёргает native getAppInfo per-package, AnimatedBuilder перерисовывает строку когда ответ пришёл.
  • By routing rule card — распределение активных соединений по rule+rulePayload с процентной полоской.
  • Memory chipsing-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 ▾ прижат справа. Enum DropdownButton получил isExpanded: true.
  • Presets для URLTesturltest_interval дропдаун: 30s / 1m / 3m / 5m / 10m / 30m. urltest_tolerance: 10 / 30 / 50 / 100 / 200.
  • Interval default 1m5m — согласуется с invariant feedback_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 и триг...

Read more