diff --git a/CHANGELOG.md b/CHANGELOG.md index 9945332..8e68914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Uninstall ws-scrcpy-web from inside the app on Windows.** The Settings → App **uninstall** action — previously Linux-only (on Windows you had to use Add/Remove Programs) — now works on Windows too: it runs the Velopack uninstaller (`Update.exe --uninstall`, which removes the install and stops/removes any installed service and the tray), with the same **keep my settings & logs** option (checked by default; unchecking also deletes config, logs, and dependencies). + +### Changed + +- **Reordered the Settings → App section and moved the uninstall confirmation to an overlay modal.** The rows are now, top to bottom: reset prompts → install for all users (Linux only) → stop server & exit → uninstall, on both Windows and Linux. The uninstall confirmation is now a top-layer modal (instead of an inline panel) with a **keep my settings & logs** checkbox that defaults to checked, a white **cancel** and a red **uninstall** button. + +### Fixed + +- **"Stop server & exit" now fully cleans up on Windows.** It previously left the tray (`ws-scrcpy-web-tray.exe`) resident and only ran `adb kill-server`, which can leave stray `adb.exe` processes behind. The tray-supervisor poll thread is now stopped before the tray is reaped (so it can't respawn the tray that was just killed), and the shutdown also runs `taskkill /F /IM adb.exe /T` to catch any stray adb — the same belt-and-braces the in-app update path already uses. + ## [0.1.30-beta.50] - 2026-06-08 ### Changed diff --git a/docs/plans/2026-06-08-app-section-redesign-windows-uninstall.md b/docs/plans/2026-06-08-app-section-redesign-windows-uninstall.md new file mode 100644 index 0000000..2c8af5c --- /dev/null +++ b/docs/plans/2026-06-08-app-section-redesign-windows-uninstall.md @@ -0,0 +1,249 @@ +# App-section redesign + in-app Windows uninstall — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Reorder Settings → App, move the uninstall confirm to an overlay modal, add an in-app Windows uninstall (parity with Linux), and fix the Windows tray reap on "stop server & exit". + +**Architecture:** Frontend (TS, `SettingsModal.ts` + a new modal) is one layer; backend (`ServiceApi.ts` win32 branch + a new Rust `windows_app_uninstall.rs` helper) is the other; the tray-reap fix is launcher-only. The contract between them is unchanged: the uninstall button POSTs `/api/service/uninstall-app { keep }` on both OSes; the server branches by platform. Mirror the existing Linux uninstall throughout (`linux_app_uninstall.rs`, `handleAppUninstall`). + +**Tech Stack:** TypeScript (vitest, jsdom), Rust (launcher; `cargo`/`cross`), no new deps. + +**Spec:** `docs/specs/2026-06-08-app-section-redesign-windows-uninstall-design.md`. + +**Parallelization:** Phases A/B (frontend) and C (backend) are independent after this plan; D (tray-reap) is independent of all. Good `/build-with-agent-team` split: one agent on A+B, one on C, one on D. + +--- + +## File Structure + +| File | Responsibility | Change | +|---|---|---| +| `src/app/client/SettingsModal.ts` | App-section assembly + pure state helper + `buildUninstallControl` | modify: reorder appends; `appSectionButtonsState.showUninstall` → both OS; rewire uninstall button to open the modal | +| `src/app/client/UninstallConfirmModal.ts` | the overlay uninstall modal (checkbox + cancel/uninstall) | **create** | +| `src/app/client/__tests__/UninstallConfirmModal.test.ts` | modal unit tests | **create** | +| `src/app/client/__tests__/SettingsModal.test.ts` | state + ordering + rewire tests | modify | +| `src/style/modal.css` | red-outline "uninstall" button style | modify | +| `src/server/api/ServiceApi.ts` | `handleAppUninstall` win32 branch | modify (~L980–1060) | +| `src/server/__tests__/ServiceApi.test.ts` | win32-pinned branch test | modify | +| `launcher/src/windows_app_uninstall.rs` | pure command builder + parse_args + dispatch + run | **create** | +| `launcher/src/main.rs` | dispatch `--windows-app-uninstall` | modify (cfg(windows) flag dispatch) | +| `launcher/src/lib.rs` (or `main.rs` mod block) | declare `mod windows_app_uninstall` (cfg windows) | modify | +| `launcher/src/tray_supervisor.rs` + `supervisor.rs` + `main.rs` | stop the poll thread before the reap | modify | +| `docs/smoke-tests/smoke-full.md` + `smoke-checklist.md` | Windows uninstall + tray-reap rows | modify | + +--- + +## Phase A — Frontend: reorder + uninstall-on-both-OS + +### Task A1: `appSectionButtonsState` reveals uninstall on Windows too + +**Files:** Modify `src/app/client/SettingsModal.ts:172-189`; Test `src/app/client/__tests__/SettingsModal.test.ts`. + +- [ ] **Step 1 — failing test.** In `SettingsModal.test.ts`, in the `appSectionButtonsState` describe block, add: + +```ts +it('shows uninstall on win32 (parity with linux), hides install-all-users', () => { + const s = appSectionButtonsState({ platform: 'win32', machineWideInstalled: false }); + expect(s.showUninstall).toBe(true); // NEW: win32 gets the uninstall row + expect(s.showInstallAllUsers).toBe(false); // install-all-users stays linux-only +}); +it('shows both rows on linux', () => { + const s = appSectionButtonsState({ platform: 'linux', machineWideInstalled: false }); + expect(s.showUninstall).toBe(true); + expect(s.showInstallAllUsers).toBe(true); +}); +``` + +- [ ] **Step 2 — run, expect FAIL** (`showUninstall` is `false` on win32 today). `npx vitest run src/app/client/__tests__/SettingsModal.test.ts -t appSectionButtonsState` +- [ ] **Step 3 — implement.** In `appSectionButtonsState`, compute `const showUninstall = linux || resp.platform === 'win32';` and return `showUninstall` (replace the `showUninstall: linux`). Update the doc-comment ("two Linux-only rows" → "install-all-users is Linux-only; uninstall shows on Linux + Windows"). +- [ ] **Step 4 — run, expect PASS.** Same command. +- [ ] **Step 5 — commit.** `git add -A && git commit -m "feat(client): reveal App-section uninstall row on Windows"` + +### Task A2: Reorder the App-section rows + +**Files:** Modify `src/app/client/SettingsModal.ts:buildAppSection` (~1595-1702). + +- [ ] **Step 1 — reorder the appends.** The desired DOM order (top→bottom) is: reset → install-all-users (Linux) → stop-server → uninstall. Move the `body.appendChild(...)` calls into that order. Keep each control's construction; only the append sequence changes. Specifically: build `reset` (+ its inline confirm panel) first and append; then `install` row + note; then `stop` row + note; then `uninstall` row. The reset confirm panel stays inline (only the *uninstall* confirm becomes a modal — Phase B). Result, in append order: + 1. reset row, reset confirm panel + 2. install-all-users row + note (hidden until Linux) + 3. stop-server row, stop note + 4. uninstall row (hidden until A1 reveals it) +- [ ] **Step 2 — verify ordering test.** Add a test asserting the rendered App-section row labels appear in order. In `SettingsModal.test.ts`, render the section (the test file already constructs a `SettingsModal`; follow its existing pattern for building the App section), collect `.settings-row` label texts, and assert `['reset welcome and bookmark prompts', 'install for all users', 'stop the server and close the app', 'uninstall ws-scrcpy-web']` is a subsequence. Run it; expect PASS after Step 1 (write the test first if the existing harness supports it — if rendering the full section in jsdom is heavy, assert order by reading `body.children` label cells). +- [ ] **Step 3 — run vitest** for the file; expect PASS. +- [ ] **Step 4 — commit.** `git commit -am "feat(client): reorder App-section (reset, install, stop, uninstall)"` + +--- + +## Phase B — Frontend: uninstall confirm modal + +### Task B1: `UninstallConfirmModal` (top-layer dialog + checkbox + cancel/uninstall) + +**Files:** Create `src/app/client/UninstallConfirmModal.ts`; Create `src/app/client/__tests__/UninstallConfirmModal.test.ts`. **Pattern to mirror:** `src/app/client/ConfirmModal.ts` (static `confirm(): Promise<...>`, extends `Modal`, `showModal()` top layer) and `AdminConfirmModal.ts`. **Test pattern:** `src/app/client/__tests__/AdminConfirmModal.test.ts` (stubs `HTMLDialogElement.showModal`). + +Contract: +```ts +// Resolves on cancel → { confirmed: false }; on uninstall → { confirmed: true, keep: }. +UninstallConfirmModal.confirm(): Promise<{ confirmed: boolean; keep: boolean }> +``` + +- [ ] **Step 1 — failing tests** (`UninstallConfirmModal.test.ts`), mirroring `AdminConfirmModal.test.ts`'s `showModal` stub: + +```ts +it('defaults keep checkbox to checked', async () => { + const p = UninstallConfirmModal.confirm(); + const box = document.querySelector('input[type=checkbox]') as HTMLInputElement; + expect(box.checked).toBe(true); // SAFETY DEFAULT + (document.querySelector('.uninstall-cancel') as HTMLButtonElement).click(); + expect(await p).toEqual({ confirmed: false, keep: true }); +}); +it('resolves confirmed + keep=false when unchecked then uninstall clicked', async () => { + const p = UninstallConfirmModal.confirm(); + (document.querySelector('input[type=checkbox]') as HTMLInputElement).checked = false; + (document.querySelector('.uninstall-confirm') as HTMLButtonElement).click(); + expect(await p).toEqual({ confirmed: true, keep: false }); +}); +it('renders the body copy and red uninstall button', () => { + void UninstallConfirmModal.confirm(); + expect(document.body.textContent).toContain('this removes the app, its dependencies, and any installed service.'); + expect(document.querySelector('.uninstall-confirm')?.className).toContain('settings-btn-danger-outline'); +}); +``` + +- [ ] **Step 2 — run, expect FAIL** (module missing). `npx vitest run src/app/client/__tests__/UninstallConfirmModal.test.ts` +- [ ] **Step 3 — implement** `UninstallConfirmModal.ts`: a `Modal` subclass building title `uninstall ws-scrcpy-web`, body `

this removes the app, its dependencies, and any installed service.

`, a `