Add Electron upgrade feature with UI and backend fixes

Introduces an Electron upgrade section in settings, allowing users to check for and upgrade to the latest stable or nightly Electron versions. Implements backend logic to read the installed Electron version directly from package.json, properly handles switching between stable and nightly builds, and improves error handling and UI feedback during upgrade operations. Includes documentation of the upgrade process and bug fixes related to version display and upgrade reliability.
This commit is contained in:
2025-11-09 14:33:55 +13:00
parent 3bf0458ede
commit 691c6c8628
4 changed files with 446 additions and 1 deletions
+94
View File
@@ -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`
+144
View File
@@ -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 {
+25
View File
@@ -245,6 +245,31 @@
</ul>
</div>
<div class="customization-group">
<h3>Electron Upgrade</h3>
<div style="display: flex; flex-direction: column; gap: 12px;">
<div style="display: flex; gap: 8px; align-items: center;">
<label for="electron-version-select" style="min-width: 100px;">Select Build:</label>
<select id="electron-version-select" style="flex: 1; padding: 8px; border-radius: 4px; background-color: var(--secondary, #00C6FF); color: var(--bg, #121418); border: none; cursor: pointer;">
<option value="stable">Stable</option>
<option value="nightly">Nightly</option>
</select>
</div>
<div id="electron-versions-container" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
<span id="electron-loading" style="color: #888;">Loading available versions...</span>
<div id="electron-version-info" style="display: none;">
<span id="electron-version-text"></span>
</div>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button id="check-electron-versions">Check for Updates</button>
<button id="upgrade-electron-btn" style="display: none;" disabled>Upgrade Electron</button>
<span id="electron-status-text" style="display: none; color: #888; align-self: center;"></span>
</div>
<p class="note" style="margin-top: 8px;">Upgrading Electron will require the application to restart.</p>
</div>
</div>
<div class="customization-group about-actions">
<button id="copy-about-btn">Copy diagnostics</button>
<a id="github-link" href="https://github.com/Bobbybear007/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
+183 -1
View File
@@ -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,7 +290,188 @@ 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', () => {