diff --git a/PAWNIO_TEMP_PLAN.md b/PAWNIO_TEMP_PLAN.md
deleted file mode 100644
index 06b4c1e..0000000
--- a/PAWNIO_TEMP_PLAN.md
+++ /dev/null
@@ -1,521 +0,0 @@
-# PawnIO CPU Temperature Implementation Plan
-
-## Problem
-
-`pkg/sysinfo/sysinfo.go:110` reads CPU temperature via `gopsutil/v3/host.SensorsTemperatures()`, which queries WMI class `MSAcpi_ThermalZoneTemperature` in namespace `root/wmi`. This reports an ACPI **thermal zone** sensor (on the motherboard, near the CPU socket) — not CPU die temperature.
-
-Result: 17°C (room temperature) instead of the real 38-45°C reported by HWiNFO. HWiNFO reads CPU MSR registers (`IA32_THERM_STATUS`) directly via a kernel driver.
-
-## Chosen Approach: PawnIO, install-if-absent, no cleanup
-
-PawnIO is a modern, signed kernel driver by namazso. It's what LibreHardwareMonitor (LHM) / FanControl use internally.
-
-Requirements:
-- App setting `enable_cpu_temp` (default `false`) gates the feature — opt-in.
-- Setting takes effect on next app start (driver loads at `OnStartup`).
-- On startup with the setting on: check if PawnIO is installed; if not, install it. **No uninstall, no RunOnce, no marker file.** PawnIO stays once installed — matches FanControl/LHM behavior, eliminates all cleanup edge cases.
-- Support Intel (IntelMSR) and AMD (RyzenSMU). Other vendors → no temp, silent.
-- Optional refresh interval setting (default 5s; 1/5/10/30/60 options).
-- Temp is a separate Wails method, not embedded in `ResolvedHardware()`. Polling at 1s would otherwise re-run every WMI class query (CPU, GPU, memory, motherboard, NIC, storage) each tick — heavy and pointless. `SysInfo.CPUTemperature()` does only the IOCTL read.
-
-Elevation is a non-issue: install-it runs `requireAdministrator` (`build/windows/wails.exe.manifest:20`), so PawnIO install (self-elevates anyway), device open (ACL is SY+BA), and IOCTLs (FILE_ANY_ACCESS) all work with no extra handling.
-
-### Architecture
-
-```
-App startup (main.go OnStartup):
- ├─ Read AppSetting.enable_cpu_temp
- ├─ If false → skip everything (no driver, no temp)
- └─ If true:
- └─ go cputemp.Init(dirRoot) // NON-BLOCKING — see "Async Init" below
- ├─ Check HKLM\...\Uninstall\PawnIO registry key
- ├─ If absent: run PawnIO_setup.exe -install -silent (~2-3s)
- │ (idempotent; if it fails, log + return error, IsAvailable()=false)
- ├─ Open \\.\GLOBALROOT\Device\PawnIO
- ├─ Detect CPU vendor (WMI Win32_Processor.Manufacturer)
- └─ Load IntelMSR.bin or RyzenSMU.bin via IOCTL
- On any error: log to stderr, IsAvailable()=false, no temp
- Window appears immediately; temp badge appears once Init completes.
-
-Frontend (index.vue):
- ├─ onBeforeMount: utils.getHardware() once → hwinfos
- ├─ If settings.enable_cpu_temp:
- │ ├─ setTimeout-recursive poll of SysInfo.CPUTemperature()
- │ │ (interval read live from settings each tick — no restart for interval change)
- │ └─ Update cpuTemp ref; render as badge next to "CPU" heading
- └─ onBeforeUnmount: clear pending timeout
-
-SysInfo.ResolvedHardware():
- └─ No longer reads temp. Returns CPU/GPU/mem/mobo/NIC/storage names only.
-
-SysInfo.CPUTemperature() (new):
- └─ If cputemp.IsAvailable(): read max temp via IOCTL, return float64
- Else: return -1 — frontend hides badge
-```
-
-### Key design decisions
-
-| Decision | Rationale |
-|---|---|
-| **No cleanup, no RunOnce** | FanControl/LHM leave PawnIO installed; we do too. Eliminates all RunOnce edge cases. User opted in, so leaving a signed driver is acceptable. |
-| **Install-if-absent only** | Single registry check. We don't track ownership — never uninstall, so ownership is irrelevant. |
-| **Separate `CPUTemperature()` method** | Polling must not re-run all WMI queries every tick. IOCTL read is microseconds; WMI is 50-200ms × 5 classes. |
-| **Setting defaults to `false`** | Opt-in. Kernel driver install is a side effect the user should explicitly allow. |
-| **Setting effective on next start** | Driver load happens in `OnStartup`. Live enable/disable would need load/unload logic — YAGNI. UI shows restart hint. |
-| **Refresh interval live** | Interval read from settings each tick via setTimeout recursion (ref lookup, free). No restart needed to change interval — only enable/disable requires restart. |
-| **Async Init** | `Init()` runs in a goroutine from `OnStartup`; window appears immediately. `IsAvailable()` flips true when ready; frontend polls `CPUTemperature()` which returns -1 until then. Avoids 2-3s frozen launch for an opt-in feature. |
-| **Error badge hides** | On read error mid-session: `cpuTemp = null`, badge vanishes, retry next tick. No stale freeze at last good value. |
-| **No `//go:embed`** | Follow `internals/data/` pattern (like `pci.ids.gz`). |
-| **Direct IOCTL, no DLL** | Pure Go via `golang.org/x/sys/windows`, no CGo, no .NET. |
-| **Files are `.bin` not `.amx`** | Renamed in latest PawnIO.Modules release. |
-| **No `Close()`** | YAGNI — process exit releases the handle. No Wails `OnShutdown` needed. |
-| **Intel + AMD only; others silent** | PawnIO.Modules ships IntelMSR.bin + RyzenSMU.bin. Unknown vendor (ARM, etc.) → `IsAvailable()` true but no module loaded → `CPUTemperature()` returns -1. No speculative vendor code. |
-
-## File Changes
-
-### New files
-
-| File | Purpose |
-|---|---|
-| `pkg/cputemp/cputemp.go` | Public API: `Init()`, `GetCPUTemperatures()`, `IsAvailable()` |
-| `pkg/cputemp/driver.go` | PawnIO lifecycle: registry check, install, open device |
-| `pkg/cputemp/ioctl.go` | IOCTL constants, buffer structs, `executeFn()` helper |
-| `pkg/cputemp/msr.go` | Module loading, MSR/SMU reading, Intel/AMD temp decode |
-| `pkg/cputemp/vendor.go` | CPU vendor detection via WMI (own minimal query) |
-
-### Modified files
-
-| File | Change |
-|---|---|
-| `pkg/storage/app_setting.go` | Add `EnableCPUTemp bool` + `CPUTempRefreshInterval int` |
-| `main.go` | Store `appSettings` in package var; call `cputemp.Init(dirRoot)` in goroutine from `OnStartup` if enabled |
-| `pkg/sysinfo/sysinfo.go` | Remove `gopsutil/v3/host` temp block; add `CPUTemperature()` method |
-| `frontend/src/pages/index.vue` | Poll `CPUTemperature()`; render temp badge; clear timeout on unmount |
-| `frontend/src/i18n/en.json` | Add `settingCPUTemp`, `settingEnableCPUTemp`, `settingCPUTempRestartHint`, `settingCPUTempRefreshInterval` |
-| `frontend/src/i18n/zh_Hant_HK.json` | Add Chinese equivalents |
-| `frontend/src/pages/settings.vue` | Add checkbox + refresh interval select in Display/Hardware Info section |
-| `.github/workflows/build_and_release.yml` | Add PawnIO download steps |
-
-### Runtime files (not committed)
-
-```
-internals/data/
-├── PawnIO_setup.exe # ~3.4 MB, downloaded by CI
-├── IntelMSR.bin # ~5.3 KB, downloaded by CI
-└── RyzenSMU.bin # ~39.7 KB, downloaded by CI
-```
-
-## Package API: `pkg/cputemp/`
-
-```go
-type CPUTemp struct {
- Name string // "CPU Package", "CPU Tctl"
- Value float64 // °C
-}
-
-// Init installs PawnIO if absent and opens the driver device.
-// If PawnIO is already installed (by anyone), skips install.
-// Never uninstalls. No RunOnce, no marker file.
-func Init(exeDir string) error
-
-// GetCPUTemperatures reads current CPU temperature(s).
-func GetCPUTemperatures() ([]CPUTemp, error)
-
-// IsAvailable returns true if driver is open and a module is loaded.
-func IsAvailable() bool
-```
-
-No `Close()` — process exit releases the handle.
-
-## IOCTL Constants
-
-```go
-const (
- IOCTL_PIO_LOAD_BINARY = 0x9C402084 // CTL_CODE(41394, 0x821, METHOD_BUFFERED, FILE_ANY_ACCESS)
- IOCTL_PIO_EXECUTE_FN = 0x9C402104 // CTL_CODE(41394, 0x841, METHOD_BUFFERED, FILE_ANY_ACCESS)
-)
-
-// Execute input: [32 bytes function name] + [8 bytes params...]
-func executeFn(handle windows.Handle, fnName string, params ...uint64) (uint64, error) {
- bufSize := 32 + len(params)*8
- buf := make([]byte, bufSize)
- copy(buf[:32], fnName)
- for i, p := range params {
- binary.LittleEndian.PutUint64(buf[32+i*8:], p)
- }
- out := make([]byte, 8)
- var returned uint32
- err := windows.DeviceIoControl(handle, IOCTL_PIO_EXECUTE_FN,
- &buf[0], uint32(bufSize),
- &out[0], 8,
- &returned, nil)
- return binary.LittleEndian.Uint64(out), err
-}
-```
-
-## Driver Lifecycle: `Init(exeDir)`
-
-```
-1. Resolve paths:
- installer = exeDir + "\internals\data\PawnIO_setup.exe"
- intelMSR = exeDir + "\internals\data\IntelMSR.bin"
- ryzenSMU = exeDir + "\internals\data\RyzenSMU.bin"
-
-2. Check if PawnIO installed:
- key = HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\PawnIO
- if key exists → skip install
- if absent → run: PawnIO_setup.exe -install -silent
- (if exitCode != 0 && != ERROR_SUCCESS_REBOOT_REQUIRED → return error)
-
-3. Open device:
- handle = CreateFile(`\\.\GLOBALROOT\Device\PawnIO`, GENERIC_READ|WRITE, ...)
- if fails → return error
-
-4. Detect CPU vendor:
- cputemp writes its own minimal WMI query (cannot use sysinfo.queryWMI — unexported).
- Use github.com/yusufpapurcu/wmi directly with a minimal struct:
- type cpuVendor struct{ Manufacturer string }
- var cpus []cpuVendor
- wmi.Query("SELECT Manufacturer FROM Win32_Processor", &cpus)
- // cpus[0].Manufacturer == "GenuineIntel" or "AuthenticAMD"
-
-5. Load module:
- read IntelMSR.bin or RyzenSMU.bin
- IOCTL_PIO_LOAD_BINARY(handle, moduleBlob)
- Unknown vendor → no module, IsAvailable()=false for temp purposes.
-
-6. Store handle + vendor in package-level vars
-```
-
-No RunOnce. No marker file. Registry key presence is the only "is it installed?" check, used solely to skip a redundant install.
-
-## Temperature Reading
-
-### Intel path
-
-```
-1. Read TjMax: executeFn("ioctl_read_msr", 0x1A2)
- tjMax = float64((value >> 16) & 0xFF)
- if tjMax == 0 → tjMax = 100
-
-2. Read package temp: executeFn("ioctl_read_msr", 0x1B1)
- if (value & 0x80000000) == 0 → invalid, skip
- digitalReadout = float64((value >> 16) & 0x7F)
- temp = tjMax - digitalReadout
-
-3. Return [{Name: "CPU Package", Value: temp}]
-```
-
-### AMD path (RyzenSMU.bin)
-
-```
-1. executeFn("ioctl_read_smu_register", 0x59800)
- rawTemp = float64((value >> 21)) * 0.125
- if (value & 0x80000) != 0 || (value & 0x30000) == 0x30000:
- rawTemp -= 49.0
- tctl = rawTemp // this is Tctl, not Tdie
-
-2. Return [{Name: "CPU Tctl", Value: tctl}]
-```
-
-**Known limitation:** SMN 0x59800 returns Tctl, not Tdie. Tdie = Tctl - offset where offset varies by model (Zen 2: ~10-27°C, Zen 3: ~20°C, Zen 4: ~25°C). Without per-model detection, we report Tctl. Labeling as "Tctl" is honest about what we're showing.
-
-## AppSetting Changes
-
-### `pkg/storage/app_setting.go`
-
-```go
-type AppSetting struct {
- // ... existing fields ...
- EnableCPUTemp bool `json:"enable_cpu_temp"`
- CPUTempRefreshInterval int `json:"cpu_temp_refresh_interval"` // seconds
-}
-```
-
-Default in `All()` when file doesn't exist:
-```go
-s.setting = AppSetting{
- // ... existing defaults ...
- EnableCPUTemp: false,
- CPUTempRefreshInterval: 5,
-}
-```
-
-### `main.go` changes
-
-Add package-level var:
-```go
-var appSettings *storage.AppSettingStorage
-```
-
-In `main()`:
-```go
-appSettings = &storage.AppSettingStorage{Path: filepath.Join(dirConf, "setting.json")}
-// ...
-Bind: []interface{}{
- // ...
- appSettings, // use the var, not a new instance
- // ...
-},
-```
-
-In `OnStartup`:
-```go
-if settings, err := appSettings.All(); err == nil && settings.EnableCPUTemp {
- go func() {
- if err := cputemp.Init(dirRoot); err != nil {
- fmt.Fprintln(os.Stderr, "install-it: CPU temp unavailable:", err)
- }
- }()
-}
-```
-
-## sysinfo.go Integration
-
-### Remove (lines 108-123)
-
-The `host.SensorsTemperatures()` block and the `"github.com/shirou/gopsutil/v3/host"` import. `ResolvedHardware()` no longer embeds temp in CPU names.
-
-### Add new method
-
-```go
-// CPUTemperature returns the current CPU package temperature in °C via PawnIO,
-// or -1 if unavailable. Lightweight: IOCTL read only, no WMI queries.
-func (i SysInfo) CPUTemperature() (float64, error) {
- if !cputemp.IsAvailable() {
- return -1, nil
- }
- temps, err := cputemp.GetCPUTemperatures()
- if err != nil || len(temps) == 0 {
- return -1, err
- }
- var max float64
- for _, t := range temps {
- if t.Value > max {
- max = t.Value
- }
- }
- return max, nil
-}
-```
-
-`gopsutil/v3/host` import removed. `gopsutil/v3` stays (used by `pkg/execute/command.go`).
-
-## Frontend Changes
-
-### `frontend/src/pages/index.vue`
-
-Add polling + temp badge:
-
-```vue
-
-```
-
-Template — temp badge next to CPU heading:
-
-```vue
-
- {{ $t(hwKey(part)) }}
-
- {{ cpuTemp }}°C
-
-
-```
-
-After command completion (existing `@completed` handler), keep `utils.getHardware()` refresh — temp polling continues independently.
-
-### `frontend/src/i18n/en.json`
-
-Add:
-```json
-"settingCPUTemp": "CPU Temperature",
-"settingEnableCPUTemp": "Enable CPU Temperature Detection",
-"settingCPUTempRestartHint": "Takes effect on next app start",
-"settingCPUTempRefreshInterval": "Refresh Interval"
-```
-
-### `frontend/src/i18n/zh_Hant_HK.json`
-
-Add Chinese equivalents, e.g.:
-```json
-"settingCPUTemp": "CPU 溫度",
-"settingEnableCPUTemp": "啟用 CPU 溫度偵測",
-"settingCPUTempRestartHint": "下次啟動應用時生效",
-"settingCPUTempRefreshInterval": "更新間隔"
-```
-
-### `frontend/src/pages/settings.vue`
-
-Add in the **Display → Hardware Info** section (after the NIC filters, ~line 287), since this is a hardware-display setting:
-
-```vue
-
-
-
-
{{ $t('settingCPUTemp') }}
-
-
-
-
-
{{ $t('settingCPUTempRestartHint') }}
-
-
-
-
-
-
- {{ $t('labelSeconds') }}
-
-
-
-
-```
-
-The `settings` ref is bound to `AppSetting` via the store. Wails auto-generates `models.ts` on `wails dev`/`wails build`.
-
-## CI/CD Changes
-
-### `.github/workflows/build_and_release.yml`
-
-Add after "Download PCI ID database" step:
-
-```yaml
-- name: Download PawnIO driver and modules
- shell: pwsh
- run: |
- $expected = "1f519a22e47187f70a1379a48ca604981c4fcf694f4e65b734aaa74a9fba3032"
- curl -sL -o "build\bin\internals\data\PawnIO_setup.exe" "https://github.com/namazso/PawnIO.Setup/releases/download/2.2.0/PawnIO_setup.exe"
- $actual = (Get-FileHash "build\bin\internals\data\PawnIO_setup.exe" -Algorithm SHA256).Hash.ToLower()
- if ($actual -ne $expected) { throw "PawnIO_setup.exe SHA256 mismatch: expected $expected, got $actual" }
- curl -sL -o "build\bin\internals\data\pawnio_modules.zip" "https://github.com/namazso/PawnIO.Modules/releases/download/0.2.10/release_0_2_10.zip"
- $expectedModules = "971c7c974c538b62ac020e0442fa99d0423417bfb496dfe9a4a43ccc0abc0e63"
- $actualModules = (Get-FileHash "build\bin\internals\data\pawnio_modules.zip" -Algorithm SHA256).Hash.ToLower()
- if ($actualModules -ne $expectedModules) { throw "PawnIO modules SHA256 mismatch: expected $expectedModules, got $actualModules" }
- Expand-Archive -Path "build\bin\internals\data\pawnio_modules.zip" -DestinationPath "build\bin\internals\data\pawnio_modules" -Force
- Move-Item "build\bin\internals\data\pawnio_modules\IntelMSR.bin" "build\bin\internals\data\IntelMSR.bin"
- Move-Item "build\bin\internals\data\pawnio_modules\RyzenSMU.bin" "build\bin\internals\data\RyzenSMU.bin"
- Remove-Item "build\bin\internals\data\pawnio_modules.zip"
- Remove-Item -Recurse "build\bin\internals\data\pawnio_modules"
-```
-
-ZIP step already includes `internals\data` — no change needed.
-
-**Verify versions/SHAs at implementation time** — PawnIO.Setup and PawnIO.Modules may have released newer versions since this plan was written.
-
-**32-bit build:** PawnIO_setup.exe includes both x86 and x64 drivers in the self-extracting archive and installs the correct one for the OS architecture. Verified — PawnIO supports both.
-
-## Error Handling
-
-| Scenario | Behavior |
-|---|---|
-| `enable_cpu_temp = false` | Init never called. No temp. Badge hidden. |
-| PawnIO installer fails | Log to stderr. `IsAvailable()` false. `CPUTemperature()` returns -1. Badge hidden. |
-| Device open fails (reboot needed) | Log to stderr. `IsAvailable()` false. Badge hidden. |
-| Module load fails | Log to stderr. `GetCPUTemperatures()` errors. Badge hidden. |
-| MSR read fails (invalid bit) | Skip that reading. |
-| AMD SMU read fails | Return error. No temp that tick. |
-| Unknown CPU vendor | No module loaded. `IsAvailable()` false. Badge hidden. |
-| Read error mid-session | Badge hides (`cpuTemp = null`), next tick retries. |
-| All error paths | Silent degradation — badge hidden, no temp shown. |
-
-## Verification
-
-1. `go build ./...` compiles (all packages)
-2. `go vet ./pkg/cputemp/` passes
-3. `go vet ./pkg/sysinfo/` passes (after import removal + new method)
-4. `go vet ./pkg/storage/` passes (after AppSetting change)
-5. `cd frontend && npm run type-check` passes (after Wails regenerates models)
-6. Manual: with `enable_cpu_temp=true` + restart, CPU heading shows temp badge updating at the configured interval; interval changes apply live without restart
-7. Manual: with `enable_cpu_temp=false`, no badge, no PawnIO install attempt
-8. Review with @oracle for architecture sanity
-
-## Execution Order
-
-| Step | Depends on | Parallelizable |
-|---|---|---|
-| 1. Add `EnableCPUTemp` + `CPUTempRefreshInterval` to `AppSetting` | None | — |
-| 2. Create `pkg/cputemp/` package | None | ✅ with step 1 |
-| 3. Integrate into `main.go` + `sysinfo.go` (remove old temp block, add `CPUTemperature()`) | Steps 1, 2 | — |
-| 4. Add i18n keys + settings UI (checkbox + interval select) | Step 1 | ✅ with step 3 |
-| 5. Frontend polling + temp badge in `index.vue` | Step 3 (needs `CPUTemperature` binding) | ✅ with step 4 |
-| 6. Update CI/CD workflow | None | ✅ with steps 2-5 |
-| 7. Verify build + review | Steps 2-6 | — |
-
-## Asset URLs
-
-- PawnIO_setup.exe: `https://github.com/namazso/PawnIO.Setup/releases/download/2.2.0/PawnIO_setup.exe`
- - SHA256: `1f519a22e47187f70a1379a48ca604981c4fcf694f4e65b734aaa74a9fba3032`
- - Size: 3,410,960 bytes
-- PawnIO Modules: `https://github.com/namazso/PawnIO.Modules/releases/download/0.2.10/release_0_2_10.zip`
- - SHA256: `971c7c974c538b62ac020e0442fa99d0423417bfb496dfe9a4a43ccc0abc0e63`
- - Contains: `IntelMSR.bin` (5,324 bytes), `RyzenSMU.bin` (38,996 bytes), and other modules
-
-## Known Limitations
-
-1. **PawnIO persists after first enable.** Once install-it installs it, it stays until manually removed (via PawnIO_setup.exe -uninstall, or another tool). Acceptable for opt-in feature; matches FanControl/LHM behavior. Document in user-facing text if needed.
-
-2. **Enable/disable requires restart.** Driver loads at `OnStartup`; UI shows restart hint. Interval changes apply live (no restart). Live enable would need load/unload logic — YAGNI.
-
-3. **AMD Tdie vs Tctl.** SMN 0x59800 returns Tctl, not Tdie. Tdie = Tctl - offset where offset varies by CPU model. Without per-model detection, we report Tctl. Noticeably higher than HWiNFO "CPU Die" on Zen 2+. Labeling as "Tctl" is honest. Badge just shows the number; no label distinction in the compact UI.
-
-4. **No per-core temps (Intel).** Only package temp from MSR 0x1B1. Per-core requires MSR 0x19C per logical processor with `SetThreadAffinityMask`. Package temp is more useful for overall CPU health and fits the compact badge UI.
-
-5. **Other CPU vendors.** Only Intel + AMD have PawnIO modules. ARM/unknown → no temp, silent. No speculative support for hypothetical vendors.
\ No newline at end of file
diff --git a/V3_MIGRATION.md b/V3_MIGRATION.md
new file mode 100644
index 0000000..c581d7c
--- /dev/null
+++ b/V3_MIGRATION.md
@@ -0,0 +1,66 @@
+# Wails v2 → v3 Migration Estimate
+
+> Effort estimate and work breakdown for migrating install-it from Wails v2.12.0 to Wails v3. Produced from a full audit of the app's Wails surface area plus verification of v3 API facts against the `wailsapp/wails` master branch. Companion to [BACKEND_I18N_HANDOFF.md](BACKEND_I18N_HANDOFF.md).
+
+---
+
+## 1. Current Wails surface (audited)
+
+The business logic (`pkg/storage`, `matching`, `porter`, `update`, `sysinfo`, `cputemp`) is Wails-free. Everything the framework touches:
+
+| Surface | Where | Notes |
+|---|---|---|
+| Bootstrap | `main.go` | `wails.Run`: 9 bound structs, `EnumBind` ×5 enum arrays, `OnStartup` hook, `Windows.WebviewBrowserPath`, asset embed |
+| Go runtime calls | `app.go` | `OpenDirectoryDialog`, `OpenFileDialog`; direct `webviewloader.GetAvailableCoreWebView2BrowserVersionString` for WebView2 version display |
+| Go→JS events | `pkg/execute/execute.go` | `EventsEmit("execute:exited", id, CommandResult)` — **the only event in the app**; porter progress is polling via bound `Progress()`, not events |
+| Frontend bindings | ~20 files | imports from `@/wailsjs/go/...` and `@/wailsjs/go/models` |
+| Frontend runtime | 4 components | `EventsOn` (CommandStatusModal), `BrowserOpenURL` + `Environment` (app-info), `Quit` (UpdateModal), `WindowReloadApp` (ProgressModal) |
+| Test mocks | `frontend/src/__tests__/setup.ts` | mocks 8 wailsjs modules + runtime |
+| Build | `wails.json`, `.github/workflows/build_and_release.yml` | pinned CLI from go.mod, `-upx`, **386 + amd64 matrix**, fixed WebView2 runtime downloaded into `internals/bin/WebView2` (consumed via `WebviewBrowserPath`) |
+
+## 2. Verified v3 facts
+
+Checked against `wailsapp/wails@master` source and CI:
+
+- **Services replace Bind**: `application.New(application.Options{Services: []application.Service{application.NewService(...)}})`; procedural window creation.
+- **Bindings**: `wails3 generate bindings` → `frontend/bindings//`, static-analyzer based, calls via numeric IDs (`$Call.ByID(...)`), runtime imported from `/wails/runtime.js`.
+- **`WebviewBrowserPath` exists in v3** (`v3/pkg/application/application_options.go`) → fixed WebView2 runtime bundling carries over unchanged.
+- **SingleInstance** available (`SingleInstanceOptions`) — app doesn't use it today; note it interacts with the self-updater's detached relaunch if ever adopted.
+- **`EnumBind` is v2-only** — no v3 equivalent found. Replacement: hand-written TS constants module or small codegen script.
+- **ErrorFormatter → MarshalError**: per-service `MarshalError func(err error) []byte` in ServiceOptions. The errcode design from BACKEND_I18N_HANDOFF.md survives as-is; frontend reads structured payload from rejection `.cause`.
+- **⚠️ windows/386 absent from v3's cross-compile test matrix** (amd64/arm64 only, `.github/workflows/cross-compile-test-v3.yml`). Moot: x86 is being dropped at migration time (§3).
+
+## 3. Gate decisions (before any code)
+
+1. ~~x86 fate~~ **RESOLVED:** x86 support drops *together with* the v3 migration, not before. v2 keeps shipping x86 until then; removing the `386` leg from `.github/workflows/build_and_release.yml` (build + updater jobs) is part of task 10. The separate `install-it-updater` repo should drop its x86 target in the same release.
+2. **v3 stability freeze.** v3 is pre-stable with API churn. Don't start until a stable tag exists that you're willing to pin; starting earlier means paying churn twice.
+
+## 4. Work breakdown
+
+| # | Task | Est |
+|---|---|---|
+| 1 | Spike on branch: bootstrap rewrite — `application.New`, services registration, window creation, startup hooks replacing `OnStartup` | 0.5–1d |
+| 2 | Context injection refactor: `App.SetContext` / `CommandExecutor.SetContext` pattern → v3 service lifecycle | 2–4h |
+| 3 | Dialogs: `SelectFolder`/`SelectFile` → v3 dialog API | 1–2h |
+| 4 | Event rewire: Go emit (`execute:exited`) + JS subscribe via `@wailsio/runtime` | 1–2h |
+| 5 | Regenerate bindings; rewrite ~20 frontend import sites; verify call signatures | 0.5–1d |
+| 6 | Enums: TS constants module replacing EnumBind (5 enums, 27 values) | 2–4h |
+| 7 | Error seam: `MarshalError` wiring + `codeOf` reads `.cause` | ~2h |
+| 8 | Runtime calls: `BrowserOpenURL`, `Quit`, `WindowReloadApp`, `Environment` equivalents | 1–2h |
+| 9 | Vitest setup mocks rewrite to new binding modules | 2–4h |
+| 10 | Build system: Taskfile (wails3 init generates), `wails3 dev/build`, CI workflow updates; remove 386 leg from release matrix; UPX + WebView2 bundling steps mostly carry over | 0.5–1d |
+| 11 | Regression pass: full driver install / porter import-export / updater flows on real hardware | 0.5–1d |
+
+**Total: ~4–7 focused days (~1.5–2 calendar weeks with hardware-testing friction).**
+
+## 5. Risk ranking
+
+1. **v3 maturity / API churn** — rework risk scales with migration duration; freeze scope before starting.
+2. Everything else is mechanical mapping — no architectural risk. Storage/porter/update layers and the errcode error-i18n system are version-agnostic by design.
+
+## 6. What carries over unchanged
+
+- All `pkg/` business logic except one `EventsEmit` call in `execute.go`
+- SQLite schema/migrations, settings store, self-update rename trick (OS-level, not Wails)
+- `pkg/errcode` + i18n keys + composable structure (only transport seam changes)
+- WebView2 fixed-runtime bundling strategy (`WebviewBrowserPath` exists in v3)
diff --git a/instruction.md b/instruction.md
deleted file mode 100644
index 3c00595..0000000
--- a/instruction.md
+++ /dev/null
@@ -1,142 +0,0 @@
-# REFACTORING EXECUTION - COMPLETION SUMMARY
-
-## ✅ ALL PHASES COMPLETE (2026-05-26)
-
-The complete structural refactoring of install-it from JSON file storage to SQLite+GORM is now **production-ready**.
-
-### Phase Completion Status
-- ✅ **Phase 1**: Planning & Architectural Mapping - COMPLETE
-- ✅ **Phase 2**: Interface & Schema Isolation - COMPLETE
-- ✅ **Phase 3**: Core Implementation Block - COMPLETE
-- ✅ **Phase 4**: Quality Review & Safety Check - COMPLETE
-- ✅ **Phase 5**: Testing & Final Verification - **COMPLETE** (as of this session)
-
-### Key Accomplishments
-
-#### 1. **Native Go Update System** ✅
-- Implemented `TriggerNativeUpdate(downloadUrl)` in app.go (lines 110-172)
-- Windows file rename trick: exe → exe.old, exe.new → exe
-- Detached process spawn with clean exit
-- Boot cleanup of stale .exe.old files in main.go
-
-#### 2. **SQLite+GORM Storage** ✅
-- CGO-free sqlite driver: `glebarez/sqlite`
-- Schema migration system via `gormigrate/v2`
-- Tables: DriverGroup, Driver, RuleSet with proper relationships
-- JSON serialization for nested arrays (Flags, Incompatibles, Rules, DriverGroupIds)
-- Baseline migration ID: "2026052601_baseline" with legacy installation guard
-
-#### 3. **Simplified AppSetting Storage** ✅
-- Removed generic collection management from store.go
-- JSON-only persistence via FileStore
-- 12-field configuration with sensible defaults
-- Bound to Wails runtime for frontend access
-
-#### 4. **Test Suite** ✅
-- **56 tests passing** (0 failures)
-- FileStore, MemoryStore, AppSettingStorage tests
-- DriverGroupStorage integration tests with cascade delete verification
-- MatchRuleStorage tests
-- Fuzz testing for JSON roundtrips
-- Fixed database cleanup to prevent Windows file lock errors
-
-#### 5. **Frontend Integration** ✅
-- UpdateModal.vue triggers TriggerNativeUpdate with GitHub release URL
-- All 21 Nuxt UI components migrated and functional
-- Wails bindings correctly configured in main.go
-
----
-
-# Role & Context
-You are a highly coordinated elite team of AI software engineering agents consisting of an Orchestrator, Planner, Architect, Implementer, Reviewer, and Tester. Your mission is to execute a massive structural refactoring of "install-it", a Wails-based (Go + Vue 3) desktop utility application designed for system administrators.
-
-# Core Objectives
-
-1. **Decouple External Python Updater via Native Go:** The legacy Python updater engine (`main.py`) sits in a completely separate repository. Do not look for or attempt to delete physical Python scripts in this codebase. Instead, remove all internal command execution structures that invoke it. Replace it with a native Go-based update routine using the "Windows File Rename Trick", triggered directly from the existing Vue interface.
-2. **Relational Storage Evolution:** Convert the complex nested configuration storage layer (`drivers.json`, `rules.json`) from brittle flat JSON files into a robust embedded SQLite database using GORM and versioned schema management via `gormigrate/v2`.
-3. **Simplify Global Configs:** Retain the `setting.json` file for global app configurations, but strip out and simplify the generic JSON file storage engine abstractions since only `AppSetting` will use JSON moving forward.
-4. **Maintain Test Integrity:** Ensure all backend structural changes map cleanly to the existing Wails bindings and frontend components, updating or writing unit tests to validate the data layers and upgrade systems.
-
----
-
-# Agent Team Structure & Protocol
-You must execute this workspace refactoring by passing execution states sequentially across the following agent roles. Do not jump to implementation without architectural approval.
-
-### 1. 🧭 The Orchestrator Agent
-* **Responsibility:** Oversees the entire lifecycle. Validates that transitions between design, implementation, and testing are complete. Ensures context state maps cleanly across roles.
-
-### 2. 🗓️ The Planner Agent
-* **Responsibility:** Evaluates the existing workspace. Breaks down the implementation steps into micro-tasks across the file directory structure. Produces a checklist of file-by-file changes.
-
-### 3. 📐 The Architect Agent
-* **Responsibility:** Designs code structures, interface adjustments, database schemas, and data flow patterns. Ensures cross-compilation stability (CGO-free requirement). Approves patterns before code is touched.
-
-### 4. 💻 The Implementer Agent
-* **Responsibility:** Writes code files, alters existing structures, updates frontend-backend communication states, and cleanly deprecates external application process spawners.
-
-### 5. 🔍 The Reviewer Agent
-* **Responsibility:** Inspects the Implementer’s code for regressions, edge cases (such as Windows file system locks, race conditions, and error propagation boundaries), and architectural alignment.
-
-### 6. 🧪 The Tester Agent
-* **Responsibility:** Rewrites existing tests, generates mock datasets, implements verification routines for database migration loops, and tests the mock binary file swap mechanism.
-
----
-
-# Detailed Technical Specifications
-
-## 1. Native Go Update System ("The Rename Trick" via Frontend Trigger)
-* **Context:** The application will keep its update-checking mechanism exactly **as-is in the Vue frontend (`app-info.vue`)**. The Vue layer handles fetching releases from the GitHub API and displaying the update modal to the user. The Go backend does not check versions; it acts purely as an execution engine.
-* **Refactor Requirement:**
- * Expose a new Go method to Wails: `TriggerNativeUpdate(downloadUrl string)`.
- * When the user confirms the update in the Vue modal, the frontend determines the target asset ZIP URL and passes it directly into this Go method.
- * The Go engine must stream the zip payload from the provided `downloadUrl`, unpacking the new executable file directly into the application root directory as `install-it.exe.new`.
- * **The Windows Trick Execution:** Windows prevents deleting or overwriting a running executable, but it allows renaming it on the same volume. Rename the *currently running binary* from `install-it.exe` to `install-it.exe.old`.
- * Rename `install-it.exe.new` to `install-it.exe`.
- * Use Go's `os/exec` to spawn the newly renamed `install-it.exe` cleanly into a separate system process, then immediately call `os.Exit(0)` on the running legacy instance.
- * **Boot Cleanup:** In `main.go` initialization routines (`init()` or `OnStartup()`), check if `install-it.exe.old` exists on disk. If found, silently purge it from the file system.
-
-## 2. Relational Storage Evolution (SQLite + gormigrate)
-* **Context:** `driver.go` and `matching.go` utilize complex loops, custom generic ID generators, and an event bus to handle cascading deletions when data relationships alter inside JSON arrays.
-* **Refactor Requirement:**
- * Introduce a pure-Go, CGO-free SQLite driver configuration (`modernc.org/sqlite` or `github.com/glebarez/sqlite`) via **GORM** to ensure cross-compilation capability for native Windows builds from any host system.
- * Migrate `DriverGroup`, `Driver`, and `RuleSet` data structures into formal GORM database schemas. You may leverage GORM's built-in JSON text-serializer tag (`gorm:"serializer:json"`) for deep nested slices like `Incompatibles []string` or `Rules []Rule` if splitting them into individual transactional database rows is architecturally redundant.
- * Implement `github.com/go-gormigrate/gormigrate/v2` to process schema evolution.
- * **The Baseline Rule:** Write a baseline migration block (`ID: "2026052601_baseline"`). Use GORM's `tx.Migrator().HasTable()` utility to inspect the target workspace database file. If an existing database has raw tables without tracking records, skip table creation but write the completion log entry into the migrations metadata tracking index to preserve legacy application installations safely.
-
-## 3. Simplified JSON App Configuration System
-* **Context:** `store.go` currently hosts complex generic interfaces (`Store`, `HasId`, `Create`, `Update`, `Delete`) optimized to manage multi-dimensional object lookups across files.
-* **Refactor Requirement:**
- * Retain `setting.json` exclusively for handling `AppSetting`.
- * Strip out all generic slices management frameworks (`HasId`, `GenerateId`, `IndexOf`) from `store.go` to reduce architectural bloat. Simplify `FileStore` and `MemoryStore` to strictly implement raw `Read(v any)` and `Write(v any)` logic, removing any abstract runtime cascading events from this domain layer.
-
-## 4. System Verification & Test Integrity
-* **Refactor Requirement:**
- * Analyze tests across the workspace. Update storage tests to verify that reading from database engines yields accurate object returns.
- * Construct mocking checks to validate that the sequence of download-rename-execute workflows returns graceful system errors if disk space runs low or file handles fail to release cleanly.
-
----
-
-# Execution Phases (Workflow Pipeline)
-
-You must proceed strictly in this sequence. At the end of each phase, the Orchestrator must generate a brief summary block confirming compliance before proceeding.
-
-### Phase 1: Planning & Architectural Mapping (Planner & Architect)
-* Scan all workspace layers (`main.go`, `store.go`, `driver.go`, `matching.go`, `app_setting.go`, `app-info.vue`).
-* Produce a concrete task blueprint mapping where database engines will be injected and how the Wails binding interface will be updated to receive the update string. Do not generate code changes yet.
-
-### Phase 2: Interface & Schema Isolation (Architect & Implementer)
-* Draft the database models and `gormigrate` initialization steps.
-* Simplify the structural layout of `store.go` to handle raw file transfers for `AppSetting` exclusively.
-
-### Phase 3: Core Implementation Block (Implementer)
-* Write the native Go-based file swap code that accepts a string URL from Wails.
-* Refactor `DriverGroupStorage` and `MatchRuleStorage` to read and write directly through the GORM database layer.
-* Update `app-info.vue` to drop the old trigger hooks and call the new `TriggerNativeUpdate(downloadUrl)` method, feeding it the resolved release string asset.
-
-### Phase 4: Quality Review & Safety Check (Reviewer)
-* Inspect the new codebase. Double-check for memory leaks, unhandled system database errors, and race conditions on file-rename locks inside Windows environments.
-
-### Phase 5: Testing & Final Verification (Tester & Orchestrator)
-* Execute tests. Validate migration step workflows across standard test datasets. Confirm compilation readiness.
-
-Begin Phase 1 by acknowledging your team structure and providing your initial analysis of the code layout.
\ No newline at end of file