[client/ui] Replace update event fan-out with typed UpdateState API

The auto-update feature was driven by two narrow Wails events
(netbird:update:available and :progress) plus a SystemEvent-metadata
iteration on the React side. Both surfaces had to know the daemon
metadata schema (new_version_available, enforced, progress_window),
and the frontend had no pull endpoint to seed its state on mount.

Extract the state machine into a new client/ui/updater package, mirroring
how i18n and preferences are split between domain logic and a thin
services facade. The package owns the State type, the metadata-key
parsing, the mutex-guarded Holder, and the single netbird:update:state
event. services.Update keeps the daemon RPCs (Trigger, GetInstallerResult,
Quit) and gains GetState as a Wails pull endpoint.

Tray-side update behaviour moves out of tray.go into a dedicated
trayUpdater (tray_update.go): owns its menu item, OS notification,
click handler, and the /update window opener triggered by the
daemon's progress_window:show. tray.go drops three callbacks and four
fields, and reads hasUpdate through the updater.

Frontend ClientVersionContext now seeds from Update.GetState() and
subscribes to netbird:update:state; the status.events iteration and
metadata-key string literals are gone. UpdateAvailableBanner renders
only for the enforced && !installing branch and labels its action
"Install now"; UpdateVersionCard splits the install vs. download
branches by Enforced so the disabled flow routes to GitHub.
This commit is contained in:
Zoltan Papp
2026-05-15 13:21:35 +02:00
parent 1ebb507cbb
commit 9d8eb76746
9 changed files with 498 additions and 211 deletions

View File

@@ -3,13 +3,17 @@ import { Button } from "@/components/Button";
import { useClientVersion } from "@/modules/auto-update/ClientVersionContext";
import { cn } from "@/lib/cn";
// TODO: Shown only when management has auto updates enabled + there are updates available + force updates is disabled
// Shown only when management has auto-update enabled (enforced=true) and the
// daemon has not yet started the installer (installing=false). The
// download-only branch (enforced=false) routes the user to GitHub via the
// tray menu instead; the force-install branch (installing=true) takes over
// with the full-screen UpdatingOverlay.
export const UpdateAvailableBanner = () => {
const { updateVersion, triggerUpdate } = useClientVersion();
const { updateVersion, enforced, installing, triggerUpdate } = useClientVersion();
const [dismissed, setDismissed] = useState(false);
if (import.meta.env.DEV) return null;
if (!updateVersion || dismissed) return null;
if (!updateVersion || !enforced || installing || dismissed) return null;
return (
<div
@@ -22,14 +26,14 @@ export const UpdateAvailableBanner = () => {
)}
>
<p className={"text-sm text-nb-gray-900 pr-4 pl-2 font-medium"}>
NetBird will update when you restart the app.
NetBird {updateVersion} is ready to install.
</p>
<div className={"flex gap-2"}>
<Button variant={"subtle"} size={"xs"} onClick={() => setDismissed(true)}>
Later
</Button>
<Button variant={"primary"} size={"xs"} onClick={triggerUpdate}>
Restart now
Install now
</Button>
</div>
</div>