Skip to content

v2 foundation: Tauri 2 + Rust engine + Svelte frontend#25

Open
bryanroscoe wants to merge 16 commits into
mainfrom
feat/v2-foundation
Open

v2 foundation: Tauri 2 + Rust engine + Svelte frontend#25
bryanroscoe wants to merge 16 commits into
mainfrom
feat/v2-foundation

Conversation

@bryanroscoe
Copy link
Copy Markdown
Owner

First substantive landing of the v2 rewrite. Per your "one-shot it" instruction, this is a working foundation — not full v1 parity, but everything that ships actually works and is honest about what's done vs. not.

What works

  • `npm run tauri dev` opens a real desktop window with a working app
  • Lists ADB devices with `[NET]`/`[USB]` tags, UNAUTHORIZED handling, friendly Shield model names (mdarcy → "Shield TV Pro (2019)", etc.)
  • Tabbed device-detail view: Overview (full getprop dump), Health (display mode + HDR + top memory users), Launcher (installed/disabled status + active launcher + Watch Next warning), Apps (device-type-filtered bloat list with risk badges), Snapshot (save state, list snapshots, preview-apply with package counts and cross-device warning)
  • Connect-by-IP form, disconnect
  • 36 Rust tests cover the engine + ADB parsers + JSON loader
  • New CI workflow runs cargo fmt + clippy -D warnings + tests on ubuntu/macos/windows, plus svelte-check and vite build

What's not wired yet (deferred to follow-up PRs)

Treating these as separate PRs keeps the review surface manageable.

Architectural commitments honored

# Commitment How
1 Engine has no I/O Every fn in `engine/` is pure; loader (file I/O) lives in `commands/` host layer
2 App lists are runtime data `v2/data/app-lists/*.json`, embedded at compile time; loader is in host layer
3 One ADB wrapper All ADB calls go through `SubprocessAdb` (single `AdbDriver` trait impl)
4 One detection fn `engine::detection::detect_device_type` — consolidates v1's two paths (FEATURES.md §13.1)
5 Versioned snapshots `schema_version` field; `SnapshotError::UnsupportedSchema` rejects future versions with a clear error; tested
6 Strict mode All fallible ops return `Result`; clippy -D warnings in CI
7 Reversibility Snapshot apply is additive-only for disable state (enforced in `compute_apply_plan`)

Validation

  • `cd v2/src-tauri && cargo fmt --check` ✅
  • `cd v2/src-tauri && cargo clippy --lib --tests -- -D warnings` ✅
  • `cd v2/src-tauri && cargo test --lib` → 36 passed / 0 failed
  • `cd v2 && npm run check` → 0 errors / 0 warnings
  • `cd v2 && npm run build` ✅

Files

61 files changed, ~11k lines. High-level layout:

  • `v2/src-tauri/src/engine/` — pure logic (types, detection, app_lists, launcher, snapshot)
  • `v2/src-tauri/src/adb/` — driver trait + subprocess impl + output parsers
  • `v2/src-tauri/src/commands/` — Tauri command bridge, state, loader
  • `v2/src/` — Svelte 5 + SvelteKit frontend
  • `v2/data/app-lists/` — common / shield / googletv JSON
  • `.github/workflows/v2-tests.yml` — CI matrix

`v2/README.md` has the full project layout and "what works / what doesn't" table.
`v2/PLAN.md` updated with phase-status marks (Phases 1-4 largely landed, Phases 5+ remaining).

Demo

```bash
cd v2 && npm run tauri dev
```

Opens a real window. With a paired Shield on your LAN, paste its IP into the Connect form and click through the tabs.

First substantive landing of the v2 rewrite. Phases 1-4 from
v2/PLAN.md are largely complete; the app compiles, tests pass on
all three OSes via the new v2-tests workflow, and `npm run tauri dev`
opens a working window that lists devices, shows profiles, runs the
Health Report (display mode + HDR + top memory), enumerates launchers,
and saves / previews snapshots.

What's here
-----------

Engine (src-tauri/src/engine/) — pure logic, no I/O:
- types.rs:       Device, AppEntry, OptimizeAction, RiskTier, etc.
- detection.rs:   ONE device-type-detection function consolidating v1's
                  two divergent paths (resolves FEATURES.md §13.1).
- app_lists.rs:   AppListBundle::for_device merging logic with
                  device-specific override semantics.
- launcher.rs:    preset launcher catalog (including the corrected
                  Dispatch package id) + package-name validation.
- snapshot.rs:    versioned schema (schema_version=1), unknown-version
                  rejection, ApplyPlanInputs → SnapshotApplyPlan
                  computation (no I/O).

