mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
feat: add web-based update management system
- Created UpdateManager class for handling application updates - Added update check, download, and apply functionality - Integrated update UI into settings modal with progress tracking - Added version display and update notifications - Implemented proper backup and rollback mechanism - Added Docker-aware restart logic - Enhanced configuration API to include version information - Added real-time progress updates via WebSocket
This commit is contained in:
@@ -50,6 +50,9 @@ RUN chown -R appuser:appgroup /usr/src/app
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Set environment variable to indicate Docker deployment
|
||||
ENV DOCKER_DEPLOYMENT=true
|
||||
|
||||
# Expose port
|
||||
EXPOSE 7655
|
||||
|
||||
|
||||
+3
-1
@@ -14,9 +14,11 @@ class ConfigApi {
|
||||
async getConfig() {
|
||||
try {
|
||||
const config = await this.readEnvFile();
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// Build the response structure including all additional endpoints
|
||||
const response = {
|
||||
version: packageJson.version,
|
||||
proxmox: config.PROXMOX_HOST ? {
|
||||
host: config.PROXMOX_HOST,
|
||||
port: config.PROXMOX_PORT || '8006',
|
||||
@@ -68,7 +70,7 @@ class ConfigApi {
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error reading configuration:', error);
|
||||
return { proxmox: null, pbs: null, advanced: {} };
|
||||
return { version: 'unknown', proxmox: null, pbs: null, advanced: {} };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,73 @@ app.get('/setup.html', (req, res) => {
|
||||
// Set up configuration API routes
|
||||
configApi.setupRoutes(app);
|
||||
|
||||
// Set up update API routes
|
||||
const UpdateManager = require('./updateManager');
|
||||
const updateManager = new UpdateManager();
|
||||
|
||||
// Check for updates endpoint
|
||||
app.get('/api/updates/check', async (req, res) => {
|
||||
try {
|
||||
const updateInfo = await updateManager.checkForUpdates();
|
||||
res.json(updateInfo);
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Download and apply update endpoint
|
||||
app.post('/api/updates/apply', async (req, res) => {
|
||||
try {
|
||||
const { downloadUrl } = req.body;
|
||||
|
||||
if (!downloadUrl) {
|
||||
return res.status(400).json({ error: 'Download URL is required' });
|
||||
}
|
||||
|
||||
// Send immediate response
|
||||
res.json({
|
||||
message: 'Update started. The application will restart automatically when complete.',
|
||||
status: 'in_progress'
|
||||
});
|
||||
|
||||
// Apply update in background
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Download update
|
||||
const updateFile = await updateManager.downloadUpdate(downloadUrl, (progress) => {
|
||||
io.emit('updateProgress', progress);
|
||||
});
|
||||
|
||||
// Apply update
|
||||
await updateManager.applyUpdate(updateFile, (progress) => {
|
||||
io.emit('updateProgress', progress);
|
||||
});
|
||||
|
||||
io.emit('updateComplete', { success: true });
|
||||
} catch (error) {
|
||||
console.error('Error applying update:', error);
|
||||
io.emit('updateError', { error: error.message });
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initiating update:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update status endpoint
|
||||
app.get('/api/updates/status', (req, res) => {
|
||||
try {
|
||||
const status = updateManager.getUpdateStatus();
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
console.error('Error getting update status:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/healthz', (req, res) => {
|
||||
res.status(200).send('OK');
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
const axios = require('axios');
|
||||
const semver = require('semver');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
class UpdateManager {
|
||||
constructor() {
|
||||
this.githubRepo = 'rcourtman/Pulse';
|
||||
this.currentVersion = require('../package.json').version;
|
||||
this.updateInProgress = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for available updates
|
||||
*/
|
||||
async checkForUpdates() {
|
||||
try {
|
||||
console.log('[UpdateManager] Checking for updates...');
|
||||
|
||||
// Fetch latest release from GitHub
|
||||
const response = await axios.get(
|
||||
`https://api.github.com/repos/${this.githubRepo}/releases/latest`,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'Pulse-Update-Checker'
|
||||
},
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
const latestVersion = response.data.tag_name.replace('v', '');
|
||||
const updateAvailable = semver.gt(latestVersion, this.currentVersion);
|
||||
|
||||
const updateInfo = {
|
||||
currentVersion: this.currentVersion,
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
releaseNotes: response.data.body || 'No release notes available',
|
||||
releaseUrl: response.data.html_url,
|
||||
publishedAt: response.data.published_at,
|
||||
assets: response.data.assets.map(asset => ({
|
||||
name: asset.name,
|
||||
size: asset.size,
|
||||
downloadUrl: asset.browser_download_url
|
||||
}))
|
||||
};
|
||||
|
||||
console.log(`[UpdateManager] Current version: ${this.currentVersion}, Latest version: ${latestVersion}`);
|
||||
return updateInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UpdateManager] Error checking for updates:', error.message);
|
||||
throw new Error(`Failed to check for updates: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download update package
|
||||
*/
|
||||
async downloadUpdate(downloadUrl, progressCallback) {
|
||||
try {
|
||||
console.log('[UpdateManager] Downloading update from:', downloadUrl);
|
||||
|
||||
const tempDir = path.join(__dirname, '..', 'temp');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
const tempFile = path.join(tempDir, 'update.tar.gz');
|
||||
|
||||
// Download with progress tracking
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: downloadUrl,
|
||||
responseType: 'stream',
|
||||
timeout: 300000 // 5 minutes
|
||||
});
|
||||
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloadedSize = 0;
|
||||
|
||||
const writer = require('fs').createWriteStream(tempFile);
|
||||
|
||||
response.data.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (progressCallback) {
|
||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||
progressCallback({ phase: 'download', progress });
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve(tempFile));
|
||||
writer.on('error', reject);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UpdateManager] Error downloading update:', error.message);
|
||||
throw new Error(`Failed to download update: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply update
|
||||
*/
|
||||
async applyUpdate(updateFile, progressCallback) {
|
||||
if (this.updateInProgress) {
|
||||
throw new Error('Update already in progress');
|
||||
}
|
||||
|
||||
this.updateInProgress = true;
|
||||
|
||||
try {
|
||||
console.log('[UpdateManager] Applying update...');
|
||||
|
||||
// Create backup directory
|
||||
const backupDir = path.join(__dirname, '..', 'backup', `backup-${Date.now()}`);
|
||||
await fs.mkdir(backupDir, { recursive: true });
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({ phase: 'backup', progress: 0 });
|
||||
}
|
||||
|
||||
// Backup critical files
|
||||
const filesToBackup = [
|
||||
'.env',
|
||||
'data/metrics.db',
|
||||
'data/acknowledgements.json'
|
||||
];
|
||||
|
||||
for (let i = 0; i < filesToBackup.length; i++) {
|
||||
const file = filesToBackup[i];
|
||||
const sourcePath = path.join(__dirname, '..', file);
|
||||
const backupPath = path.join(backupDir, file);
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.dirname(backupPath), { recursive: true });
|
||||
await fs.copyFile(sourcePath, backupPath);
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.warn(`[UpdateManager] Warning: Could not backup ${file}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
const progress = Math.round(((i + 1) / filesToBackup.length) * 100);
|
||||
progressCallback({ phase: 'backup', progress });
|
||||
}
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({ phase: 'extract', progress: 0 });
|
||||
}
|
||||
|
||||
// Extract update
|
||||
const tempExtractDir = path.join(__dirname, '..', 'temp', 'extract');
|
||||
await fs.mkdir(tempExtractDir, { recursive: true });
|
||||
|
||||
await execAsync(`tar -xzf ${updateFile} -C ${tempExtractDir}`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({ phase: 'extract', progress: 100 });
|
||||
}
|
||||
|
||||
// Detect deployment type
|
||||
const isDocker = process.env.DOCKER_DEPLOYMENT === 'true' || require('fs').existsSync('/.dockerenv');
|
||||
const pulseDir = path.join(__dirname, '..');
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({ phase: 'apply', progress: 50 });
|
||||
}
|
||||
|
||||
// Apply update files
|
||||
console.log('[UpdateManager] Extracting update files...');
|
||||
|
||||
// List files to update (exclude config and data)
|
||||
const updateFiles = await fs.readdir(tempExtractDir);
|
||||
for (const file of updateFiles) {
|
||||
if (file === '.env' || file === 'data') continue;
|
||||
|
||||
const sourcePath = path.join(tempExtractDir, file);
|
||||
const destPath = path.join(pulseDir, file);
|
||||
|
||||
// Remove existing file/directory
|
||||
try {
|
||||
await fs.rm(destPath, { recursive: true, force: true });
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Copy new file/directory
|
||||
await execAsync(`cp -rf "${sourcePath}" "${destPath}"`);
|
||||
}
|
||||
|
||||
// Install dependencies
|
||||
console.log('[UpdateManager] Installing dependencies...');
|
||||
await execAsync(`cd "${pulseDir}" && npm ci --production`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({ phase: 'apply', progress: 100 });
|
||||
}
|
||||
|
||||
// Schedule restart
|
||||
console.log('[UpdateManager] Scheduling restart...');
|
||||
setTimeout(() => {
|
||||
if (isDocker) {
|
||||
// In Docker, just exit - container will be restarted
|
||||
console.log('[UpdateManager] Exiting for Docker restart...');
|
||||
process.exit(0);
|
||||
} else {
|
||||
// For systemd/manual deployments, try to restart
|
||||
console.log('[UpdateManager] Attempting restart...');
|
||||
|
||||
// Try systemctl first
|
||||
execAsync('sudo systemctl restart pulse').catch(() => {
|
||||
// If systemctl fails, just exit
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(path.join(__dirname, '..', 'temp'), { recursive: true, force: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Update applied successfully. The application will restart automatically.'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UpdateManager] Error applying update:', error.message);
|
||||
this.updateInProgress = false;
|
||||
throw new Error(`Failed to apply update: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update status
|
||||
*/
|
||||
getUpdateStatus() {
|
||||
return {
|
||||
updateInProgress: this.updateInProgress,
|
||||
currentVersion: this.currentVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UpdateManager;
|
||||
@@ -361,6 +361,71 @@ PulseApp.ui.settings = (() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Management -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Software Updates</h3>
|
||||
|
||||
<div id="update-status" class="mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Current Version: <span id="current-version" class="font-mono font-semibold">${currentConfig.version || 'Unknown'}</span>
|
||||
</p>
|
||||
<p id="latest-version-info" class="text-sm text-gray-600 dark:text-gray-400 mt-1"></p>
|
||||
</div>
|
||||
<button type="button" onclick="PulseApp.ui.settings.checkForUpdates()"
|
||||
id="check-updates-button"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Check for Updates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Details (hidden by default) -->
|
||||
<div id="update-details" class="hidden">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
|
||||
<h4 class="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
||||
Update Available: <span id="update-version"></span>
|
||||
</h4>
|
||||
<div id="update-release-notes" class="text-sm text-gray-700 dark:text-gray-300 prose prose-sm max-w-none"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Published: <span id="update-published"></span>
|
||||
</p>
|
||||
<button type="button" onclick="PulseApp.ui.settings.applyUpdate()"
|
||||
id="apply-update-button"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm rounded-md flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
||||
</svg>
|
||||
Apply Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Progress (hidden by default) -->
|
||||
<div id="update-progress" class="hidden">
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" id="update-progress-text">Preparing update...</p>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
|
||||
<div id="update-progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
Do not close this window or refresh the page during the update process.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="settings-messages"></div>
|
||||
</form>
|
||||
@@ -773,6 +838,175 @@ PulseApp.ui.settings = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Update management functions
|
||||
let updateInfo = null;
|
||||
|
||||
async function checkForUpdates() {
|
||||
const button = document.getElementById('check-updates-button');
|
||||
const updateDetails = document.getElementById('update-details');
|
||||
const latestVersionInfo = document.getElementById('latest-version-info');
|
||||
|
||||
try {
|
||||
// Disable button and show loading state
|
||||
button.disabled = true;
|
||||
button.innerHTML = `
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Checking...
|
||||
`;
|
||||
|
||||
const response = await fetch('/api/updates/check');
|
||||
if (!response.ok) throw new Error('Failed to check for updates');
|
||||
|
||||
updateInfo = await response.json();
|
||||
|
||||
// Update UI based on result
|
||||
if (updateInfo.updateAvailable) {
|
||||
updateDetails.classList.remove('hidden');
|
||||
document.getElementById('update-version').textContent = `v${updateInfo.latestVersion}`;
|
||||
document.getElementById('update-published').textContent = new Date(updateInfo.publishedAt).toLocaleDateString();
|
||||
|
||||
// Render release notes (convert markdown to HTML)
|
||||
const releaseNotes = updateInfo.releaseNotes || 'No release notes available';
|
||||
document.getElementById('update-release-notes').innerHTML = releaseNotes
|
||||
.replace(/## (.*?)$/gm, '<h3 class="font-semibold mt-3 mb-1">$1</h3>')
|
||||
.replace(/### (.*?)$/gm, '<h4 class="font-medium mt-2 mb-1">$1</h4>')
|
||||
.replace(/- (.*?)$/gm, '<li class="ml-4">$1</li>')
|
||||
.replace(/(\n\n)/g, '</p><p class="mb-2">')
|
||||
.replace(/^/, '<p class="mb-2">')
|
||||
.replace(/$/, '</p>');
|
||||
|
||||
latestVersionInfo.innerHTML = `<span class="text-green-600 dark:text-green-400">Update available!</span>`;
|
||||
} else {
|
||||
updateDetails.classList.add('hidden');
|
||||
latestVersionInfo.innerHTML = `<span class="text-gray-600 dark:text-gray-400">You are running the latest version</span>`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Settings] Error checking for updates:', error);
|
||||
showMessage('Failed to check for updates: ' + error.message, 'error');
|
||||
latestVersionInfo.innerHTML = `<span class="text-red-600 dark:text-red-400">Error checking for updates</span>`;
|
||||
} finally {
|
||||
// Re-enable button
|
||||
button.disabled = false;
|
||||
button.innerHTML = `
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Check for Updates
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUpdate() {
|
||||
if (!updateInfo || !updateInfo.updateAvailable) return;
|
||||
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to update Pulse to version ${updateInfo.latestVersion}?\\n\\n` +
|
||||
`The application will restart automatically after the update is applied.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
const updateDetails = document.getElementById('update-details');
|
||||
const updateProgress = document.getElementById('update-progress');
|
||||
const applyButton = document.getElementById('apply-update-button');
|
||||
|
||||
try {
|
||||
// Find the tarball asset
|
||||
const tarballAsset = updateInfo.assets.find(asset => asset.name.endsWith('.tar.gz'));
|
||||
if (!tarballAsset) {
|
||||
throw new Error('Update package not found');
|
||||
}
|
||||
|
||||
// Hide details, show progress
|
||||
updateDetails.classList.add('hidden');
|
||||
updateProgress.classList.remove('hidden');
|
||||
applyButton.disabled = true;
|
||||
|
||||
// Set up WebSocket listeners for progress updates
|
||||
setupUpdateProgressListeners();
|
||||
|
||||
// Start update
|
||||
const response = await fetch('/api/updates/apply', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
downloadUrl: tarballAsset.downloadUrl
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to start update');
|
||||
|
||||
const result = await response.json();
|
||||
showMessage(result.message, 'info');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Settings] Error applying update:', error);
|
||||
showMessage('Failed to apply update: ' + error.message, 'error');
|
||||
|
||||
// Reset UI
|
||||
updateDetails.classList.remove('hidden');
|
||||
updateProgress.classList.add('hidden');
|
||||
applyButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setupUpdateProgressListeners() {
|
||||
const progressBar = document.getElementById('update-progress-bar');
|
||||
const progressText = document.getElementById('update-progress-text');
|
||||
|
||||
// Listen for progress updates
|
||||
if (PulseApp.socket) {
|
||||
PulseApp.socket.on('updateProgress', (data) => {
|
||||
if (progressBar && progressText) {
|
||||
progressBar.style.width = `${data.progress}%`;
|
||||
|
||||
switch(data.phase) {
|
||||
case 'download':
|
||||
progressText.textContent = `Downloading update... ${data.progress}%`;
|
||||
break;
|
||||
case 'backup':
|
||||
progressText.textContent = `Backing up configuration... ${data.progress}%`;
|
||||
break;
|
||||
case 'extract':
|
||||
progressText.textContent = `Extracting update... ${data.progress}%`;
|
||||
break;
|
||||
case 'apply':
|
||||
progressText.textContent = `Applying update... ${data.progress}%`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PulseApp.socket.on('updateComplete', (data) => {
|
||||
if (data.success) {
|
||||
showMessage('Update completed successfully! The application will restart momentarily...', 'success');
|
||||
progressText.textContent = 'Update complete! Restarting...';
|
||||
|
||||
// Reload page after 3 seconds
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
PulseApp.socket.on('updateError', (data) => {
|
||||
showMessage('Update failed: ' + data.error, 'error');
|
||||
|
||||
// Reset UI
|
||||
const updateDetails = document.getElementById('update-details');
|
||||
const updateProgress = document.getElementById('update-progress');
|
||||
updateDetails.classList.remove('hidden');
|
||||
updateProgress.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
@@ -782,6 +1016,8 @@ PulseApp.ui.settings = (() => {
|
||||
addPbsEndpoint,
|
||||
removeEndpoint,
|
||||
testConnections,
|
||||
saveConfiguration
|
||||
saveConfiguration,
|
||||
checkForUpdates,
|
||||
applyUpdate
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user