// Desktop auto-update — prompt-on-update flow. // // Runs ONLY inside the Tauri desktop shell; a plain browser has no updater, so // this is a guarded no-op there. On launch it checks the configured update // endpoint (tauri.conf.json → plugins.updater); if a signed newer version is // available it asks the operator, then downloads + installs and relaunches. // // The plugins are imported dynamically so the browser build never bundles them // and never tries to resolve the Tauri APIs. Offline-first: a failed check (no // network — the appliance is usually offline) is swallowed; updates only happen // when someone has brought the box online (e.g. a phone hotspot) on purpose. // // A release build's console.error is invisible with no way to attach devtools // in the field (kiosk mode blocks the context menu; this WebKitGTK build's // remote inspector doesn't answer standard discovery endpoints either — both // confirmed dead ends 2026-09-03). logClient() ships straight to the // server-side app_logs store regardless of the client's console-forward log // level (that gate is meant for noisy console chatter, not this), so a real // post-accept install failure is visible via wiki/concepts/app-logs.md / // LogsViewer.tsx without needing a terminal or devtools at all. import { logClient } from "./logger.js"; import { inTauri } from "./tauri-env.js"; export interface UpdatePrompt { /** Newer version string offered by the server. */ version: string; /** Release notes, if the server provided them. */ notes?: string; } /** * Check for an update. If one is available, calls `confirm` (your UI) with the * version/notes; when it resolves true, downloads + installs and relaunches. * No-op (resolves silently) in the browser or when no update / offline. */ export async function checkForDesktopUpdate( confirm: (info: UpdatePrompt) => Promise, ): Promise { if (!inTauri()) return; try { const { check } = await import("@tauri-apps/plugin-updater"); const update = await check(); if (!update) return; // up to date const accepted = await confirm({ version: update.version, notes: update.body }); if (!accepted) return; // Download + install the signed update (signature verified against the // pubkey in tauri.conf.json), then relaunch into the new version. try { await update.downloadAndInstall((progress) => { logClient({ level: "info", message: `desktop update download progress: ${progress.event}`, context: { kind: "desktop_update_progress", version: update.version, event: progress.event }, }); }); } catch (err) { // A real update WAS found and accepted — this is a genuine install // failure (bad signature, corrupted download, disk/permission issue), // not "offline". Surface it instead of silently reverting to the old // version with no explanation. logClient({ level: "error", message: `desktop update download/install failed: ${err instanceof Error ? err.message : String(err)}`, stack: err instanceof Error ? err.stack : undefined, context: { kind: "desktop_update_install_failed", version: update.version }, }); throw err; } const { relaunch } = await import("@tauri-apps/plugin-process"); await relaunch(); } catch (err) { // Offline / endpoint unreachable / no update server yet → ignore. The app // keeps running on the current version; checking again next launch. Still // log it (info, not error — this path is expected/normal far more often // than it's a real problem) so a real install failure (rethrown above, // logged as error) isn't lost among routine offline checks. logClient({ level: "info", message: `desktop update check/apply skipped: ${err instanceof Error ? err.message : String(err)}`, context: { kind: "desktop_update_skipped" }, }); } }