ADB driver (src-tauri/src/adb/):
- driver.rs:      AdbDriver trait + SubprocessAdb impl with 30s
                  timeouts, kill-on-drop, typed AdbError, structured
                  AdbOutput, PATH search for adb binary.
- parse.rs:       Output parsers for `adb devices`, `pm list packages`,
                  `dumpsys meminfo` (Total PSS by process — sums
                  per-base-package across worker processes), and
                  `dumpsys display` (modeId → supportedModes lookup,
                  HdrCapabilities mSupportedHdrTypes decoding).

Tauri commands (src-tauri/src/commands/):
- state.rs:       AppState with Arc<dyn AdbDriver>.
- loader.rs:      embeds data/app-lists/*.json at compile time and
                  parses them; honors commitment #2 (loader in host
                  layer, not engine).
- devices.rs:     list_devices, device_profile, connect_device,
                  disconnect_device.
- health.rs:      health_report (display + top memory in one call),
                  app_list_for_device.
- launcher.rs:    list_launchers, current_launcher,
                  channel_provider_disabled.
- snapshot.rs:    list_snapshots, save_snapshot, preview_apply.

Data (v2/data/app-lists/):
- common.json:    universal Android TV bloat — includes the defunct
                  apps (Funimation, Stadia, Quibi, HBO Now legacy)
                  with shutdown-date descriptions, all major streaming
                  apps as opt-in, and the channel-breaking
                  providers.tv flagged High Risk with the cost
                  explained.
- shield.json:    Nvidia-specific bloat.
- googletv.json:  Onn / Chromecast / Streamer-specific bloat.

Frontend (v2/src/):
- SvelteKit (SPA mode via adapter-static), Svelte 5 (runes), TypeScript.
- lib/api.ts:     typed wrappers around every Tauri invoke().
- lib/types.ts:   TS counterparts of Rust serde types.
- routes/+layout.svelte: dark-theme app shell, top nav.
- routes/+page.svelte:   device list with Connect IP form, UNAUTHORIZED
                         handling, friendly Shield model names.
- routes/devices/[serial]/+page.svelte: tabbed device detail
                         (Overview / Health / Launcher / Apps /
                         Snapshot) — lazy-loads each tab.
- routes/snapshots/+page.svelte: global snapshot list.

CI (.github/workflows/v2-tests.yml):
- cargo fmt --check, cargo clippy -D warnings, cargo test --lib
  on ubuntu-latest, macos-latest, windows-latest.
- Frontend: npm run check + npm run build on ubuntu.

Validation
----------
- 36 Rust tests passing (engine + ADB parsers + loader sanity).
- cargo fmt and clippy clean.
- svelte-check 0 errors / 0 warnings.
- npm run build produces output.

Architectural commitments honored
---------------------------------
- #1 Engine has no I/O: every fn in engine/ is pure; tests prove
  it by exercising them without an ADB driver instance.
- #2 App lists are runtime data, not embedded code: data files
  live in v2/data/, loader is in commands/ (host layer), engine
  receives bundle as input.
- #3 All ADB output goes through one wrapper (SubprocessAdb).
- #4 Detection logic exists exactly once.
- #5 Snapshots are versioned: schema_version field, unknown-version
  rejection tested.
- #6 Strict mode: every fallible op returns Result; clippy treats
  warnings as errors in CI.
- #7 Reversibility model preserved: methods (disable/uninstall)
  carried through; snapshot apply is additive-only for disable
  state (engine enforces this in compute_apply_plan).

What's not yet wired
--------------------
- Optimize/Restore execution (engine computes plans; execution is
  the next layer).
- Launcher set-default actions (UI shows status; promotion logic
  via role API + set-home-activity still to port).
- Snapshot apply execution (preview works; execution is paired
  with optimize execution).
- Tweaks (HDMI-CEC, match-content-frame-rate, long-press-timeout).
- Display Scaling, APK sideload, Reboot, Recovery.
- Bundler config for installers (Phase 10).
- Auto-update plugin (Phase 10).
- Mobile (Phase 11 — gated on ADB-wire-protocol research spike).

v2/PLAN.md updated with status marks; v2/README.md updated with
the current project structure and "what works / what doesn't"
table.
Two fixes in one commit:

1. The previous v2 foundation commit silently dropped the entire
   src-tauri/src/adb/ source tree because the repo-root .gitignore
   has an unanchored `adb` pattern (matching the v1 binary at the
   top level). That pattern was also matching my src-tauri/src/adb/
   directory containing driver.rs / parse.rs / install.rs / mod.rs —
   so PR #25 had a non-functional CI matrix (the local build worked
   only because the files were on disk).

   Fix: anchor the root .gitignore patterns to `/adb` and `/adb.exe`
   so they only ignore the repo-root binaries, and force-add the
   src-tauri/src/adb/ files.

2. The GUI surfaced "adb binary not found at adb" because GUI apps
   on macOS don't inherit the user's shell PATH, so even with adb
   installed via Homebrew, the launched bundle couldn't find it.

   This commit matches v1's Check-Adb behavior — try harder to locate,
   and if nothing's found, offer a one-click download of Google's
   official platform-tools (same source URL v1 uses):

   - `discover_adb_binary`: walks a deterministic priority list —
     SHIELD_OPTIMIZER_ADB env var, our own install root (so a previous
     download is picked up), ANDROID_HOME/SDK_ROOT, PATH search, and
     OS-specific well-known locations (Homebrew dirs on macOS, distro
     bins on Linux, LOCALAPPDATA on Windows, plus Android Studio's
     bundled SDK paths).
   - `install_platform_tools` (adb/install.rs): downloads `platform-
     tools-latest-<os>.zip` from `dl.google.com/android/repository/`,
     extracts to `<data_local_dir>/ShieldOptimizer/platform-tools/`
     with zip-slip protection and Unix exec-bit preservation. Returns
     the path to the extracted adb.
   - `AppState.adb` is now `RwLock<Arc<dyn AdbDriver>>` so the live
     driver can be hot-swapped post-install without restarting the app.
   - New `NoAdbDriver` fallback returns `AdbError::BinaryNotFound`
     (with a long-form error message explaining install options) on
     every call when nothing was found at startup. The UI matches on
     that string to render the install pane.
   - New Tauri commands: `adb_status`, `install_adb`.
   - Frontend: when listDevices errors with "could not locate an adb
     binary", the device list view swaps in an install pane with a
     "Download platform-tools" button that calls `install_adb`,
     then refreshes.

   Also took the opportunity to make health_report and snapshot
   queries actually run concurrently with tokio::join! — the previous
   code awaited sequentially despite spawning futures.

Tests: 41 passing (up from 36; added discover_uses_explicit_env_override,
discover_returns_none_when_nothing_exists, install_root_is_under_
data_local_dir, platform_tools_url_matches_current_os, and
extract_zip_rejects_zip_slip). clippy -D warnings clean. svelte-check
0 errors.
Three independent reviews surfaced a mix of correctness, security,
accessibility, and consistency issues. Fixes by severity:

🔴 Correctness / security

- SubprocessAdb::run now returns AdbError::NonZeroExit when the
  process exited nonzero, instead of silently returning Ok with
  empty stdout. Downstream parsers were treating "pm list packages
  failed" as "no packages" (Codex finding).
- Zip-slip protection in adb/install.rs now rejects absolute paths
  in archive entries (not just `..` traversal) and double-checks
  that the resolved out_path is contained under `root` (Rust review
  + Codex).
- preview_apply confines snapshot reads to the configured
  snapshot_dir — canonicalize both paths and verify containment
  before reading. Frontend can no longer hand us arbitrary paths
  (Codex).
- Snapshot::from_json rejects schema_version=0 (was accepted as v1
  because the check was only `> SCHEMA_VERSION`). Added a regression
  test (Rust review).

🟡 UX / accessibility

- Device-detail tabs now have proper role="tablist" / role="tab" /
  aria-selected / aria-controls / tabpanel wiring with tabindex=0 on
  panels. Container elements switched to <div> per Svelte's a11y
  linter (frontend review).
- Snapshot "preview only" disclaimer promoted from a quiet muted
  line below the counts to a yellow warning box ABOVE them, so
  users won't mistake the preview for an in-progress execution
  (frontend review).
- Adb-missing detection no longer parses a substring out of an
  error message — calls adb_status() first and renders the install
  pane on `available: false` directly (Codex + frontend review).
- Apps tab gained a Refresh button (frontend review).

🟢 Polish / specs

- AdbStatus.path is now populated (was documented but always None)
  via discover_adb_binary in the status command (Codex).
- once_cell::sync::Lazy → std::sync::LazyLock; dropped once_cell
  dep entirely (Rust review).
- save_snapshot batches all 9 `settings get` calls into one shell
  invocation instead of looping (~200ms vs ~1.8s; Codex).
- connect_device now validates IPv4 + port shape on the backend
  via normalize_connect_address; rejects empty, malformed octets,
  and out-of-range ports. 6 unit tests added (Codex + frontend).
- Snapshot::from_json uses one Value parse + from_value, instead
  of parsing the input string twice (Rust review).
- HTML title is "Shield Optimizer" instead of the scaffold
  placeholder.
- README's "Frontend framework: Pending decision" updated to
  "Svelte 5 + SvelteKit (locked)" — the actual current state
  (Codex called out the drift).
- Removed unused scaffold SVGs from static/.

Verified: cargo fmt --check, cargo clippy --lib --tests -D warnings,
cargo test --lib (48 passing, was 41), npm run check (0/0 warnings),
npm run build.

Deferred to follow-up PRs (called out but not addressed here to
keep the diff focused):
- Typed errors across the IPC boundary (replace
  `Result<T, String>` with a serde-derived error enum) — bigger
  refactor.
- Splitting devices/[serial]/+page.svelte (~600 lines now) into
  per-tab components — cosmetic.
- App-list runtime-fetch from a versioned URL — the README/PLAN
  commitment is still TODO; for now embedded JSON is the only
  source.
- Removing or restricting the CWD walk-up in discover_adb_binary.
- $app/stores → $app/state (Svelte 5 routing migration).
Three substantive additions in response to the "make sure we're not
missing features" follow-up and the "we need an icon" ask.

Network discovery (v1 parity for Scan-Network)
---------------------------------------------
- New adb/scan.rs:
  - local_subnet_prefix(): detects default gateway per-OS (macOS: route -n
    get default; Linux: /proc/net/route first, falling back to `ip route`;
    Windows: route print -4 0.0.0.0). Honors SHIELD_OPTIMIZER_SUBNET env
    var as override (matches v1's -Subnet flag).
  - scan_subnet(): probes all 254 hosts in the /24 by TCP-connecting to
    port 5555 in parallel via FuturesUnordered with 64 in-flight + 250ms
    timeout per probe. Priority sweep mirrors v1's order (100-150 first,
    then 2-99, then 151-254).
- New commands/scan.rs:
  - scan_network Tauri command: runs scan_subnet, attempts adb connect
    against each responder, returns a structured ScanResult with subnet
    label / found / connected / failed lists + human-readable summary.
- Frontend: device list page now has a "Scan Network" button next to
  Connect IP. Streams the summary message into the same area.

This is a faster, simpler approach than v1's ICMP+ARP+adb_connect chain.
TCP-probing :5555 directly tells us whether ADB is listening, which is
the only thing we actually care about for the device list. Many TVs
firewall ICMP but must expose 5555 to be reachable at all.

Health Report — full vitals
---------------------------
v1's Health Report showed temp, RAM totals, storage, plus display mode
and top memory. v2 was only showing display + top memory. Now matches
v1 surface area:
- parse_meminfo_summary: extracts Total/Free/Used/ZRAM from the dumpsys
  meminfo summary block (handles both K and M units across Android
  versions; normalizes to MB).
- parse_thermal_max_celsius: pulls the highest readable zone temp from
  dumpsys thermalservice. Tolerates two known formats (`mValue=42.0`
  Temperature{} blocks and `temp=42.0` rows). Sanity-clamps to 10-120°C
  to filter the 0.0 / 999.0 garbage values some sensors emit.
- parse_storage_info: pulls Size/Used/Avail/Use% from df -h /data.
- health_report command fetches all four dumpsys outputs concurrently
  via tokio::join! (display + meminfo + thermalservice + df) and
  decodes into a single typed payload. Thermal and storage are
  best-effort — a permissions-restricted device gets None values
  rather than a whole-report failure.
- Frontend Health tab shows a new "Vitals" section above the existing
  Display & Audio: temperature, RAM (used / total + %), Swap, Storage
  (used / total + %).

App icon
--------
- Designed a heater-shield + checkmark monogram in our blue palette
  (#58a6ff → #1f6feb gradient over the app's #1f6feb → #0a2a5e
  rounded-square background). The shield resonates with both the
  Nvidia Shield audience and the "protect/optimize" semantic; the
  checkmark reads as "verified / optimized" at small sizes.
- SVG source checked in at v2/src-tauri/icons/source.svg with a 1024px
  PNG cached alongside; ran `npm run tauri icon` to regenerate every
  platform variant from it (macOS .icns, Windows .ico, every Square*
  Windows-Store size, all Android mipmap densities, all iOS sizes).

Tests
-----
Up to 56 passing (was 48). New coverage:
- parses_meminfo_summary_kb
- parses_thermal_max_temp + parses_thermal_rejects_garbage_values
- parses_df_data_storage
- parses_dotted_prefix / rejects_malformed_prefix
- parses_linux_le_hex_ip / rejects_bad_hex

cargo fmt / clippy -D warnings / svelte-check all clean.
If the initial `adb devices` returns nothing (i.e. no previously-paired
devices are visible) and adb is available, kick off a scan_network
without waiting for the user to click. Matches v1's behavior — users
with a Shield on their LAN don't have to do anything to see it.

Skips when adb is missing (we render the install pane instead) or when
there's already at least one authorized device in the list.
User feedback was that v2 was read-only — just data with no way to
change anything. This commit wires up the write surface:

Backend commands
----------------
- commands/launcher.rs::set_default_launcher — ports v1's full multi-
  strategy promotion (PR #17/#18). Order: `pm enable` → role API
  (skips on "Unknown command") → `cmd package query-activities` to
  discover the HOME activity, then set-home-activity via cmd package
  and pm aliases with --user 0 → HOME-intent kick fallback. Each
  attempt is verified by re-resolving the active launcher. Returns
  a structured SetLauncherResult { ok, strategy, current_launcher,
  last_error } so the UI can show which strategy worked / failed.
- commands/apps.rs — disable_package / enable_package /
  uninstall_package / reinstall_existing. Each runs the matching
  `pm` subcommand with --user 0, inspects stdout/stderr for Failure
  markers (pm's exit codes are unreliable across Android versions),
  returns ActionResult { ok, message }.
- commands/sideload.rs::install_apk — `adb -s <serial> install -r
  <path>`, with `decode_install_error` translating known
  INSTALL_FAILED_* / DELETE_FAILED_* codes into user-readable hints
  (insufficient storage, version downgrade, no matching ABIs, etc.).
- tauri-plugin-dialog added so the frontend can show a native file
  picker for APKs.

Frontend
--------
- Health tab gained a "Live refresh" checkbox + last-refreshed
  relative-time label (`Updated 5s ago`, ticks every second when
  live). Auto-polls every 3s while on. Cleans up timers on destroy.
- Top Memory Users now shows up to 20 entries (was 10) and each
  row gets a "Disable" button that calls disable_package and writes
  the result inline.
- Launcher tab — each installed launcher row now has a "Set as
  default" button that calls set_default_launcher and reloads the
  list. The currently-active launcher is tagged ACTIVE.
- App List tab — each row has Disable/Enable/Uninstall buttons
  (Uninstall only for entries marked UNINSTALL method). Uninstall
  asks for confirmation. Result message renders above the table.
- New "Install APK" tab — opens the native file picker via
  tauri-plugin-dialog, runs install_apk against the device, surfaces
  the verbatim adb output and decoded hint if the install failed.

Capabilities
------------
- dialog:allow-open added to capabilities/default.json so the file
  picker works.

Tests
-----
56 passing (same as before — no new parser logic, just commands and
UI). cargo fmt / clippy -D warnings / svelte-check all clean.
…s + HANDOFF doc

Mid-session checkpoint before context clear. Lands the next batch of
backend commands needed to close the v1 parity gap (audit in PR
comments showed v2 was ~40% of v1's feature surface). Frontend UI
for these still TODO — see v2/HANDOFF.md §2 for resume plan.

Backend commands added
----------------------
- commands/recovery.rs::panic_recovery — v1's Run-PanicRecovery (§12).
  Reads pm list packages -d, runs pm enable per package, returns
  per-package success/failure with a summary message.
- commands/reboot.rs::reboot_device(serial, mode) — v1's Show-RebootMenu
  (§11). RebootMode enum: normal / recovery / bootloader.
- commands/tuning.rs — v1's Set-DisplayInputTuning (§7) + Set-DisplayScaling
  (§8). Three commands:
    * get_tweaks(serial) — batched settings get for all 9 tracked keys
      (HDMI-CEC quad, match_content_frame_rate, long_press_timeout,
      animation triple). Returns TweaksState.
    * write_setting(serial, namespace, key, value) — settings put or
      settings delete (empty value). Namespace whitelist + shell-meta
      character rejection on value.
    * set_display_scaling(serial, preset) — wm size + wm density,
      DisplayScalePreset enum (uhd_4k / fhd_1080p / reset).
- commands/snapshot.rs::apply_snapshot — was the biggest regression in
  the audit. Preview-only before; now actually executes:
    1. Path-confined read of the snapshot file (same canonicalize-and-
       contain check as preview_apply).
    2. Fetches current installed/disabled lists, computes the plan.
    3. pm disable-user --user 0 each package from the plan.
    4. Calls set_default_launcher_impl with the saved launcher.
    5. Batches all "settings put" writes into one shell call.
    6. Returns ApplyResult with disabled/failed/launcher/settings counts
       + a summary string.

Refactors
---------
- launcher::set_default_launcher gained an _impl(state, serial, package)
  helper so other commands (apply_snapshot) can call the multi-strategy
  promotion without the tauri::State<'_, T> lifetime constraint.

UI tweaks
---------
- App List: removed the "Default" column (it pre-selected the Optimize
  wizard's default in v1; v2 has no wizard yet, so the column was
  showing a flag for nothing). Now renders a RECOMMENDED tag next to
  app names where default_optimize is true, with a tooltip noting
  the future wizard.
- App List: .small-action.danger restyled — was outlined-only red on
  transparent, looked inconsistent next to the filled Disable button.
  Now uses the same dark fill, just a subtle red border, with a stronger
  red fill on hover.

HANDOFF doc
-----------
- v2/HANDOFF.md captures: uncommitted state, critical-path TODO with
  priorities (Frontend UI for new commands, app catalog completion,
  Optimize/Restore wizard, release pipeline, theme support, PIN
  pairing, restart-ADB, help screen, disable-stock-launchers, etc.),
  release pipeline plan (matrix builds via tauri-action,
  Flatpak considerations, code-signing decision points, auto-updater
  wiring), and key file pointers. Designed to survive a context clear
  so resumption is straightforward.

Verified: 56 tests pass, clippy -D warnings clean, svelte-check 0/0,
cargo build green. Frontend wiring for these new commands is the next
thing — see HANDOFF.md §2.
Header gets a Reboot dropdown (Normal/Recovery/Bootloader) and a
Disconnect button. The Overview tab grows an Emergency Recovery
section that calls panic_recovery and surfaces the restored/failed
counts. The Snapshot tab now has a real Apply button below the
preview that calls apply_snapshot and renders the ApplyResult
summary — the "preview only" disclaimer is gone.

New Tweaks tab between App List and Install APK: HDMI-CEC quad,
match_content_frame_rate radio, long-press-timeout buttons, an
animation-scale row that writes all three keys at once, and a
Display Scaling section (Shield 4K / 1080p / Reset). Each control
calls write_setting or set_display_scaling.

Play Store launch: new open_play_store command (am start with a
market://details URL, package name validated to alnum+._) so apps
with default_restore can be reinstalled when uninstall removed
them. Default_restore rows now show a Play Store button; missing
rows show Reinstall (system) + Play Store side-by-side.

Also added a legend above the App List explaining what the
RECOMMENDED tag means — the tooltip-only version wasn't
discoverable.
The App List was showing every app as Enabled and dumping a wall of
buttons; the RECOMMENDED tag explained nothing. Restructured:

  App | State | Risk | Recommended | Links

State is now real — package_states queries the device with `pm list
packages` + `pm list packages -d` in parallel and reports each row
as Enabled / Disabled / Missing.

Recommended is one action per row, derived from default_optimize
and method against the live state. If the device is already where
Optimize would put it, the cell shows "✓ Already disabled" instead
of a button. The subtle Enable button shows up when state diverges
so a single click can reverse course.

Links column always offers Play Store for anything with
default_restore (or anything that's missing), so re-installing an
app you previously uninstalled is one click.

Backend: new package_states command + PackageState enum.
The headline missing v1 feature is now wired end to end. New engine
module engine::optimize computes a plan per app (Disable / Uninstall /
Enable / Skip with reason) given the installed/disabled sets and the
memory map from dumpsys meminfo. compute_plan is pure logic with
unit tests for every branch.

Host side: commands/optimize.rs ships prepare_optimize (parallel
pm list packages, pm list packages -d, dumpsys meminfo) and
apply_performance_settings (animation triple at 0.5 for Optimize,
1.0 for Restore — matches v1's Perf step).

UI: new Optimize tab on the device page. Toggle Optimize/Restore,
review every row with its memory annotation and risk tag, untick
any item you want to skip, then Run. Per-row progress (pending /
done / skipped / failed), abort button, summary at the end, and a
post-run button for the animation-scale step. Failed rows surface
the ADB error in a tooltip.

App catalog: ported every entry v1 had that v2 was missing.
Common: +22 streaming apps, Chromecast Built-in, Google TTS, Play
Games, Setup Wraith. Shield: +Google Speech Services, Plex Media
Server, Stock Launcher. GoogleTV: +Droidlogic Launcher Provider,
Google TV Home. Total 67 entries now (v1 ~68 — within rounding).
Everything FEATURES.md says v1 does, v2 now does. Row-by-row
status now lives in HANDOFF.md §2A.

Connection layer:
- pair_device — Android 11+ adb pair + auto-connect on :5555.
  6-digit PIN validation. UI: Pair PIN button on home page that
  reveals an IP + PIN form.
- restart_adb — kill-server then start-server. UI: Restart ADB
  button on home toolbar.
- UNAUTHORIZED guidance — inline numbered steps under any device
  in unauthorized state instead of a bare red tag.

Main-menu actions:
- report_all — runs health_report against every authorized device
  and returns one summary per device. UI: Report All button on
  home toolbar opens a collapsible panel with temp / RAM / storage
  / display / audio for each.

Launcher tab:
- list_home_handlers — queries cmd package query-activities for
  every HOME-capable package, tags safe fallbacks (Settings
  packages), reports current enable state.
- disable_stock_launchers — pm disable-user --user 0 per package,
  refuses to touch safe fallbacks.
- restore_stock_launchers — pm enable per package.
- New "Disable stock launchers" wizard at the bottom of the
  Launcher tab with per-handler checkboxes and Restore All.

Health Report:
- Audio output device parsed from dumpsys audio (`HDMI`,
  `BUILTIN_SPEAKER`, etc.) and surfaced in the Display & Audio
  block. Mirrors v1's Get-DisplayMode audio probe.

Apps layer:
- decode_uninstall_error — maps the known pm uninstall failure
  patterns (Broken pipe, not installed for, DELETE_FAILED_*) to
  human-readable hints, auto-appended to the uninstall_package
  message so the App List shows actionable text instead of
  "Failure [DELETE_FAILED_INTERNAL_ERROR]".

Theme:
- Light theme via prefers-color-scheme — variableized the layout
  colors so the chrome (header, footer, page bg, buttons, inputs)
  adapts to OS light mode. Deeper component cards stay dark; a
  full light pass would be its own effort.

Tests:
- 65 Rust tests pass (4 new for engine::optimize, 3 for
  decode_uninstall_error, 2 for parse_active_audio_device).
auto-discovery, snapshots page

App List:
- Smaller app name (was too dominant)
- State badge now vertically centered with the row
- "Recommended" cell uses nowrap so the action button doesn't wrap
  beside its ✓ label
- "Links" stays in its own cell — no more Play Store stacked under
  Recommended

Launcher tab:
- Per-launcher buttons: Install (Play Store) when MISSING, Enable
  when disabled, Set as default when not active + enabled, Disable
  as a subtle option when enabled-but-not-current. Replaces the
  single Set-as-default-only model.

Memory-user safety (Health tab):
- Top memory table grew a Risk column. We look the package up in
  the curated catalog and tag UNKNOWN if it's not there.
- Disable now goes through safeDisableFromMemory which raises a
  rich confirm with the risk tier, optimize description, and a
  loud warning for HIGH / ADVANCED / UNKNOWN packages.

Tweaks tab:
- New get_display_scaling command shows the device's current `wm
  size` + `wm density` before you pick a preset, mirroring v1's
  Set-DisplayScaling flow.
- Display scaling buttons converted to a card-style grid — each
  preset shows its target resolution and density beneath the
  label, no longer wraps the row.

Install APK:
- New list_apks_in_folder backend command + auto-scan of the last
  folder the user picked from. The tab now shows every APK in
  that folder with size + per-row Install. Mirrors v1's ./apks/
  discovery without forcing a specific path.
- Added "Choose folder…" alongside the existing single-file
  picker.

Snapshots page (was empty/useless):
- Lists every snapshot with device name, type, serial, timestamp,
  package + setting + launcher counts.
- Per-row "Apply to device…" dropdown that offers Preview or
  Apply against any authorized device. This is the cross-device
  clone path users were asking about.
- Delete button per row, Open Folder action so users can
  copy/share the JSON manually.
- Listing now carries device_serial / device_type / settings_count
  / launcher; snapshot save returns the same enriched shape.

Other:
- Disconnect button is now small/subtle in the device header with
  a clarifying tooltip — primary actions get the visual weight.
- Catalog preloads on Health tab so the memory-user risk lookup
  has something to consult immediately.
- :global(.risk-unknown) styling for the new badge state.

Backend:
- snapshot::delete_snapshot (path-confined to snapshot_dir)
- snapshot::snapshot_dir_path
- tuning::get_display_scaling
- sideload::list_apks_in_folder
Engine: new engine::safety module with two tiers.

NEVER_DISABLE (host-layer refusal — pm command is never sent):
- android, com.android.systemui, com.android.shell, com.android.settings,
  com.android.tv.settings (emergency HOME fallback), the settings provider,
  the package installers (both AOSP and Google variants), permission
  controllers, external storage provider, media + downloads providers,
  bluetooth + bluetoothmidi + input devices, keychain + certinstaller,
  Google base layer (gms / gsf / gsf.login / ext.services), location.fused,
  calendar + contacts providers. Each entry carries the reason that gets
  surfaced to the user.

CAUTION (recoverable, but the UI shouts):
- com.android.providers.tv (Watch Next channels), com.google.android.tts,
  com.google.android.katniss (Assistant), com.google.android.apps.mediashell
  (Chromecast Built-in), com.android.vending (Play Store),
  com.google.android.feedback, com.android.printspooler.

Defense in depth:
- disable_package and uninstall_package both refuse outright if the target
  is NEVER_DISABLE; the user can't override from the UI.
- apply_snapshot refuses the same set, categorizing each refusal as a
  failed entry so the summary surfaces it.
- disable_stock_launchers extends its SAFE_FALLBACKS check with the broader
  NEVER_DISABLE list, and list_home_handlers reports `safe_fallback=true`
  for either case so the wizard's checkbox is disabled.
- New safety_info command returns the classification for any package; the
  Health tab's memory table calls it for every visible row.

Health tab UI:
- Memory table grew safety semantics: NEVER_DISABLE shows a "SYSTEM" badge
  + "Protected" label in place of the Disable button. CAUTION shows a
  yellow CAUTION badge + danger-styled Disable that opens a richer
  confirm with the engine's reason text. Plain SAFE behaves as before.
- safeDisableFromMemory now blocks NEVER_DISABLE with an alert that
  surfaces the engine's reason rather than letting the user click through
  to a generic "Refusing to disable" failure.

Launcher v1 parity fix (caught mid-stall by code-review agent):
- list_home_handlers was using `cmd package query-activities --components`
  and splitting on `/` to extract the package. v1's Get-HomeHandlers uses
  the non-components form and parses `packageName=<pkg>` rows. Output
  format differs on some Android versions, and the slash-split was too
  permissive (would have matched anything containing /).
- Switched to the no-components invocation + strict packageName= regex via
  std::sync::LazyLock. Two new parse tests, including one that confirms
  unrelated lines with slashes aren't mistakenly picked up.

73 Rust tests pass (6 new for engine::safety, 2 for parse_home_handler_packages).
Tiebreak by display name so refresh doesn't shuffle the order. Uses a
$derived from the existing `devices` array so device updates flow
through automatically.
Triggers on \`v2-*\` tags (or manual workflow_dispatch with an explicit
tag input for testing). Build matrix:
- ubuntu-22.04 → .deb / .AppImage / .rpm. webkit2gtk-4.1 + appindicator +
  rsvg2 + patchelf installed via apt before the bundler runs.
- macos-latest → universal .dmg + .app.tar.gz. rustup installs both
  aarch64- and x86_64-apple-darwin targets so \`--target
  universal-apple-darwin\` succeeds.
- windows-latest → .msi + .exe.

tauri-action@v0 handles \`cargo tauri build\`, collects the bundle
outputs, creates the GitHub Release, and uploads every artifact. RC
and beta tags (\`v2-0.2.0-rc1\`) get \`prerelease: true\` automatically.

Release body documents the first-run dismissal steps for Gatekeeper and
SmartScreen so people know what to expect from an unsigned build.

Signing intentionally skipped — there are \`SIGNING(macOS)\` and
\`SIGNING(Windows)\` markers at the top of the file pointing at the
secrets that need to land if we change our minds later. Auto-updater
also skipped for now (\`includeUpdaterJson: false\`); separate workflow
bolt-on when we want it.
Release workflow:
- New `notes` job runs once on a Linux runner and shares the release
  body + tag + prerelease flag with every per-OS bundler via job
  outputs. Eliminates the templated-body limitation of the previous
  approach.
- Prerelease detection broadened to a single regex matching
  `-(alpha|beta|rc|preview|pre)([.-]?[0-9]+)?$`. So all of these become
  GitHub prereleases automatically:
    v2-0.1.0-beta
    v2-0.1.0-beta.1
    v2-0.1.0-rc1
    v2-0.2.0-alpha
    v2-0.3.0-preview
    v2-1.0.0-pre
- Release body resolution order:
    1. If v2/CHANGELOG.md has a section matching `## v2-X.Y.Z`, the awk
       block extracts it verbatim. Authoring surface for rich
       hand-written notes, mirroring how v1 has been releasing.
    2. Otherwise auto-generate from `git log <prev-v2-tag>..<this-tag>`
       scoped to v2/ + the v2 workflow file, falling back to the full
       log if that range is empty.
  Either way we append the unsigned-build first-run dismissal block.
- fetch-depth: 0 on checkout so `git log` and `git tag --list` see
  history.

v2/CHANGELOG.md:
- New file. Scaffolded with a v2-0.1.0-beta section explaining the
  feature parity with v1 + known limitations of the unsigned beta.
  Future releases add a section above the existing entries.

v2/release.sh:
- Mirrors v1's release.sh ergonomics for the v2 track.
- Bumps the version atomically across tauri.conf.json, Cargo.toml,
  and package.json (Python-driven so JSON formatting + Cargo.toml
  shape are preserved).
- Flags: --patch (default) / --minor / --major for semver bumps,
  --beta / --rc / --alpha / --preview for pre-release suffixes,
  --set X.Y.Z for an exact override.
- Combinable: `release.sh --minor --beta` goes from 0.1.5 to
  0.2.0-beta. A repeated `release.sh --beta` increments the trailing
  number (-beta -> -beta.2).
- Commits + tags as `v2-VERSION` and offers to push. Pushing the tag
  fires the workflow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant