diff --git a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js index 392a526..a638da6 100644 --- a/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js +++ b/containers/libreportal/frontend/components/apps/overview/js/overview-manager.js @@ -306,6 +306,7 @@ class OverviewManager { case 'update': this.updater.applyUpdate(app); break; case 'update-all': this.updater.applyAll(); break; case 'rollback': this.updater.rollback(app); break; + case 'upgrade': this.updater.upgrade(app, ua.dataset.version); break; case 'apply-artifact': this.updater.applyArtifact(ua.dataset.id); break; case 'revert-artifact': this.updater.revertArtifact(ua.dataset.id); break; } diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-format.js b/containers/libreportal/frontend/components/tasks/js/tasks-format.js index 6520cb7..04996a8 100644 --- a/containers/libreportal/frontend/components/tasks/js/tasks-format.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-format.js @@ -145,6 +145,7 @@ Object.assign(TasksManager.prototype, { 'system_health_heal': 'Fix System Issues', 'system_network_heal': 'Heal Network', 'updater_check': 'Check for Updates', 'updater_apply': 'Update', 'updater_apply_all': 'Update All', 'updater_rollback': 'Roll Back', + 'updater_upgrade': 'Upgrade Version', 'artifact_apply': 'Apply Hotfix', 'artifact_revert': 'Revert Hotfix', 'app_add': 'Add App', 'setup-config': 'Apply Configuration', diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js b/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js index 7980263..1166ca6 100644 --- a/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-list-render.js @@ -328,6 +328,7 @@ Object.assign(TasksManager.prototype, { 'updater_apply': { icon: '⬆️', class: 'update' }, 'updater_apply_all': { icon: '⬆️', class: 'update' }, 'updater_rollback': { icon: '↩️', class: 'restore' }, + 'updater_upgrade': { icon: '⏫', class: 'update' }, 'artifact_apply': { icon: '⚡', class: 'update' }, 'artifact_revert': { icon: '↩️', class: 'restore' }, 'custom': { icon: '⚙️', class: 'custom' } diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index f7837ea..ffe1ad3 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -109,6 +109,7 @@ class UpdaterPage { case 'update': this.applyUpdate(app); break; case 'update-all': this.applyAll(); break; case 'rollback': this.rollback(app); break; + case 'upgrade': this.upgrade(app, action.dataset.version); break; case 'apply-artifact': this.applyArtifact(action.dataset.id); break; case 'revert-artifact': this.revertArtifact(action.dataset.id); break; case 'goto': this.switchTab(action.dataset.tab); break; @@ -301,6 +302,33 @@ class UpdaterPage { if (!app) return; this.dispatch('updater_rollback', { app }, `Rolling ${app} back to its pre-update snapshot…`); } + // Cross-version upgrade. Always confirmed, and never quietly: this walks + // real migrations one release at a time and can take a long while, so the + // dialog states the plan and the guarantee rather than asking "are you sure?". + upgrade(app, version) { + if (!app) return; + const a = this.apps.find((x) => x.name === app) || {}; + const from = a.channel || a.current_version || 'the current version'; + const to = version || a.newer_version || 'the newest release'; + const body = ` +
+

${this.escape(app)} will move from ${this.escape(from)} + to ${this.escape(to)}, one release at a time.

+

Every step takes its own recovery snapshot first, then waits for + ${this.escape(app)} to confirm it is serving that version with no migration outstanding. + If any step fails it is rolled back and the upgrade stops there, leaving the app on the last + version that verified.

+

This can take a long time — each release runs its own database + migration. Watch it in Tasks.

+
`; + const go = () => this.dispatch('updater_upgrade', { app, version: version || '' }, + `Upgrading ${app} to ${to}, one release at a time…`); + if (window.showConfirmation) { + window.showConfirmation(`Upgrade ${app} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body); + } else if (window.confirm(`Upgrade ${app} from ${from} to ${to}, one release at a time?`)) { + go(); + } + } applyArtifact(id) { if (!id) return; this.dispatch('artifact_apply', { id }, `Applying hotfix ${id} (a snapshot is taken first)…`); @@ -555,7 +583,8 @@ class UpdaterPage { // current on the version you track — while still saying a newer one // exists and what to do about it. const newerLine = a.newer_version - ? `
A newer release line is available: ${this.escape(a.newer_version)} (you track ${this.escape(a.channel || '—')}). Automatic updates keep you current within your line; to move, set Version on this app's Advanced config tab after checking the release notes.
` + ? `
A newer release line is available: ${this.escape(a.newer_version)} (you track ${this.escape(a.channel || '—')}). Automatic updates keep you current within your line. Upgrading walks the releases one at a time, snapshotting and verifying each — read the release notes first. +
` : ''; versionSection = `

Version

${badge} ${cur}${avail ? ` ${avail}` : ''}
diff --git a/containers/libreportal/frontend/core/tasks/js/task-actions.js b/containers/libreportal/frontend/core/tasks/js/task-actions.js index dfcf5cd..cadc2e2 100755 --- a/containers/libreportal/frontend/core/tasks/js/task-actions.js +++ b/containers/libreportal/frontend/core/tasks/js/task-actions.js @@ -328,6 +328,18 @@ async configUpdate(changes) { return await this.executeTask('updater_apply', app, `libreportal updater apply ${app}`, `Update ${app}`); } catch (error) { throw new Error(`Failed to update ${app}: ${error.message}`); } } + // Stepped, cross-version upgrade. Separate from updaterApply because the CLI + // verb is separate: apply moves within a release line, upgrade walks between + // lines one version at a time, snapshotting and verifying every step. Passing + // no version means "climb to the newest". + async updaterUpgrade(app, version) { + if (!app) throw new Error('No app specified'); + try { + const v = (version || '').toString().trim(); + const cmd = v ? `libreportal updater upgrade ${app} ${v}` : `libreportal updater upgrade ${app}`; + return await this.executeTask('updater_upgrade', app, cmd, `Upgrade ${app}${v ? ` to ${v}` : ''}`); + } catch (error) { throw new Error(`Failed to upgrade ${app}: ${error.message}`); } + } async updaterApplyAll(apps) { try { const list = (apps || '').toString(); diff --git a/containers/libreportal/frontend/core/tasks/js/task-router.js b/containers/libreportal/frontend/core/tasks/js/task-router.js index a2e44f1..1ac2931 100755 --- a/containers/libreportal/frontend/core/tasks/js/task-router.js +++ b/containers/libreportal/frontend/core/tasks/js/task-router.js @@ -87,6 +87,8 @@ class TaskRouter { return await this.actions.updaterApplyAll(params.apps); case 'updater_rollback': return await this.actions.updaterRollback(params.app); + case 'updater_upgrade': + return await this.actions.updaterUpgrade(params.app, params.version); // Hotfix channel (components/updater "Improvements") — apply/revert one // signed hotfix via the locked-down `libreportal artifact` CLI as a task.