diff --git a/ELECTRON_UPGRADE_FIXES.md b/ELECTRON_UPGRADE_FIXES.md new file mode 100644 index 0000000..8831828 --- /dev/null +++ b/ELECTRON_UPGRADE_FIXES.md @@ -0,0 +1,94 @@ +# Electron Upgrade Feature - Bug Fixes + +## Problem Identified +The upgrade feature was downloading and installing new Electron versions successfully, but the app always showed the old version (1.0.0) after restart because: + +1. **Version Source Issue**: The app was reading `app.getVersion()` which gets the version from `package.json` at startup time +2. **Package.json Not Re-read**: Even after npm installed a new Electron version, the app didn't re-read the updated `package.json` +3. **Runtime Display**: The About tab showed the bundled Electron version (37.x) which is baked into the binary at build time + +## Solutions Implemented + +### 1. **New Helper Function: `getInstalledElectronVersion()`** +- Reads `package.json` directly every time it's called (not cached) +- Extracts the actual installed Electron version from `devDependencies` +- Handles both stable (`electron`) and nightly (`electron-nightly`) packages +- Strips version specifiers (^, ~, etc.) to get the clean version number +- Falls back to `app.getVersion()` if reading fails + +```javascript +function getInstalledElectronVersion() { + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + const electronDep = packageJson.devDependencies?.electron; + const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; + + if (electronDep) { + return electronDep.replace(/^\D+/, ''); + } + if (electronNightlyDep) { + return electronNightlyDep.replace(/^\D+/, ''); + } + return app.getVersion(); + } catch (err) { + return app.getVersion(); + } +} +``` + +### 2. **Updated `get-electron-versions` Handler** +- Now uses `getInstalledElectronVersion()` instead of `app.getVersion()` +- Returns the actual installed version that was modified by npm +- Performs fresh version checks each time (no caching) + +### 3. **Improved `upgrade-electron` Handler** +- Increased `maxBuffer` to handle large npm output +- Added cleanup logic to remove the old Electron variant when switching types + - Removes `electron` when upgrading to `nightly` + - Removes `electron-nightly` when upgrading to `stable` +- Better error logging to debug npm failures +- Returns clearer messages about installation status + +### 4. **Enhanced UI/UX in settings.js** +- Added more descriptive status text ("Downloading and installing..." instead of just "Upgrading...") +- Disables all controls during upgrade to prevent multiple clicks +- Reduced restart delay from 2000ms to 1500ms for faster feedback +- Better error handling with proper cleanup of disabled states + +## How It Works Now + +1. **User clicks "Check for Updates"** + - Queries npm registry for latest version + - Uses `getInstalledElectronVersion()` to read current version from `package.json` + - Compares versions and shows if update is available + +2. **User clicks "Upgrade Electron"** + - Confirms action + - Runs `npm install --save-dev electron@latest` (or `electron-nightly@latest`) + - npm downloads and installs new version + - Handler removes the other Electron variant from `package.json` if needed + - Shows success message + +3. **App Restarts** + - Uses `app.relaunch()` and `app.quit()` + - When app relaunches, it: + - Loads new Electron binary from `node_modules` + - Runs new Electron version + - Settings page shows correct new version on next check + +## Testing Recommendations + +1. Test upgrading from stable to nightly version +2. Test upgrading from nightly back to stable +3. Verify version display updates after restart +4. Check that old variant is removed from `package.json` +5. Verify app runs stably with new Electron version + +## Notes for Future Development + +- The About tab displays `process.versions.electron` which is the bundled Chromium version, not the Electron framework version +- The Electron version we display in the upgrade section comes from `package.json` which is the actual framework version +- When building with electron-builder, the bundled version becomes fixed until next rebuild +- For development/testing, the upgrade feature reads live from `package.json` diff --git a/main.js b/main.js index cd0f3e2..50beb6b 100644 --- a/main.js +++ b/main.js @@ -666,6 +666,150 @@ ipcMain.handle('open-devtools', (event) => { return contents.isDevToolsOpened(); }); +// Helper function to read package.json version +function getInstalledElectronVersion() { + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Get the version from devDependencies + const electronDep = packageJson.devDependencies?.electron; + const electronNightlyDep = packageJson.devDependencies?.['electron-nightly']; + + if (electronDep) { + return electronDep.replace(/^\D+/, ''); // Remove ^ or ~ or other version specifiers + } + if (electronNightlyDep) { + return electronNightlyDep.replace(/^\D+/, ''); + } + + return app.getVersion(); + } catch (err) { + console.error('Error reading installed electron version:', err); + return app.getVersion(); + } +} + +// Electron version management handlers +ipcMain.handle('get-electron-versions', async (event, buildType = 'stable') => { + const https = require('https'); + + return new Promise((resolve) => { + let url; + + if (buildType === 'nightly') { + // Get latest nightly version from npm + url = 'https://registry.npmjs.org/electron-nightly/latest'; + } else { + // Get latest stable version from npm + url = 'https://registry.npmjs.org/electron/latest'; + } + + const request = https.get(url, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const packageInfo = JSON.parse(data); + // Get the actual installed version from package.json, not app.getVersion() + const installedVersion = getInstalledElectronVersion(); + resolve({ + available: packageInfo.version, + current: installedVersion, + buildType: buildType + }); + } catch (err) { + console.error('Failed to parse version info:', err); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: 'Failed to fetch version info' + }); + } + }); + }); + + request.on('error', (err) => { + console.error('Failed to fetch versions:', err); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: err.message + }); + }); + + request.setTimeout(5000, () => { + request.destroy(); + resolve({ + available: null, + current: getInstalledElectronVersion(), + error: 'Version check timed out' + }); + }); + }); +}); + +ipcMain.handle('upgrade-electron', async (event, buildType = 'stable') => { + const { execFile } = require('child_process'); + const packageName = buildType === 'nightly' ? 'electron-nightly' : 'electron'; + + return new Promise((resolve) => { + // First, remove the other electron package if switching types + const otherPackage = buildType === 'nightly' ? 'electron' : 'electron-nightly'; + + // Run npm install to upgrade the package + const args = ['install', '--save-dev', packageName + '@latest']; + + execFile('npm', args, + { cwd: __dirname, shell: true, maxBuffer: 10 * 1024 * 1024 }, + (error, stdout, stderr) => { + if (error) { + console.error('Upgrade failed:', error); + console.error('stderr:', stderr); + resolve({ + success: false, + error: error.message, + message: 'Failed to upgrade Electron' + }); + } else { + console.log('Upgrade output:', stdout); + console.log('Upgrade stderr:', stderr); + + // Update package.json to remove the other electron variant if needed + try { + const packageJsonPath = path.join(__dirname, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + // Remove electron if we're upgrading to nightly + if (buildType === 'nightly' && packageJson.devDependencies?.electron) { + delete packageJson.devDependencies.electron; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); + } + // Remove electron-nightly if we're upgrading to stable + else if (buildType === 'stable' && packageJson.devDependencies?.['electron-nightly']) { + delete packageJson.devDependencies['electron-nightly']; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); + } + } catch (err) { + console.warn('Could not clean up alternate electron package:', err); + } + + resolve({ + success: true, + message: 'Electron upgrade completed. Restarting application...' + }); + } + } + ); + }); +}); + +ipcMain.handle('restart-app', async (event) => { + // Quit and relaunch the app + app.relaunch(); + app.quit(); +}); + // Open local file dialog -> returns file:// URL (or null if cancelled) ipcMain.handle('show-open-file-dialog', async () => { try { diff --git a/renderer/settings.html b/renderer/settings.html index 4b2249f..8b59f73 100644 --- a/renderer/settings.html +++ b/renderer/settings.html @@ -245,6 +245,31 @@ +
+

