| author | |
| committer | |
| log | 32d89806d7b17dbcffa4f02b46b24b410bf54b5e |
| tree | 634b843f15891c1a0e304ce49617b0d9a07ce1b1 |
| parent | 2fd4e06b345e6edbe12f1de456bb3f057c2b14f0 |
- Add `updateState` helpers for auto-update eligibility reasons and progress throttling
- Show a user-facing dialog when manual update checks are unavailable
- Ensure manual update checks can open a window when none exist
- Add tests for disable-reason logic and 10% download progress bucketing3 files changed, 158 insertions(+), 17 deletions(-)
apps/desktop/src/main.ts+38-17| ... | @@ -15,6 +15,7 @@ import { NetService } from "@t3tools/shared/Net"; | ... | @@ -15,6 +15,7 @@ import { NetService } from "@t3tools/shared/Net"; |
| 15 | import { RotatingFileSink } from "@t3tools/shared/logging"; | 15 | import { RotatingFileSink } from "@t3tools/shared/logging"; |
| 16 | import { showDesktopConfirmDialog } from "./confirmDialog"; | 16 | import { showDesktopConfirmDialog } from "./confirmDialog"; |
| 17 | import { fixPath } from "./fixPath"; | 17 | import { fixPath } from "./fixPath"; |
| 18 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; | ||
| 18 | 19 | ||
| 19 | fixPath(); | 20 | fixPath(); |
| 20 | 21 | ||
| ... | @@ -425,11 +426,28 @@ function dispatchMenuAction(action: string): void { | ... | @@ -425,11 +426,28 @@ function dispatchMenuAction(action: string): void { |
| 425 | } | 426 | } |
| 426 | 427 | ||
| 427 | function handleCheckForUpdatesMenuClick(): void { | 428 | function handleCheckForUpdatesMenuClick(): void { |
| 428 | if (!shouldEnableAutoUpdates()) { | 429 | const disabledReason = getAutoUpdateDisabledReason({ |
| 430 | isDevelopment, | ||
| 431 | isPackaged: app.isPackaged, | ||
| 432 | platform: process.platform, | ||
| 433 | appImage: process.env.APPIMAGE, | ||
| 434 | disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", | ||
| 435 | }); | ||
| 436 | if (disabledReason) { | ||
| 429 | console.info("[desktop-updater] Manual update check requested, but updates are disabled."); | 437 | console.info("[desktop-updater] Manual update check requested, but updates are disabled."); |
| 438 | void dialog.showMessageBox({ | ||
| 439 | type: "info", | ||
| 440 | title: "Updates unavailable", | ||
| 441 | message: "Automatic updates are not available right now.", | ||
| 442 | detail: disabledReason, | ||
| 443 | buttons: ["OK"], | ||
| 444 | }); | ||
| 430 | return; | 445 | return; |
| 431 | } | 446 | } |
| 432 | 447 | ||
| 448 | if (!BrowserWindow.getAllWindows().length) { | ||
| 449 | mainWindow = createWindow(); | ||
| 450 | } | ||
| 433 | void checkForUpdates("menu"); | 451 | void checkForUpdates("menu"); |
| 434 | } | 452 | } |
| 435 | 453 | ||
| ... | @@ -562,17 +580,15 @@ function setUpdateState(patch: Partial<DesktopUpdateState>): void { | ... | @@ -562,17 +580,15 @@ function setUpdateState(patch: Partial<DesktopUpdateState>): void { |
| 562 | } | 580 | } |
| 563 | 581 | ||
| 564 | function shouldEnableAutoUpdates(): boolean { | 582 | function shouldEnableAutoUpdates(): boolean { |
| 565 | if (isDevelopment || !app.isPackaged) { | 583 | return ( |
| 566 | return false; | 584 | getAutoUpdateDisabledReason({ |
| 567 | } | 585 | isDevelopment, |
| 568 | if (process.env.T3CODE_DISABLE_AUTO_UPDATE === "1") { | 586 | isPackaged: app.isPackaged, |
| 569 | return false; | 587 | platform: process.platform, |
| 570 | } | 588 | appImage: process.env.APPIMAGE, |
| 571 | if (process.platform === "linux" && !process.env.APPIMAGE) { | 589 | disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", |
| 572 | // electron-updater only supports Linux auto-updates when running from AppImage. | 590 | }) === null |
| 573 | return false; | 591 | ); |
| 574 | } | ||
| 575 | return true; | ||
| 576 | } | 592 | } |
| 577 | 593 | ||
| 578 | async function checkForUpdates(reason: string): Promise<void> { | 594 | async function checkForUpdates(reason: string): Promise<void> { |
| ... | @@ -719,11 +735,16 @@ function configureAutoUpdater(): void { | ... | @@ -719,11 +735,16 @@ function configureAutoUpdater(): void { |
| 719 | }); | 735 | }); |
| 720 | autoUpdater.on("download-progress", (progress) => { | 736 | autoUpdater.on("download-progress", (progress) => { |
| 721 | const percent = Math.floor(progress.percent); | 737 | const percent = Math.floor(progress.percent); |
| 722 | setUpdateState({ | 738 | if ( |
| 723 | status: "downloading", | 739 | shouldBroadcastDownloadProgress(updateState, progress.percent) || |
| 724 | downloadPercent: progress.percent, | 740 | updateState.message !== null |
| 725 | message: null, | 741 | ) { |
| 726 | }); | 742 | setUpdateState({ |
| 743 | status: "downloading", | ||
| 744 | downloadPercent: progress.percent, | ||
| 745 | message: null, | ||
| 746 | }); | ||
| 747 | } | ||
| 727 | const milestone = percent - (percent % 10); | 748 | const milestone = percent - (percent % 10); |
| 728 | if (milestone > lastLoggedDownloadMilestone) { | 749 | if (milestone > lastLoggedDownloadMilestone) { |
| 729 | lastLoggedDownloadMilestone = milestone; | 750 | lastLoggedDownloadMilestone = milestone; |
apps/desktop/src/updateState.test.ts created+82| ... | @@ -0,0 +1,82 @@ | ||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | import type { DesktopUpdateState } from "@t3tools/contracts"; | ||
| 3 | |||
| 4 | import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; | ||
| 5 | |||
| 6 | const baseState: DesktopUpdateState = { | ||
| 7 | enabled: true, | ||
| 8 | status: "idle", | ||
| 9 | currentVersion: "1.0.0", | ||
| 10 | availableVersion: null, | ||
| 11 | downloadedVersion: null, | ||
| 12 | downloadPercent: null, | ||
| 13 | checkedAt: null, | ||
| 14 | message: null, | ||
| 15 | }; | ||
| 16 | |||
| 17 | describe("shouldBroadcastDownloadProgress", () => { | ||
| 18 | it("broadcasts the first downloading progress update", () => { | ||
| 19 | expect( | ||
| 20 | shouldBroadcastDownloadProgress( | ||
| 21 | { ...baseState, status: "downloading", downloadPercent: null }, | ||
| 22 | 1, | ||
| 23 | ), | ||
| 24 | ).toBe(true); | ||
| 25 | }); | ||
| 26 | |||
| 27 | it("skips progress updates within the same 10% bucket", () => { | ||
| 28 | expect( | ||
| 29 | shouldBroadcastDownloadProgress( | ||
| 30 | { ...baseState, status: "downloading", downloadPercent: 11.2 }, | ||
| 31 | 18.7, | ||
| 32 | ), | ||
| 33 | ).toBe(false); | ||
| 34 | }); | ||
| 35 | |||
| 36 | it("broadcasts progress updates when a new 10% bucket is reached", () => { | ||
| 37 | expect( | ||
| 38 | shouldBroadcastDownloadProgress( | ||
| 39 | { ...baseState, status: "downloading", downloadPercent: 19.9 }, | ||
| 40 | 20.1, | ||
| 41 | ), | ||
| 42 | ).toBe(true); | ||
| 43 | }); | ||
| 44 | }); | ||
| 45 | |||
| 46 | describe("getAutoUpdateDisabledReason", () => { | ||
| 47 | it("reports development builds as disabled", () => { | ||
| 48 | expect( | ||
| 49 | getAutoUpdateDisabledReason({ | ||
| 50 | isDevelopment: true, | ||
| 51 | isPackaged: false, | ||
| 52 | platform: "darwin", | ||
| 53 | appImage: undefined, | ||
| 54 | disabledByEnv: false, | ||
| 55 | }), | ||
| 56 | ).toContain("packaged production builds"); | ||
| 57 | }); | ||
| 58 | |||
| 59 | it("reports env-disabled auto updates", () => { | ||
| 60 | expect( | ||
| 61 | getAutoUpdateDisabledReason({ | ||
| 62 | isDevelopment: false, | ||
| 63 | isPackaged: true, | ||
| 64 | platform: "darwin", | ||
| 65 | appImage: undefined, | ||
| 66 | disabledByEnv: true, | ||
| 67 | }), | ||
| 68 | ).toContain("T3CODE_DISABLE_AUTO_UPDATE"); | ||
| 69 | }); | ||
| 70 | |||
| 71 | it("reports linux non-AppImage builds as disabled", () => { | ||
| 72 | expect( | ||
| 73 | getAutoUpdateDisabledReason({ | ||
| 74 | isDevelopment: false, | ||
| 75 | isPackaged: true, | ||
| 76 | platform: "linux", | ||
| 77 | appImage: undefined, | ||
| 78 | disabledByEnv: false, | ||
| 79 | }), | ||
| 80 | ).toContain("AppImage"); | ||
| 81 | }); | ||
| 82 | }); | ||
apps/desktop/src/updateState.ts created+38| ... | @@ -0,0 +1,38 @@ | ||
| 1 | import type { DesktopUpdateState } from "@t3tools/contracts"; | ||
| 2 | |||
| 3 | export function shouldBroadcastDownloadProgress( | ||
| 4 | currentState: DesktopUpdateState, | ||
| 5 | nextPercent: number, | ||
| 6 | ): boolean { | ||
| 7 | if (currentState.status !== "downloading") { | ||
| 8 | return true; | ||
| 9 | } | ||
| 10 | |||
| 11 | const currentPercent = currentState.downloadPercent; | ||
| 12 | if (currentPercent === null) { | ||
| 13 | return true; | ||
| 14 | } | ||
| 15 | |||
| 16 | const previousStep = Math.floor(currentPercent / 10); | ||
| 17 | const nextStep = Math.floor(nextPercent / 10); | ||
| 18 | return nextStep > previousStep || nextPercent === 100; | ||
| 19 | } | ||
| 20 | |||
| 21 | export function getAutoUpdateDisabledReason(args: { | ||
| 22 | isDevelopment: boolean; | ||
| 23 | isPackaged: boolean; | ||
| 24 | platform: NodeJS.Platform; | ||
| 25 | appImage?: string | undefined; | ||
| 26 | disabledByEnv: boolean; | ||
| 27 | }): string | null { | ||
| 28 | if (args.isDevelopment || !args.isPackaged) { | ||
| 29 | return "Automatic updates are only available in packaged production builds."; | ||
| 30 | } | ||
| 31 | if (args.disabledByEnv) { | ||
| 32 | return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; | ||
| 33 | } | ||
| 34 | if (args.platform === "linux" && !args.appImage) { | ||
| 35 | return "Automatic updates on Linux require running the AppImage build."; | ||
| 36 | } | ||
| 37 | return null; | ||
| 38 | } | ||