# Self-update robustness and Wails migration review

**Review date:** 2026-08-28

**Scope:** MPV Manager self-update discovery, download, verification, replacement, rollback, restart UX, release publication, future multi-binary support, and the consequences of adopting Wails.

**Review type:** Evidence-backed architecture and failure-mode review, followed by implementation status updates.

## Implementation update — 2026-08-30

The v1.3 updater foundation is implemented. The linker/version identity defect
is fixed, and staged binaries must report the exact product, component,
normalized version, OS, and architecture through `--version --json`. Release
metadata now uses one component-aware schema, is signed with Ed25519, is
bounded and validated, and is rejected unless its signing key is embedded in
the release binary.

Preparation acquires a cross-process OS lock, downloads the selected artifact
once, verifies its signed size, BLAKE3 digest, and executable identity, stages
every configured manager target beside its destination, and fsyncs a unique
transaction journal. A copied helper waits for the initiating process to exit,
rechecks every staged target, applies all targets with unique backups, and
rolls the whole set back on any failure or interrupted startup. TUI relaunches
must acknowledge exact identity and remain alive through a stabilization
window before commit. Web updates hand off to the helper, show a non-dismissible
restart notice, and terminate on a backend-owned deadline.

Automated coverage now includes manifest signing/tampering/trust failures,
generator/consumer schema compatibility, lock contention, one-download
multi-target staging, durable commit and partial-target rollback, interrupted
transaction recovery, helper-host validation, identity mismatch rollback,
structured updater phases, successful/failed Web helper handoff and shutdown
ownership, the modal dismissal policy, and deterministic TUI
success/failure/relaunch cases.

The remaining v1.3 blockers are provisioning the protected signing/publication
variables, completing historical-artifact, additional-architecture, and native
signing/permission policy gates, and final release validation/publication.
Linux amd64, Windows 11 amd64, and macOS Apple Silicon current-protocol mechanics
now pass the isolated native matrix. Atlas task `b7ef5143` owns the remaining
release gates.

## Executive verdict

The original review found a solid base for a **single writable portable raw
binary**, but not a dependable cross-platform release updater. The immediate
identity and restart-lifecycle defects are now resolved as described above.

The portable updater foundation is ready for native qualification, but v1.3 is
still **not ready for publication** until protected release keys/endpoints are
provisioned and the native platform gates close. It cannot update the planned
signed macOS `.app` by replacing only its inner executable; the entire signed
bundle must be the update unit in v1.4.

The evidence sections retain the original defects and impact analysis so
regressions remain understandable, while their headings and resolution notes
show the current state.

The recommended direction is to make releases and updates **component-aware** now, then adopt Wails v3 for the Windows and macOS desktop products while keeping the current Web/TUI/CLI application on Linux. There is no planned Windows terminal edition. Wails v3 can run its update swap in helper mode without shipping a separate updater product, while the manifest and transaction protocol remain capable of describing more components if a future release genuinely needs them.

## Release roadmap decision

- **v1.3.0 — self-update hardening on the current product:** keep the existing Web/TUI/CLI application and current platform artifacts. Close the applicable SU-01–SU-09, UX-01–UX-04, and TEST-01 findings: exact version identity, signed/shared manifest, transaction locking and journaling, truthful component outcomes, backend-owned restart/termination, helper/relauncher behavior where needed, release publication, and native Windows/macOS/Linux qualification. Do not introduce Wails or change the product/frontend topology in this release.
- **v1.4.0 — Wails desktop products:** introduce the shared-frontend Wails v3 application for Windows and macOS, including the signed Windows installer/portable archive and signed/notarized macOS `.app`/DMG. Reuse the v1.3 update trust, identity, publication, and transaction foundations rather than coupling the Wails migration to unresolved updater defects.

This sequencing keeps v1.3 focused on making updates safe for the installed base and gives v1.4 a known-good updater foundation for its new packaging units.

## What was reviewed