Electron Upgrade

+
+
+ + +
+
+ Loading available versions... + +
+
+ + + +
+

Upgrading Electron will require the application to restart.

+
+
+
diff --git a/renderer/settings.js b/renderer/settings.js index d14e02d..07e9444 100644 --- a/renderer/settings.js +++ b/renderer/settings.js @@ -262,7 +262,8 @@ async function populateAbout() { byId('about-mem').textContent = `${info.totalMemGB} GB`; const copyBtn = document.getElementById('copy-about-btn'); - if (copyBtn) { + if (copyBtn && !copyBtn.dataset.listenerAttached) { + copyBtn.dataset.listenerAttached = 'true'; copyBtn.addEventListener('click', async () => { const payload = [ `Nebula ${info.appVersion} (${info.isPackaged ? 'packaged' : 'dev'})`, @@ -289,8 +290,189 @@ async function populateAbout() { // Populate about info after DOM is ready window.addEventListener('DOMContentLoaded', () => { populateAbout(); + setupElectronUpgrade(); + + // Refresh about info when About tab is clicked + const aboutTabBtn = document.getElementById('tab-about'); + if (aboutTabBtn) { + aboutTabBtn.addEventListener('click', () => { + // Refresh after a short delay to allow tab transition + setTimeout(() => { + populateAbout(); + }, 100); + }); + } }); +// Electron upgrade feature setup +async function setupElectronUpgrade() { + const versionSelect = document.getElementById('electron-version-select'); + const checkBtn = document.getElementById('check-electron-versions'); + const upgradeBtn = document.getElementById('upgrade-electron-btn'); + const loadingSpan = document.getElementById('electron-loading'); + const versionContainer = document.getElementById('electron-versions-container'); + const versionInfo = document.getElementById('electron-version-info'); + const versionText = document.getElementById('electron-version-text'); + const statusText = document.getElementById('electron-status-text'); + + if (!checkBtn || !versionSelect) return; + + let availableVersion = null; + let currentVersion = null; + + const checkVersions = async () => { + try { + if (!ipc) { + showStatus('IPC not available'); + return; + } + + checkBtn.disabled = true; + loadingSpan.style.display = 'block'; + versionInfo.style.display = 'none'; + statusText.style.display = 'none'; + statusText.textContent = ''; + + const buildType = versionSelect.value; + const result = await ipc.invoke('get-electron-versions', buildType); + + if (result.error) { + statusText.textContent = `Error: ${result.error}`; + statusText.style.display = 'block'; + showStatus(`Failed to check versions: ${result.error}`); + } else { + availableVersion = result.available; + currentVersion = result.current; + + if (availableVersion) { + versionText.textContent = `Available: ${availableVersion} | Current: ${currentVersion}`; + versionInfo.style.display = 'block'; + + // Enable upgrade button only if there's a newer version + const isNewer = compareVersions(availableVersion, currentVersion) > 0; + upgradeBtn.disabled = !isNewer; + upgradeBtn.style.display = 'block'; + + if (isNewer) { + statusText.textContent = 'Update available!'; + statusText.style.color = '#4CAF50'; + } else { + statusText.textContent = 'You are running the latest version.'; + statusText.style.color = '#888'; + } + statusText.style.display = 'block'; + showStatus(`Current: ${currentVersion} | Latest ${buildType}: ${availableVersion}`); + } + } + + checkBtn.disabled = false; + loadingSpan.style.display = 'none'; + } catch (error) { + console.error('Error checking versions:', error); + statusText.textContent = `Error: ${error.message}`; + statusText.style.display = 'block'; + showStatus('Failed to check versions'); + checkBtn.disabled = false; + loadingSpan.style.display = 'none'; + } + }; + + const handleUpgrade = async () => { + const buildType = versionSelect.value; + if (!availableVersion) { + showStatus('No version information available'); + return; + } + + const confirmed = confirm( + `Upgrade Electron from ${currentVersion} to ${availableVersion} (${buildType})?\n\nThe application will restart automatically.` + ); + + if (!confirmed) return; + + try { + upgradeBtn.disabled = true; + checkBtn.disabled = true; + versionSelect.disabled = true; + statusText.textContent = 'Downloading and installing...'; + statusText.style.color = '#FFC107'; + statusText.style.display = 'block'; + showStatus('Starting Electron upgrade...'); + + const result = await ipc.invoke('upgrade-electron', buildType); + + if (result.success) { + statusText.textContent = result.message; + statusText.style.color = '#4CAF50'; + showStatus(result.message); + + // Restart the app after a short delay + setTimeout(() => { + if (ipc) { + ipc.invoke('restart-app').catch(err => console.error('Restart failed:', err)); + } + }, 1500); + } else { + statusText.textContent = `Failed: ${result.error}`; + statusText.style.color = '#F44336'; + showStatus(`Upgrade failed: ${result.error}`); + upgradeBtn.disabled = false; + checkBtn.disabled = false; + versionSelect.disabled = false; + } + } catch (error) { + console.error('Upgrade error:', error); + statusText.textContent = `Error: ${error.message}`; + statusText.style.color = '#F44336'; + statusText.style.display = 'block'; + showStatus(`Upgrade error: ${error.message}`); + upgradeBtn.disabled = false; + checkBtn.disabled = false; + versionSelect.disabled = false; + } + }; + + checkBtn.addEventListener('click', checkVersions); + upgradeBtn.addEventListener('click', handleUpgrade); + + versionSelect.addEventListener('change', () => { + // Reset UI when build type changes + versionInfo.style.display = 'none'; + upgradeBtn.style.display = 'none'; + upgradeBtn.disabled = true; + statusText.style.display = 'none'; + loadingSpan.style.display = 'block'; + availableVersion = null; + }); + + // Auto-refresh about tab and electron versions when this section comes into view + const aboutTabBtn = document.getElementById('tab-about'); + if (aboutTabBtn) { + aboutTabBtn.addEventListener('click', () => { + setTimeout(() => { + // Refresh about info when About tab is clicked + populateAbout(); + // Also refresh electron versions display + checkVersions(); + }, 100); + }); + } +} + +// Helper function to compare semantic versions +function compareVersions(v1, v2) { + const parts1 = v1.split('-')[0].split('.').map(x => parseInt(x, 10)); + const parts2 = v2.split('-')[0].split('.').map(x => parseInt(x, 10)); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + if (p1 > p2) return 1; + if (p1 < p2) return -1; + } + return 0; +} + // Keep settings open when clicking GitHub by asking host to open externally/new tab window.addEventListener('DOMContentLoaded', () => { const gh = document.getElementById('github-link');