diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5f8216a..f7aeef8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,7 +36,7 @@ jobs:
dist/*.deb
dist/*.rpm
dist/*.pacman
- dist/latest.yml
+ dist/latest*.yml
build-windows:
runs-on: windows-latest
@@ -115,4 +115,4 @@ jobs:
generate_release_notes: true
draft: true
# DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push)
- prerelease: ${{ github.ref_type != 'tag' }}
+ prerelease: ${{ github.ref_type != 'tag' }}
\ No newline at end of file
diff --git a/GUI/js/update.js b/GUI/js/update.js
index bd1403c..9f78f80 100644
--- a/GUI/js/update.js
+++ b/GUI/js/update.js
@@ -23,6 +23,10 @@ class ClientUpdateManager {
this.showUpdateDownloaded(updateInfo);
});
+ window.electronAPI.onUpdateError((errorInfo) => {
+ this.handleUpdateError(errorInfo);
+ });
+
this.checkForUpdatesOnDemand();
}
@@ -57,6 +61,10 @@ class ClientUpdateManager {
@@ -161,7 +169,6 @@ class ClientUpdateManager {
const progressPercent = document.getElementById('update-progress-percent');
const progressSpeed = document.getElementById('update-progress-speed');
const progressSize = document.getElementById('update-progress-size');
- const statusText = document.getElementById('update-status-text');
if (progressBar && progress) {
const percent = Math.round(progress.percent || 0);
@@ -182,9 +189,7 @@ class ClientUpdateManager {
progressSize.textContent = `${transferredMB} MB / ${totalMB} MB`;
}
- if (statusText) {
- statusText.textContent = `Downloading update... ${percent}%`;
- }
+ // Don't update status text here - it's already set and the progress bar shows the percentage
}
}
@@ -208,6 +213,83 @@ class ClientUpdateManager {
console.log('✅ Update downloaded, ready to install');
}
+ handleUpdateError(errorInfo) {
+ console.error('Update error:', errorInfo);
+
+ // If manual download is required, update the UI (this will handle status text)
+ if (errorInfo.requiresManualDownload) {
+ this.showManualDownloadRequired(errorInfo);
+ return; // Don't do anything else, showManualDownloadRequired handles everything
+ }
+
+ // For non-critical errors, just show error message without changing status
+ const errorMessage = document.getElementById('update-error-message');
+ const errorText = document.getElementById('update-error-text');
+
+ if (errorMessage && errorText) {
+ let message = errorInfo.message || 'An error occurred during the update process.';
+ if (errorInfo.isMacSigningError) {
+ message = 'Auto-update requires code signing. Please download manually.';
+ }
+ errorText.textContent = message;
+ errorMessage.style.display = 'block';
+ }
+ }
+
+ showManualDownloadRequired(errorInfo) {
+ const statusText = document.getElementById('update-status-text');
+ const progressContainer = document.getElementById('update-progress-container');
+ const buttonsContainer = document.getElementById('update-buttons-container');
+ const installBtn = document.getElementById('update-install-btn');
+ const downloadBtn = document.getElementById('update-download-btn');
+ const errorMessage = document.getElementById('update-error-message');
+ const errorText = document.getElementById('update-error-text');
+
+ // Hide progress and install button
+ if (progressContainer) {
+ progressContainer.style.display = 'none';
+ }
+
+ if (installBtn) {
+ installBtn.style.display = 'none';
+ }
+
+ // Update status message (only once, don't change it again)
+ if (statusText && !statusText.dataset.manualMode) {
+ statusText.textContent = 'Please download and install the update manually.';
+ statusText.dataset.manualMode = 'true'; // Mark that we've set manual mode
+ }
+
+ // Show error message with details
+ if (errorMessage && errorText) {
+ let message = 'Auto-update is not available. ';
+ if (errorInfo.isMacSigningError) {
+ message = 'This app requires code signing for automatic updates.';
+ } else if (errorInfo.message) {
+ message = errorInfo.message;
+ } else {
+ message = 'An error occurred during the update process.';
+ }
+ errorText.textContent = message;
+ errorMessage.style.display = 'block';
+ }
+
+ // Show and enable the manual download button (make it primary since it's the only option)
+ if (downloadBtn) {
+ downloadBtn.style.display = 'block';
+ downloadBtn.disabled = false;
+ downloadBtn.classList.remove('update-download-btn-secondary');
+ downloadBtn.innerHTML = 'Download Update Manually';
+ }
+
+ // Show buttons container if not already visible
+ if (buttonsContainer) {
+ buttonsContainer.style.display = 'block';
+ }
+
+ console.log('⚠️ Manual download required due to update error');
+ }
+
blockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
diff --git a/backend/appUpdater.js b/backend/appUpdater.js
index 6bd95d7..cc87efd 100644
--- a/backend/appUpdater.js
+++ b/backend/appUpdater.js
@@ -5,6 +5,9 @@ const logger = require('./logger');
class AppUpdater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
+ this.autoUpdateAvailable = true; // Track if auto-update is possible
+ this.updateAvailable = false; // Track if an update was detected
+ this.updateVersion = null; // Store the available update version
this.setupAutoUpdater();
}
@@ -40,6 +43,10 @@ class AppUpdater {
autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version);
+ this.updateAvailable = true;
+ this.updateVersion = info.version;
+ this.autoUpdateAvailable = true; // Reset flag when new update is available
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-available', {
version: info.version,
@@ -68,9 +75,68 @@ class AppUpdater {
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
+
+ // Check if this is a network error (not critical, don't show UI)
+ const errorMessage = err.message?.toLowerCase() || '';
+ const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
+ errorMessage.includes('network') ||
+ errorMessage.includes('connection') ||
+ errorMessage.includes('timeout') ||
+ errorMessage.includes('enotfound');
+
+ if (isNetworkError) {
+ console.warn('Network error in auto-updater - will retry later. Not showing error UI.');
+ return; // Don't show error UI for network issues
+ }
+
+ // Determine if this is a critical error that prevents auto-update
+ const isCriticalError = this.isCriticalUpdateError(err);
+
+ if (isCriticalError) {
+ this.autoUpdateAvailable = false;
+ console.warn('Auto-update failed. Manual download required.');
+ }
+
+ // Handle missing metadata files (platform-specific builds)
+ if (err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND') {
+ const platform = process.platform === 'darwin' ? 'macOS' :
+ process.platform === 'win32' ? 'Windows' : 'Linux';
+ const missingFile = process.platform === 'darwin' ? 'latest-mac.yml' :
+ process.platform === 'win32' ? 'latest.yml' : 'latest-linux.yml';
+ console.warn(`${platform} update metadata file (${missingFile}) not found in release.`);
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
+ this.mainWindow.webContents.send('update-error', {
+ message: `Update metadata file for ${platform} not found in release. Please download manually.`,
+ code: err.code,
+ requiresManualDownload: true,
+ updateVersion: this.updateVersion,
+ isMissingMetadata: true
+ });
+ }
+ return;
+ }
+
+ // macOS-specific: Handle unsigned app errors gracefully
+ if (process.platform === 'darwin' && err.code === 2) {
+ console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
+ this.mainWindow.webContents.send('update-error', {
+ message: 'Auto-update requires code signing. Please download manually from GitHub.',
+ code: err.code,
+ isMacSigningError: true,
+ requiresManualDownload: true,
+ updateVersion: this.updateVersion
+ });
+ }
+ return;
+ }
+
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
- message: err.message
+ message: err.message,
+ code: err.code,
+ requiresManualDownload: isCriticalError,
+ updateVersion: this.updateVersion
});
}
});
@@ -104,12 +170,55 @@ class AppUpdater {
// Check for updates and notify if available
autoUpdater.checkForUpdatesAndNotify().catch(err => {
console.error('Failed to check for updates:', err);
+
+ // Network errors are not critical - just log and continue
+ const errorMessage = err.message?.toLowerCase() || '';
+ const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
+ errorMessage.includes('network') ||
+ errorMessage.includes('connection') ||
+ errorMessage.includes('timeout') ||
+ errorMessage.includes('enotfound');
+
+ if (isNetworkError) {
+ console.warn('Network error checking for updates - will retry later. This is not critical.');
+ return; // Don't show error UI for network issues
+ }
+
+ const isCritical = this.isCriticalUpdateError(err);
+ if (this.mainWindow && !this.mainWindow.isDestroyed() && isCritical) {
+ this.mainWindow.webContents.send('update-error', {
+ message: err.message || 'Failed to check for updates',
+ code: err.code,
+ requiresManualDownload: true
+ });
+ }
});
}
checkForUpdates() {
// Manual check for updates (returns promise)
- return autoUpdater.checkForUpdates();
+ return autoUpdater.checkForUpdates().catch(err => {
+ console.error('Failed to check for updates:', err);
+
+ // Network errors are not critical - just return no update available
+ const errorMessage = err.message?.toLowerCase() || '';
+ const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
+ errorMessage.includes('network') ||
+ errorMessage.includes('connection') ||
+ errorMessage.includes('timeout') ||
+ errorMessage.includes('enotfound');
+
+ if (isNetworkError) {
+ console.warn('Network error - update check unavailable');
+ return { updateInfo: null }; // Return empty result for network errors
+ }
+
+ const isCritical = this.isCriticalUpdateError(err);
+ if (isCritical) {
+ this.autoUpdateAvailable = false;
+ }
+ throw err;
+ });
}
quitAndInstall() {
@@ -123,6 +232,59 @@ class AppUpdater {
updateAvailable: false
};
}
+
+ isCriticalUpdateError(err) {
+ // Check for errors that prevent auto-update
+ const errorMessage = err.message?.toLowerCase() || '';
+ const errorCode = err.code;
+
+ // Missing update metadata files (platform-specific)
+ if (errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' ||
+ errorMessage.includes('cannot find latest') ||
+ errorMessage.includes('latest-linux.yml') ||
+ errorMessage.includes('latest-mac.yml') ||
+ errorMessage.includes('latest.yml')) {
+ return true;
+ }
+
+ // macOS code signing errors
+ if (process.platform === 'darwin' && (errorCode === 2 || errorMessage.includes('shipit'))) {
+ return true;
+ }
+
+ // Download failures
+ if (errorMessage.includes('download') && errorMessage.includes('fail')) {
+ return true;
+ }
+
+ // Network errors that prevent download (but we handle these separately as non-critical)
+ // Installation errors
+ if (errorMessage.includes('install') && errorMessage.includes('fail')) {
+ return true;
+ }
+
+ // Permission errors
+ if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
+ return true;
+ }
+
+ // File system errors (but not "not found" for metadata files - handled above)
+ if (errorMessage.includes('enoent') || errorMessage.includes('cannot find')) {
+ // Only if it's not about metadata files
+ if (!errorMessage.includes('latest') && !errorMessage.includes('.yml')) {
+ return true;
+ }
+ }
+
+ // Generic critical error codes
+ if (errorCode && (errorCode >= 100 ||
+ errorCode === 'ERR_UPDATER_INVALID_RELEASE_FEED' ||
+ errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')) {
+ return true;
+ }
+
+ return false;
+ }
}
module.exports = AppUpdater;
diff --git a/preload.js b/preload.js
index 72c4236..bc6e86e 100644
--- a/preload.js
+++ b/preload.js
@@ -60,6 +60,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
onUpdateDownloaded: (callback) => {
ipcRenderer.on('update-downloaded', (event, data) => callback(data));
},
+ onUpdateError: (callback) => {
+ ipcRenderer.on('update-error', (event, data) => callback(data));
+ },
quitAndInstallUpdate: () => ipcRenderer.invoke('quit-and-install-update'),
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),