- [`pkg/version/version.go`](../pkg/version/version.go): manifest parsing, platform selection, bounded download, BLAKE3 verification, swapping, rollback, secondary installation, and startup cleanup.
- [`cmd/mpv-manager/main.go`](../cmd/mpv-manager/main.go): CLI/TUI/Web entry points, update commands, server lifecycle, and process exit behavior.
- [`pkg/web/api.go`](../pkg/web/api.go), [`pkg/web/api_modal.go`](../pkg/web/api_modal.go), [`pkg/web/api_settings.go`](../pkg/web/api_settings.go), and [`internal/webassets/static/modal.js`](../internal/webassets/static/modal.js): update and shutdown UX.
- [`pkg/tui/models_update.go`](../pkg/tui/models_update.go), [`pkg/tui/models_messages.go`](../pkg/tui/models_messages.go), and [`pkg/tui/models_views.go`](../pkg/tui/models_views.go): update streaming, completion, and input behavior.
- [`cmd/generate-info/main.go`](../cmd/generate-info/main.go), [`.gitlab-ci.yml`](../.gitlab-ci.yml), and the live [`releases.json`](https://mpv.rocks/api/releases.json): release schema, artifacts, hashes, and publication.
- Relevant tests in [`pkg/version`](../pkg/version), [`pkg/web`](../pkg/web), [`pkg/tui`](../pkg/tui), and the frontend Vitest suite.
- Current official Wails v2/v3 build, platform, packaging, and updater documentation, researched on the review date.

The review did not overwrite the running manager with a real remote artifact. That would mutate the shared development executable, and the live manifest currently reports the same `1.2.0` version. Replacement behavior was assessed from the implementation, existing fixture tests, cross-builds, and non-destructive lifecycle probes.

## Current update flow

```text
signed releases.json ── embedded-key verification ── target selection
        │
        ▼
cross-process lock ── unique transaction directory + durable journal
        │
        ▼
download once ── signed size + BLAKE3 + exact binary identity
        │
        ▼
stage primary and configured secondary targets beside each destination
        │
        ▼
copied helper waits for initiating PID to exit, then acquires the lock
        │
        ▼
backup/replace/reverify every target
        │
        ├── Web/CLI: commit; user restarts after automatic termination
        └── TUI: relaunch, exact health acknowledgement, stabilization, commit
                         │
                 any failure/interruption
                         ▼
                 rollback every target
```

Strengths worth preserving:

- Artifact downloads have a 15-minute client deadline and 512 MiB limit.
- The progress path retries transient download failures three times.
- Missing signing trust, invalid signatures, unsupported schemas, size/hash
  mismatches, and wrong binary identities fail closed before replacement.
- The previous binaries are retained until all configured targets verify; a
  failure rolls the complete target set back.
- Startup recovery uses the durable journal to restore interrupted
  transactions and clean committed ones without disturbing an active helper.
- Platform/architecture asset selection has shared-schema coverage for all six
  current targets and leaves room for future product components.
- Package-manager builds can disable self-update with `SelfUpdateDisabled`.

## Platform readiness

| Distribution model | Current result | Assessment |
|---|---|---|
| Linux portable raw binary in a user-writable directory | The native synthetic-version matrix passes two-target commit, partial-apply rollback, locking, healthy TUI relaunch, and failed-health rollback on Linux amd64. | **Conditional**: historical N-1/N-2 artifacts, unwritable destinations, and Linux arm64 remain release gates. |
| Linux DEB/RPM/Arch/package-manager install | Self-update is safe only when every downstream build sets `SelfUpdateDisabled=true`. | **Delegated**: package-manager-owned files must be updated through that package manager. The UI should identify the owner and show the correct command/source. |
| Windows portable `.exe` | The same five-case synthetic-version matrix passes natively on Windows 11 amd64, including post-exit replacement of the just-running `.exe`. The gate found and fixed a missing `.exe` suffix on the staged identity-check payload. | **Conditional**: v1.1/v1.2 require a documented one-time manual replacement; ACL/antivirus fault injection, arm64, and Authenticode policy remain release gates. |
| macOS portable command-line binary | All five current-protocol transaction cases pass natively on Apple Silicon, including executable replacement and relaunch health. | **Conditional**: historical artifacts, Intel hardware, unwritable destinations, and quarantine/signing policy remain release gates. |
| Windows installed Wails desktop app | Not represented by the manifest or updater. | **Unsupported**: a signed NSIS/MSIX-installed GUI product needs installation-scope/elevation handling and preferably a post-exit helper or installer-driven replacement. |
| macOS Wails `.app` bundle | Not represented by the manifest or updater. | **Unsupported**: updating only `Contents/MacOS/<binary>` can leave resources and `Info.plist` at another version and invalidates the signed bundle. The signed/notarized `.app` must be treated as the update unit. |
| Linux Wails AppImage/DEB/RPM | Not represented by the manifest or updater. | **Unsupported**: each format has a different owner and replacement policy; a raw inner-binary swap is not an acceptable common strategy. |

Windows replacement is still subject to ACL/delete permissions. v1.3 avoids
replacing a live process by moving the transaction into the copied helper, but
native fault tests must determine whether the portable product also needs a
write-through `MoveFileExW` implementation; see Microsoft’s
[`MoveFileExW` reference](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw).

### Native qualification harness and current results

`cmd/qualify-selfupdate` is a development-only controller/fixture; it is not a
published updater or release artifact. `make qualify-selfupdate` builds two
copies from the current source with synthetic `1.2.99` and `1.3.0` identities,
uses isolated configuration and temporary install directories, and exercises:

1. rejection of a payload whose authenticated BLAKE3 digest is wrong;
2. rejection of a payload whose authenticated byte length is wrong;
3. atomic primary-plus-secondary commit from one downloaded payload;
4. complete rollback after the secondary staged file is deliberately removed;
5. cross-process lock rejection without replacing either target;
6. updated-process health acknowledgement and stable relaunch commit; and
7. rollback of both targets after an intentional relaunch health failure.

On 2026-08-30 all five cases passed on CachyOS Linux amd64, Windows 11 Pro amd64
(`win-dev`), and macOS 26.6.2 Apple Silicon (`macos-dev`), using Go 1.27 source.
The Windows run initially failed before replacement because the staged payload
lacked the platform-required `.exe` suffix; `updatePayloadFileName` now supplies
it and the rerun passed. The macOS run used native Darwin arm64 artifacts and
confirmed executable permissions survive commit and rollback.

On 2026-08-31 the expanded seven-case Linux amd64 matrix passed again after the replacement
protocol changed to never remove the public executable pathname. The same
validation added digest-before-exec ordering, journaled original target
size/hash/identity, mandatory verified rollback evidence, and a Linux native
PTY regression proving that a helper-launched TUI can render and consume input
on the initiating terminal. Windows amd64/arm64 and Darwin amd64/arm64 affected
packages also cross-compile; repeating the full native matrix on those systems
remains a release gate.

The follow-up transaction pass now uses one per-user lock for the complete
primary/secondary transaction, writes an `initializing` journal before network
or staging work, safely removes identifiable pre-journal orphans, and
revalidates committed targets before deleting rollback evidence. Update
selections carry an opaque proof bound to the verified manifest and selected
asset; the synthetic qualifier bypass is compiled only with the
`selfupdate_qualification` build tag. Detached helpers persist terminal
outcomes, which replace the Web `handed_off` task record in shared history on
the next normal start. Signed stable metadata also has a persistent
highest-accepted publication/version state, build-time freshness floor, key
validity epochs, and an embedded revocation denylist.

This harness validates the current transaction protocol under native process
and filesystem semantics. It does not substitute for upgrading an immutable
historical v1.1/v1.2 release, signed artifact policy, permission/antivirus fault
injection, or the Web browser lifecycle gate.

### Historical v1.1/v1.2 bootstrap boundary

The signed v2 document deliberately emits both `mpv-version` and the exact
historical `MpvVersion` spelling, plus the six legacy `manager` URL/BLAKE3
entries. Tests decode generated signed output through a v1.2-shaped structure,
so an old client can discover the v1.3 raw artifact and verify it against the
published BLAKE3 digest. The signature authenticates the publisher for v1.3+
clients; immutable old clients cannot be taught to verify it.

The old updater then renames the running executable to `.backup` from inside
that same process. This can be post-publication qualified on Linux/macOS, where
renaming an executing file is supported, but it is not a dependable Windows
bootstrap: Windows holds the running image open. No v1.3 code can retrofit the
external helper into an already installed v1.1/v1.2 process. The v1.3 release
must therefore tell Windows users on v1.2 or earlier to download the new
executable and replace the old one once. Updates originating from v1.3 use the
qualified helper transaction.

## Findings

Severity meanings:

- **P0** — blocks the next self-updating release.
- **P1** — can strand, misreport, incompletely update, or incorrectly terminate an installation; address before calling the updater production-ready.
- **P2** — robustness/extensibility debt that should be included in the redesign.

### SU-01 — Resolved 2026-08-30 — Tagged builds retained `1.2.0`

**Evidence:** [`CurrentVersion`](../pkg/version/version.go#L24) is declared in a `const` block. The [Makefile](../Makefile#L30) and [GitLab build template](../.gitlab-ci.yml#L166) pass `-X gitgud.io/mike/mpv-manager/pkg/version.CurrentVersion=<version>`. A diagnostic build with `CurrentVersion=9.9.9` still printed `Version: 1.2.0`.

**Impact:** A v1.3.0 artifact can report 1.2.0, pass the current smoke check, then be offered the same update again after restart. Dashboard, diagnostics, manifest comparisons, and package metadata can disagree.

**Required correction:** Make the build version a string variable with a development default; reject release builds whose embedded version is not exactly the normalized tag. Add a CI test that builds with a sentinel version and asserts `--version --json` returns the exact version, product ID, component ID, GOOS, and GOARCH.

**Resolution:** `CurrentVersion` is link-injectable, Make/CI normalize the tag,
the identity contract reports `mpv-manager` / `manager-portable`, and both a CI
sentinel build and release Linux-amd64 assertion verify every required field.

### SU-02 — Application identity resolved 2026-08-30 — Native signature gates remain

**Evidence:** [`verifyUpdatedBinary`](../pkg/version/version.go#L705) accepts any executable whose `--version` invocation exits zero. It does not parse output or compare it with `VersionCheckResult.LatestVersion`; nor does it verify product identity, component identity, architecture, or native code signature.

**Impact:** The wrong MPV Manager build, an old build, or an unrelated executable with a successful `--version` can be committed. SU-01 is therefore not detected.

**Required correction:** Add a stable machine-readable probe such as `version --json`, and verify every expected field before commit. Validate Authenticode on Windows and the signed/notarized app bundle on macOS as release gates.

**Resolution:** Post-swap verification parses the JSON identity and rolls back
on wrong/invalid product, component, version, OS, or architecture. Windows and
macOS native signature enforcement remains part of Atlas task `5a0590af`.

### SU-03 — Resolved 2026-08-30 — Hash integrity had no independent authenticity root

**Evidence:** The BLAKE3 hash and artifact URL arrive in the same unsigned HTTPS JSON document. [`verifySelfUpdate`](../pkg/version/version.go#L563) correctly detects corruption, but anyone able to replace the manifest can replace both the artifact and its hash.

**Impact:** TLS protects transport but a compromised release host, CDN origin, DNS/TLS control plane, or publication credential can authorize malicious bytes.

**Required correction:** Sign a canonical manifest or every component digest with an offline Ed25519 release key whose public key is embedded in the application. Include schema version, release/component versions, hashes, sizes, channels, minimum updater version, and key ID in the signed payload. Document rotation and revocation.

Wails v3 makes the same integrity/authenticity distinction and recommends an embedded Ed25519 key for production in its [self-update tutorial](https://v3.wails.io/tutorials/04-self-update-a-wails-app/).

**Resolution:** `pkg/releasemanifest` signs a deterministic schema-v2 payload
with Ed25519. Release binaries embed a key ring, unsigned/untrusted/tampered
documents fail closed, and CI verifies the generated and published manifests.
Protected key provisioning and rotation are documented release operations.

### SU-04 — Resolved for the v1.3 portable product — Replacement was not an installation transaction

**Evidence:** [`swapSelfUpdate`](../pkg/version/version.go#L662) renames exactly one executable in place while that executable is still running. It does not coordinate other MPV Manager processes, Windows installer scope/elevation, a macOS `.app`, Linux package ownership, desktop resources, or component compatibility.

**Impact:** The current approach cannot safely update the planned Wails product. A desktop instance and TUI/CLI instance can keep old files open or keep executing incompatible code/config migrations. Signed bundles/installations can be damaged by modifying an inner file.

**Required correction:** Stage updates in a unique transaction directory, ask known processes to shut down, launch an external signed helper, let the helper wait for the initiating and registered PIDs to exit, apply platform-specific artifacts, relaunch, wait for a health acknowledgement, and rollback on timeout/failure.

**Resolution:** v1.3 copies its already-authenticated running binary into the
transaction as the helper, waits for the initiating PID, applies post-exit,
relaunches TUI mode, and requires an exact health acknowledgement plus a live
stabilization window. Wails bundle/installer units remain v1.4 work.

### SU-05 — Resolved 2026-08-30 — Concurrent attempts shared fixed paths without a lock

**Evidence:** Every attempt uses `<executable>.new` and `<executable>.backup`. No process-local mutex, advisory file lock, transaction ID, or persistent journal guards [`stageSelfUpdate`](../pkg/version/version.go#L250).

**Impact:** Two Web requests, Web plus TUI/CLI, or two manager processes can truncate each other’s download, rename another transaction’s backup, fail rollback, or report success for mixed state.

**Required correction:** Acquire a cross-process update lock before checking/staging, return the active transaction to additional callers, and use unique transaction directories plus a durable journal. A desktop single-instance lock alone is insufficient because the current Web/TUI/CLI application can have multiple processes and the schema intentionally permits future components.

**Resolution:** Unix `flock` and Windows `LockFileEx` serialize attempts. UUIDv7
transaction paths, destination-adjacent staging/backup names, and fsynced JSON
journals isolate each attempt and support startup recovery.

### SU-06 — Resolved 2026-08-30 — The secondary PATH copy was non-transactional

**Evidence:** After the primary binary commits, [`stageSelfUpdate`](../pkg/version/version.go#L292) downloads the artifact again and updates the configured manager path. Errors are logged and deliberately not returned. [`installStagedBinary`](../pkg/version/version.go#L649) has no backup or post-swap version check. The TUI prints “PATH installation also updated” based only on configuration state, not the operation result.

**Impact:** An update can report complete with primary and PATH installations on different versions. A failed secondary rename is invisible to Web/CLI users; a successful but invalid secondary replacement has no rollback.

**Required correction:** Model the PATH/terminal installation as a component. Stage and verify it once with the rest of the transaction, commit it with backup/rollback, and return structured per-component outcomes. Do not infer success from “is configured.”

**Resolution:** The artifact is downloaded once, then durably staged beside
the primary and any existing configured secondary binary. The journal records
each target outcome; all applied targets roll back if any target fails.

### SU-07 — Resolved in CI 2026-08-30 — Release discovery was outside the release transaction

**Evidence:** CI builds and uploads raw artifacts, packages, and `BLAKE3SUMS.txt`, and creates a GitLab release. It only **builds** `generate-info`; it never runs it or atomically publishes `releases.json`. Atlas task `ad331971` tracks the required signed transactional publication foundation; current task state is available in generated [`TRACKING.md`](../TRACKING.md).

**Impact:** A successful tag pipeline does not automatically make a release discoverable. A partial/manual publication can reference incomplete artifacts or publish a version before every platform is ready.

**Required correction:** Generate, validate, sign, and publish the manifest only after every required artifact, checksum, signature, and native smoke gate passes. Upload to a versioned immutable URL first, then atomically advance a small signed channel pointer. Make publication resumable and idempotent.

**Resolution:** The tag pipeline now refuses to build release binaries without
trusted keys, generates/signs/verifies the manifest only after registry upload,
uploads an immutable tag-scoped copy, calls the configured atomic publication
endpoint, downloads the public result, and verifies its signature and expected
version before GitLab creates the release. Protected variables must be
provisioned before the first v1.3 tag.

### SU-08 — Resolved 2026-08-30 — The manifest was fixed and producer/consumer types drifted

**Evidence:** Both [`pkg/version.ReleaseInfo`](../pkg/version/version.go#L42) and [`cmd/generate-info.Release`](../cmd/generate-info/main.go#L176) hard-code six manager fields. Adding desktop, terminal, updater, packages, or universal builds requires source changes in producer and consumer. The live generator emits `mpv-version`, while consumers tag the field as `MpvVersion`; current tests use the consumer spelling and do not exercise the generated document.

**Impact:** The schema cannot express optional installed components, formats, signing, size, updater compatibility, or component-specific versions. Existing spelling drift silently drops MPV version data and demonstrates the risk of separate producer/consumer types.

**Required correction:** Define one versioned manifest package used by generator, publisher, consumer, fixtures, and schema validation. Decode with unknown-field/schema-version policy appropriate to forward compatibility, and add a test that pipes real generator output into the production decoder.

**Resolution:** `pkg/releasemanifest` is the sole producer/consumer schema. Its
component/asset model carries platform, architecture, CPU baseline, format,
scope, strategy, signed size/hash, expected identity, and minimum updater
version while retaining legacy top-level fields for the v1.2-to-v1.3 bootstrap.
Marshaled output includes both the current `mpv-version` and legacy
`MpvVersion` spellings, and generator tests decode the signed result through a
v1.2-shaped consumer.

### UX-01 — Resolved 2026-08-30 — Web auto-termination was client-driven and dismissible

**Original evidence:** The update success modal stated that a restart was required,
but its five-second countdown and shutdown request were browser-owned. Closing
the modal or tab, losing the connection, or a JavaScript error could prevent
termination.

**Follow-up verification:** The previously reported *premature success-modal close* regression is fixed in the current source. The production `dialog.js` and `modal.js` were exercised in Chromium with the update and modal API responses stubbed at the network boundary. The “Update Successful!” notice remained visible and was not in its closing state at 50 ms, after the old 200 ms hide deadline (350 ms), and during the countdown (1.45 seconds). It made exactly one `/api/shutdown` request only after the countdown expired, then changed to the shutdown overlay. The fix is the combination of cancelling a pending `hideModalTimeout` when `showModal` replaces the old modal and suppressing the outer `finally` hide for `show_modal` responses. This narrows UX-01 to browser ownership/dismissibility; it is not a premature-close defect.

The 2026-08-30 release smoke repeated this against the live server-owned modal
configuration: at 700 ms the “Update Ready” notice was still visible, both
dismiss controls were hidden, the restart instruction and five-second countdown
were present, and exactly one intercepted shutdown POST fired at expiry. A final
non-intercepted shutdown returned HTTP 200 and the Linux process exited 0.

**Original impact:** The old process could keep running indefinitely after
replacing its file, contrary to the UI promise and requested behavior.

**Required correction:** Once the backend has durably staged/committed an update, it must own a short graceful-shutdown deadline. The browser may display/cancel only if the product explicitly supports “restart later”; it must not be the sole shutdown trigger. For desktop, the helper should relaunch automatically. For terminal automation, exit successfully and make relaunch opt-in.

**Resolution:** Completion schedules shutdown in the server process independent
of JavaScript. The restart-required modal is non-dismissible and its countdown
is supplemental UX rather than the only termination mechanism.

### UX-02 — Resolved 2026-08-30 — Graceful Web shutdown exited as an error

**Original evidence:** The shutdown API raced a graceful server close against
`os.Exit(0)`, while `runWebMode` treated the resulting nil server error as a
failure. A live isolated probe observed:

```text
POST /api/shutdown -> HTTP 200
Error: Web server failed: <nil>
process exit status: 1
```

**Original impact:** User-requested and update-triggered closes were reported as
crashes, and `os.Exit` bypassed deferred cleanup.

**Required correction:** Give the process one owner for shutdown. Return/recognize normal server closure, run cleanup, and exit 0 through `main`; do not race two `os.Exit` calls.

**Resolution:** The API requests graceful server shutdown without calling
`os.Exit`; `runWebMode` recognizes a nil server result as normal and returns
through deferred cleanup. A live authenticated shutdown probe exits 0 without
the former `<nil>` error.

### UX-03 — Resolved for v1.3 on 2026-08-30 — TUI update did not complete or restart

**Original evidence:** On success, the TUI update goroutine closed its output
channels without sending the terminal result. The model never marked the update
complete, Enter only quit instead of relaunching, and Escape could return to the
old process.

**Original additional defect:** Update type was inferred from an "Updating"
string prefix, so ordinary MPV client updates could be mistaken for manager
updates.

**Required correction:** Use an explicit operation/component enum, always complete/close a single result channel, show “Restarting…” after success, and hand control to the helper. Add deterministic Bubble Tea model tests for manager success/failure, ordinary client updates, Escape policy, and helper-launch failure.

**Resolution:** `OpTypeManagerUpdate` owns the workflow, the goroutine always
sends a terminal result, and Enter launches the updated binary in internal
delayed-restart mode before quitting the old TUI. Escape cannot resume old code
after commit. Tests cover manager success/failure, ordinary updates, Escape,
launch failure, and retry. The journaled health-ack helper remains in the
transaction task rather than this UI correction.

### UX-04 — Resolved 2026-08-30 — Web progress was a blocking spinner

**Original evidence:** The manager-update request blocked during download,
reported only a spinner to the browser, and fetched the manifest again after the
user had selected a release.

**Original impact:** A long download looked hung, connection loss was ambiguous,
and confirmation and download could observe different manifest versions.

**Required correction:** Run update work as a background job with a single immutable release selection and structured states: `checking`, `downloading`, `verifying`, `ready_to_restart`, `restarting`, `committed`, `rolled_back`, `failed`. Stream it through the existing SSE/job infrastructure or Wails events.

**Resolution:** `/api/manager/update` returns HTTP 202 with a job ID. The job
streams the required named phases through the existing SSE infrastructure and
uses the exact `VersionCheckResult` selected by its one manifest check.

### SU-09 — Resolved for v1.3 safety 2026-08-30 — Crash durability and metadata bounds were incomplete

**Evidence:** The progress downloader ignores destination `Close` errors, files/directories are not explicitly synced before rename/commit, and the manifest body uses unbounded `io.ReadAll`. There is no free-space preflight. The hand-written version comparator treats non-numeric suffixes as numeric prefixes rather than full semantic versions.

**Impact:** Disk-full or delayed-write failures can be accepted until a later launch; a large manifest can consume unnecessary memory; prerelease/channel ordering can be wrong.

**Required correction:** Bound manifest size, validate content type/schema, check every close, sync staged content and transaction metadata as required by the platform, preflight disk space and destination permissions, and use one tested semantic-version policy.

**Resolution:** Manifest reads are content-type checked and capped at 2 MiB;
asset downloads are capped at 512 MiB and signed exact sizes; close/write errors
are checked; staged files and journals are synced before handoff; destination
staging exposes space/permission failures before replacement; and version
ordering now handles prereleases and ignores build metadata. A separate
advisory free-space estimate is unnecessary for correctness because partial
copies are removed and cannot reach helper handoff.

### TEST-01 — Partially resolved 2026-08-30 — Native qualification in progress

**Current evidence:** Successful Web job/shutdown, modal policy, exact identity
rollback, and TUI completion/relaunch tests run on Linux. The development-only
native qualifier additionally passes the original five commit/rollback/lock/relaunch
cases above on Linux amd64, Windows 11 amd64, and macOS Apple Silicon. The
Windows gate caught and closed the staged-payload `.exe` defect. A current
Windows v1.3 binary also passes native identity/help, Web/API/platform probes,
and graceful exit-0 smoke checks.

**Remaining impact:** Cross-compilation proves buildability, but not native
replacement, rollback, signing, or relaunch behavior.

**Required correction:** Add native release gates listed under “Acceptance gates” below. Cross-compilation proves buildability, not update correctness.

**Update:** Linux amd64, Windows 11 amd64, and macOS Apple Silicon transaction
mechanics are natively qualified with synthetic versions. Historical N-1/N-2
upgrades, additional architectures, signed-artifact policy, and remaining
permission/antivirus/quarantine fault cases remain mandatory release work.

## Recommended future-ready update architecture

The architecture below is the **v1.4 target**. v1.3 retains `cmd/mpv-manager` and the current Web/TUI/CLI behavior on every platform while extracting reusable update services behind it.

### Product layout

Use distinct platform products with shared Go packages and one Web frontend:

```text
cmd/mpv-manager-desktop   Wails GUI; Windows installed/portable app and macOS .app
cmd/mpv-manager           current Linux Web/TUI/CLI product
pkg/web                   shared templates, htmx/Alpine assets, HTTP handler, jobs
pkg/update                manifest, policy, staging, verification, journal
pkg/updateplatform        Windows/macOS/Linux replacement adapters
```

The Windows and macOS products are desktop-only. The Linux binary retains Web, TUI, and CLI modes and stays free of WebView/GTK dependencies. The installed and portable Windows downloads contain the same Wails desktop application; the difference is delivery and installation ownership, not a second frontend or console edition.

Do not add a separately published updater executable initially. Wails v3's updater deliberately swaps and relaunches without a separate helper artifact by running helper-mode mechanics around the application. If native fault testing later proves that MPV Manager needs a standalone helper, it can be signed, versioned, embedded, and extracted to a unique transaction directory; its protocol must then remain backward compatible and the manifest must declare `min_updater_version`.

### Manifest v2 sketch

```json
{
  "schema_version": 2,
  "release_version": "1.3.0",
  "channel": "stable",
  "published_at": "2026-09-01T12:00:00Z",
  "min_updater_version": "1.2.1",
  "components": {
    "manager-portable": {
      "version": "1.3.0",
      "assets": []
    }
  },
  "signature": {
    "algorithm": "ed25519",
    "key_id": "release-2026-01",
    "value": "..."
  }
}
```

Each asset should carry at least:

- `goos`, `goarch`, CPU baseline, format (`raw`, `zip`, `app`, `nsis`, `msix`, `appimage`, `deb`, `rpm`), URL, byte size, digest, signature, and native-signing expectation.
- Component/product identity and exact version expected from its health probe.
- Installation scope/owner (`portable`, `user`, `system`, package manager) and update strategy.
- Compatibility constraints where independently installed components may differ.

For v1.3, `manager-portable` represents the current application across its supported raw artifacts. v1.4 can add `desktop` and `linux-portable` components without changing the signed envelope or forcing them to be installed together. The updater should discover the installed product and select only its component and packaging strategy. If a future release introduces multiple co-installed components, the same schema can define their compatibility set and require whole-set rollback when one component fails.

### Transaction protocol

1. Acquire a cross-process update lock and load/recover any prior journal.
2. Fetch and verify one signed manifest; keep that exact selection for the transaction.
3. Discover installed components and their owner/scope; delegate package-managed components.
4. Preflight permissions, free space, supported OS/runtime, native signature policy, and running instances.
5. Download every required asset once into a unique transaction directory.
6. Verify length, digest, Ed25519 signature, product/component/version/architecture, and native signature before requesting shutdown.
7. Persist and sync a journal containing the old/new paths, hashes, PIDs, primary component, and relaunch arguments.
8. Notify every MPV Manager frontend: “Update ready; restarting automatically.” Reject new mutating jobs and drain active state.
9. Enter the v1.3 helper/restarter mode (or Wails helper mode in v1.4), then exit the initiating app cleanly with status 0. The helper waits for only the recorded MPV Manager PIDs; it must not broadly kill processes by name.
10. Apply the platform adapter:
    - **Windows portable/installed:** replace after exit; preserve ACL/scope; use a signed installer when installation ownership requires it.
    - **macOS desktop:** replace the complete signed/notarized `.app` bundle on the same volume; never patch only its inner executable.
    - **Linux portable:** atomic same-filesystem rename; use an AppImage-aware mechanism for AppImage.
    - **Package manager:** do not modify managed files; invoke or display the owner-specific upgrade path.
11. Relaunch the primary component with `--post-update <transaction-id>` (or the Wails equivalent).
12. Require a bounded health acknowledgement containing exact product/component/version. On timeout/failure, restore all backups and relaunch the old primary.
13. Commit, clean backups asynchronously, record structured history, and release the lock.

## Wails migration report

### Current framework status

As of 2026-08-28, Wails v3 remains beta/pre-GA; the latest visible release is `v3.0.0-beta.15`, and its release notes describe the API as stable while warning that issues may remain. MPV Manager does **not** need to wait for GA: the team's use of v3 in the Financr application provides relevant operating experience. Pin an exact v3 version, upgrade deliberately, and make MPV Manager's native qualification gates—not the GA label—the release decision. See the official [Wails release list](https://github.com/wailsapp/wails/releases) and [v3 FAQ](https://v3.wails.io/faq/).

Wails v3 has a first-party updater that stages, shows progress/release notes, exits, uses helper mode to swap, and relaunches. It supports SHA-256 and optional Ed25519 verification and warns about macOS signing/notarization, Windows SmartScreen, and atomic sidecar publication. See the official [self-update tutorial](https://v3.wails.io/tutorials/04-self-update-a-wails-app/). It is the preferred desktop implementation starting point, but it does not by itself solve MPV Manager's package-manager ownership, signed-manifest publication, or exact post-update identity gates.

### Decided platform split

The follow-up product decision is:

| Platform | Primary product | Alternative | Rationale |
|---|---|---|---|
| Windows | Wails v3 desktop application delivered through an installer; sign the application and installer with Microsoft Artifact Signing | Signed portable archive containing the same Wails desktop application | Almost no Windows users need TUI/CLI. The alternative is no-install desktop delivery, not a terminal product. |
| Linux | Keep the current Go binary with Web UI default plus TUI/CLI modes | Distribution/package-manager builds with self-update disabled | Avoids imposing GTK/WebKit ABI dependencies on the broad Linux support matrix. The current browser-based model is conventional and useful on Linux. |
| macOS | Wails v3 desktop application as a signed, hardened, notarized `.app`, normally delivered in a DMG | A zipped copy of the same signed `.app` may be used as the updater payload | A real application bundle matches platform expectations. `macos-dev` can perform native testing, signing, notarization, and final DMG creation. |

Under this split, each platform has one primary application component. The component-aware manifest is retained for format/platform ownership and future expansion, not because Windows ships two products. Shared configuration schemas must remain backward compatible across a supported version window.

Suggested release artifact naming:

- `mpv-manager-<version>-windows-<arch>-installer.exe` — the signed NSIS installer package.
- `mpv-manager-<version>-windows-<arch>-portable.zip` — the same signed Wails desktop app without installation.
- `mpv-manager-<version>-darwin-universal.dmg` — the signed/notarized user download.
- `mpv-manager-<version>-darwin-universal-app.zip` — a byte-preserving archive of the signed/notarized `.app` for update staging.

The word `setup` in the earlier proposal meant the **installer package**, not another installed binary. Wails' Windows packager produces an NSIS `<AppName>-installer.exe`; it installs the actual application, shortcuts, uninstaller, and WebView2 bootstrap behavior. Calling the artifact `-installer.exe` is clearer. This installer is also distinct from updater helper mode: Wails v3 documents that it can stage, swap, and relaunch without shipping a separate helper executable.

The Wails desktop apps can use Wails v3's updater behind MPV Manager's signed release metadata and exact version identity. Windows installation scope must determine whether an update can replace the installed app directly or should run a signed installer transaction. macOS updates must replace the complete signed `.app`, never only `Contents/MacOS/mpv-manager`.

### One frontend, two hosts

Wails does **not** require a frontend rebuild. `application.AssetOptions` accepts an arbitrary Go `http.Handler`, and the official [asset-server documentation](https://v3.wails.io/contributing/asset-server/) describes that public handler surface and its production embedding behavior. MPV Manager can refactor `pkg/web` into a reusable router and run the same templates, htmx, Alpine components, CSS, and API behavior in either host:

```text
shared pkg/web handler + shared embedded frontend
          ├── Linux browser host: net/http server on loopback
          └── Windows/macOS host: Wails AssetOptions.Handler in native WebView
```

The first Wails implementation should therefore preserve the existing frontend and its current Vitest/browser coverage. Desktop-specific work is limited to the host adapter: trust `wails.localhost` without weakening browser-mode Host/Origin protection, map close/restart lifecycle to Wails, and prove that htmx navigation and job streaming work through the asset handler. If SSE streaming is unsuitable in a native WebView, only the job transport should switch to Wails events behind a small frontend adapter; that still does not create a second page/component frontend.

A later page-by-page conversion from HTTP APIs to Wails bindings/events is optional optimization, not a migration prerequisite. Maintaining separate browser and native implementations of every screen would be wasteful and is explicitly out of plan.

### htmx 4 timing

Do not add the htmx 4 migration to v1.3.0. Version 4.0.0 was released on 2026-08-28 as a ground-up `fetch()` rewrite, while the project intentionally keeps htmx 2 as the npm `latest` line until early 2027 and states that htmx 2 will remain supported. v1.3 is a release-safety change for the installed base; coupling it to a new request/swap engine would make updater regressions harder to isolate.

The project now manages htmx 2.0.10 through npm and synchronizes its reviewed
distribution into the embedded static tree. The htmx 4 migration should be
**phase zero of v1.4 development, after v1.3 is stable and before Wails host
integration**. Test it first through the current loopback browser host; only
then give the migrated shared frontend to Wails. If a current 4.x patch or the
spike is not satisfactory at that point, v1.4 can ship on htmx 2.0.10 and defer
htmx 4 without blocking Wails.

The migration is contained but not a drop-in file replacement:

- The official `4.0.0 upgrade-check` reports three old JavaScript event names: two Logs swap listeners and the global response-error listener. Manual review adds nine `hx-on:htmx:*` event attributes that must use the v4 colon-separated names.
- No implicit attribute inheritance, removed attributes, custom extensions, or server-side `HX-Trigger`/`HX-Target` consumers were found.
- Five programmatic `htmx.ajax()` call sites need promise, target, error, and timeout verification.
- Apps refresh uses two OOB swaps; v4 changes main/OOB ordering, so the installed-apps and available-updates result must be tested together.
- v4 swaps 4xx/5xx responses by default, unlike the application's intentional htmx 2 behavior. Configure `noSwap` or explicit `hx-status` handling while moving `htmx:responseError` to `htmx:response:error`.
- v4's default request timeout is 60 seconds instead of unlimited. Select and test an explicit bound for potentially slow installed-app/update probes.

Migrate directly rather than keeping the htmx 2 compatibility extension. The small surface makes a clean migration and focused browser regression suite less risky than carrying two event/attribute dialects into the Wails frontend.

### Main consequences

| Area | Consequence |
|---|---|
| Binary topology | Windows/macOS ship the Wails desktop application; Linux ships the current multimode application. They are built from shared Go packages, with no Windows terminal product. |
| Frontend architecture | Keep one server-rendered Go-template/htmx/Alpine frontend. Wails' arbitrary `http.Handler` support makes the shared-handler approach a supported architecture; bindings/events are optional targeted adaptations. |
| Local HTTP security | The direct Wails asset handler removes the desktop loopback listener while retaining familiar request/route semantics. Browser mode keeps the auth cookie, DNS-rebinding, and Origin policy; desktop mode needs a narrowly scoped `wails.localhost` trust policy. |
| Jobs/SSE | Keep the current job APIs/SSE through the shared handler if native tests pass. Wails events are a targeted fallback for streaming, not a reason to rewrite the pages. |
| CLI/TUI | They remain in the Linux product and do not inherit Wails/WebView runtime dependencies. |
| Windows runtime | Wails uses WebView2; Windows 10/11 often has it, but packaging must choose download/embed/browser/error behavior when absent. See Wails’ [Windows WebView2 guidance](https://wails.io/docs/next/guides/windows/). |
| Linux support | Wails adds GTK/WebKit runtime dependencies and ABI-specific packages. Wails v2 documents GTK3/WebKit2GTK 4.0/4.1 variation; v3 defaults toward GTK4/WebKitGTK 6.0 with a temporary GTK3 path. This narrows “works on most distros” unless multiple packages/builds are produced. See [v2 Linux distro support](https://wails.io/docs/guides/linux-distro-support/) and the [v3 FAQ](https://v3.wails.io/faq/). |
| macOS product | Distribution becomes a complete `.app` (and likely DMG) with signing, hardened runtime, notarization, bundle metadata, and universal/architecture artifacts. Wails documents that cross-built apps are not signed and final DMGs require macOS; see [macOS packaging](https://v3.wails.io/guides/build/macos/). |
| Build pipeline | Current `CGO_ENABLED=0` Linux-container cross-builds are insufficient for macOS/Linux Wails targets. Wails v3 uses CGO for both and supports Docker/Zig cross-builds, but macOS still needs native signing/notarization. See [cross-platform building](https://v3.wails.io/guides/build/cross-platform/). |
| Windows packaging/signing | Wails can produce NSIS installers. Sign both the installed application and installer with Microsoft Artifact Signing, then verify signatures during release and update tests. See the [Wails Windows packaging guide](https://v3.wails.io/guides/build/windows/) and [Microsoft Artifact Signing integrations](https://learn.microsoft.com/en-us/azure/artifact-signing/how-to-signing-integrations). |
| Automation/testing | Existing browser/frontend tests remain shared. Native WebView lifecycle, close/restart, installer, signature, and updater tests are additional platform gates on `win-dev` and `macos-dev`, not a duplicate frontend suite. |
| Concurrency/config | Multiple instances or modes can use the same config/job history concurrently. Config schema migrations, write locks, update locks, and single-instance policy must be explicit. |

### Does Wails require a Windows CI runner?

**Not to compile it.** Wails v3 documents that Windows is its simplest cross-compilation target: the default Windows build does not require CGO and can be built from Linux or macOS with `wails3 build GOOS=windows`. A Linux CI runner can continue producing unsigned Windows binaries.

**Use Windows for the production release gate anyway.** Microsoft documents Windows, Windows SDK `SignTool.exe`, .NET 8, and the Artifact Signing client Dlib as prerequisites for the SignTool integration. In this GitLab-based project, the most direct pipeline is to make `win-dev` a protected self-hosted runner (or invoke an equivalently isolated Windows signing worker) for:

- Wails/NSIS packaging and Microsoft Artifact Signing of both the application and installer;
- signature verification, clean install/upgrade/uninstall, WebView2 bootstrap/runtime, SmartScreen/AV, file-lock, restart, and rollback tests;
- publishing only after those native gates pass.

Similarly, Wails can cross-build an unsigned macOS `.app`, but Apple signing/notarization and final DMG creation require macOS tooling. Use `macos-dev` as the protected macOS packaging/signing/native-test worker. Keep signing identities and cloud credentials scoped to protected release tags; ordinary merge-request builds should remain unsigned and run on the existing Linux runner.

### Migration options

#### Option A — One shared handler in browser and Wails hosts (**selected**)

Refactor the Web server so it exposes a reusable `http.Handler`, serve it through Wails' asset-handler facility, and keep templates, htmx, Alpine, APIs, and jobs shared. Browser mode wraps the handler in `http.Server`; desktop mode gives it to Wails.

**Advantages:** fastest visual migration, smallest frontend rewrite, existing 155 frontend assertions remain valuable.

**Costs:** retains server-style routing and requires a desktop trust/lifecycle adapter. Some server-only auth middleware becomes conditional because the Wails handler is not network-exposed.

#### Option B — Incrementally replace selected transports with Wails bindings/events

Keep the same visual assets, templates, and Alpine components, but replace individual page APIs or SSE with Wails bindings/events only where native behavior materially benefits.

**Advantages:** no loopback listener for desktop, clearer desktop lifecycle, native dialogs/menu/update integration, and smaller local attack surface.

**Costs:** every converted transport needs browser and Wails adapters while Linux browser mode remains supported. Do this selectively rather than rebuilding the frontend.

#### Option C — Build a separate native frontend (**rejected**)

Reimplement the same screens around Wails bindings while retaining the current Linux browser UI.

**Advantages:** can remove HTTP concepts from the desktop product completely.

**Costs:** creates two frontends to design, implement, and test. That cost is not justified when Wails can host the existing handler.

### Recommendation

1. For **v1.3.0**, keep the current product and close the self-update findings before publishing the next self-updating release.
2. In v1.3, extract a UI-independent update service and signed manifest v2, implement restart/transaction correctness, and qualify the current artifacts natively on `win-dev`, `macos-dev`, and Linux. Do not bake the fixed one-binary manifest deeper into a future desktop app.
3. For **v1.4.0**, adopt a pinned Wails v3 release without a GA hard gate. Start with the reusable-handler proof, then qualify it on `win-dev` and `macos-dev` before it becomes a production replacement.
4. In v1.4, ship the Wails desktop as the Windows primary product through a Microsoft Artifact Signed installer, with the same signed desktop app in a portable archive as the alternative. Do not build a Windows terminal edition.
5. In v1.4, ship a real signed, hardened, notarized macOS `.app` and DMG; update the complete app bundle through the Wails/helper lifecycle.
6. Keep Linux on the current Web/TUI/CLI binary through both releases. Preserve one shared frontend across the Linux browser and Windows/macOS WebViews, converting only targeted transports to bindings/events if native testing demonstrates a need.

## Acceptance gates

### Before v1.3.0

- Release build embeds the exact tag; `--version --json` proves version, product, component, OS, and architecture.
- Updated binary must report the selected version before commit.
- One update attempt at a time across processes; concurrent callers observe the same transaction.
- Web success message is displayed, backend owns auto-termination, and normal shutdown exits 0.
- TUI success reaches completion, cannot continue running old code, and distinguishes manager updates from client updates.
- CLI update exits 0 with a truthful message; relaunch policy is documented.
- CI generates/validates/signs/publishes the release manifest only after all required assets exist.
- Generator output is decoded by the production manifest type in tests.

### Native current-product matrix before v1.3.0

- Windows 10/11 amd64 and Windows arm64: current portable executable, writable/unwritable locations, antivirus/locked backup, insufficient permission, concurrent instance, helper relaunch, rollback, Web update notice/termination, and exit status.
- macOS Intel and Apple Silicon (or universal): current raw executable, writable/unwritable locations, code-signature policy for the distributed artifact, helper relaunch, health timeout, rollback, and Web update notice/termination.
- Linux amd64/arm64 portable: user-writable/unwritable paths, AppImage if supported, power-loss fault injection around every journal/swap phase, relaunch, and rollback.
- DEB/RPM/Arch: self-update disabled, correct package-owner UI, and no writes to package-managed files.
- Update paths from N-2 and N-1 to N, including manifest/updater protocol compatibility and config migrations. For v1.3, Windows v1.1/v1.2 is an explicitly documented manual bootstrap because the immutable old updater replaces its own running executable; automated N-2/N-1 coverage starts with updates originating from v1.3.
- Failure cases: offline, HTTP error, oversized/truncated download, wrong hash/signature/product/version/architecture, insufficient disk, stale journal, corrupted backup, relaunch failure, health timeout, and partial multi-component commit.

### Desktop product matrix before v1.4.0

- Windows 10/11 amd64 and Windows arm64: Artifact Signed Wails app and NSIS installer, portable desktop archive, clean install/upgrade/uninstall, WebView2 bootstrap/runtime, antivirus/locked backup, installation scope, concurrent instance, helper relaunch, rollback, and exit status.
- macOS Intel and Apple Silicon (or universal): signed/notarized `.app`, DMG installation, Gatekeeper validation, complete bundle replacement, read-only/translocated location handling, helper relaunch, health timeout, and rollback.
- Shared frontend parity: the current route and workflow suite passes through both the Linux browser host and native Wails asset handler without maintaining duplicate screen implementations.

## Validation evidence

Run during this review:

```text
go test -race -count=1 ./pkg/version ./pkg/web ./pkg/tui
  PASS (version 17.542s, web 5.902s, tui 1.296s)

npm test -- --run
  PASS (11 files, 155 assertions)

linker sentinel build: CurrentVersion=9.9.9
  binary output: Version: 1.2.0 (FAIL, SU-01)

isolated Web shutdown probe
  API: HTTP 200
  process: "Error: Web server failed: <nil>", exit 1 (FAIL, UX-02)
```

Passing unit/race/frontend suites establish that existing covered behavior remains healthy; they do not override the explicit failing probes or missing native scenarios above.

## Prioritized implementation sequence

1. **v1.3 release safety:** variable version injection, exact version/product probe, generated-manifest consumer test, and CI release assertion.
2. **v1.3 restart correctness:** server-owned graceful termination with exit 0; repair TUI completion/classification; test Web/TUI/CLI outcomes.
3. **v1.3 update trust/concurrency:** signed manifest, bounded schema, cross-process lock, unique transaction journal, single manifest fetch, durable staging, and truthful per-install outcomes.
4. **v1.3 helper/transaction lifecycle:** installed-product discovery, structured outcomes, post-exit swap/relaunch/health rollback, and future component compatibility without publishing an unnecessary updater artifact.
5. **v1.3 native release qualification:** current Windows/macOS/Linux artifacts pass update, relaunch, rollback, concurrency, and failure gates on their native platforms.
6. **v1.4 Wails implementation:** pin a known-good v3 release, prove the shared `http.Handler` frontend host, package/sign Windows and macOS desktop products, and adapt only the lifecycle/auth/job transports that native testing requires.
