Skip to content

feat: add non-blocking in-app update for patch-behind container installs - #124

Merged
henrywang merged 1 commit into
mainfrom
123-inapp-patch-update
Aug 23, 2026
Merged

feat: add non-blocking in-app update for patch-behind container installs#124
henrywang merged 1 commit into
mainfrom
123-inapp-patch-update

Conversation

@henrywang

Copy link
Copy Markdown
Owner

Summary

ContainerCompatibility.mismatch() deliberately compares major.minor only —
patch releases are ignored, since a patch bump upstream is additive/bugfix,
not an API break. That means a daemon on the same major.minor as
requiredVersion (e.g. installed 1.2.0 vs pinned 1.2.2) reads as fully
compatible: no gate, no prompt, and — since SystemView.swift never called
upgradeContainer/installContainer outside DaemonGateView's blocking
gate — no way in the app to actually reach the pinned patch.

That left patch-gated capabilities (ContainerCompatibility.isAtLeast, e.g.
running-container export added in 1.2.1, #107) silently unreachable on an
otherwise-healthy install, with zero UI signal why. Discovered directly:
this machine's real daemon reports 1.2.0 against Berthly's 1.2.2 pin, and
nothing in the app said so.

Changes

  • ContainerCompatibility.isPatchBehind(installed:required:) — same
    major.minor as required (so mismatch still reads compatible) but behind
    the exact pinned patch.
  • System page: Status row now shows "Update available" for a patch-behind
    install, plus a new "Update Container to v1.2.2…" button (non-blocking —
    the page stays reachable, unlike the hard version-mismatch gate).
  • upgradeContainer still stops the daemon mid-flight, which would tear
    SystemView down before the update finishes. The progress-tracking state
    that used to be private @State inside DaemonGateView is lifted into a
    new DaemonOperationCoordinator (@Observable, injected via
    .environment), so both DaemonGateView's own triggers (install/start/
    hard-mismatch-update) and the System page's new button share one place
    that survives the daemon-restart teardown.
  • Added the three string-catalog entries ("Up to date", "Update available",
    "Newer than Berthly supports") that were missing from
    Localizable.xcstrings even before this change — the .tooOld/.tooNew
    cases existed in source but were apparently never extracted. Caught while
    reusing that same statusText switch.

Test plan

  • swiftlint lint --strict — 0 violations
  • BerthlyTests (unit) — full suite passes, including new
    isPatchBehind cases (exact match, older patch same minor, newer patch,
    older minor, newer major)
  • BerthlyUITests (mock mode) — testNotInstalledGateInstallsAndConnects
    and testVersionMismatchGateUpdatesAndConnects pass unchanged (regression
    check on the DaemonGateView refactor), plus a new
    SystemViewTests.testPatchUpdateSurvivesDaemonRestart covering the new
    button end-to-end in mock mode, including that the progress screen
    survives the daemon-restart teardown
  • Manual verification against the real local daemon (1.2.0, pinned 1.2.2):
    built and launched the app, confirmed the System page shows "Update
    available" / Installed 1.2.0 / Required 1.2.2 / "Update Container to
    v1.2.2…" — screenshot-verified. Did not click through the actual upgrade
    (admin-elevated system change); that's for the user to trigger deliberately.

Fixes #123

ContainerCompatibility.mismatch() deliberately ignores patch versions, so a
daemon on the same major.minor as requiredVersion (e.g. 1.2.0 vs pinned
1.2.2) reads as fully compatible — no gate, no prompt, and no way in the app
to reach the pinned patch. That left patch-gated capabilities (isAtLeast,
e.g. running-container export from #107) silently unreachable with no
signal why.

Adds ContainerCompatibility.isPatchBehind and a "Update Container to
v1.2.2…" affordance on the System page, distinct from DaemonGateView's
hard version-mismatch gate. upgradeContainer still stops the daemon
mid-flight, which would tear SystemView down before the update finishes, so
the progress-tracking state (previously private to DaemonGateView) is
lifted into a DaemonOperationCoordinator injected via .environment — the
System page's button and DaemonGateView's own triggers now share it.

Fixes #123
@henrywang henrywang added this to the 1.2.1 milestone Aug 23, 2026
@henrywang
henrywang merged commit f2e4cef into main Aug 23, 2026
5 checks passed
@henrywang
henrywang deleted the 123-inapp-patch-update branch August 23, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No in-app path to update container across patch versions

1 participant