feat(updater): Upgrade button for cross-version moves
Puts the stepped engine behind the "34 available" chip so it is not CLI-only. Wired end to end: updater_upgrade task type -> task-router -> updaterUpgrade action -> `libreportal updater upgrade <app> [version]`, with a label and icon in the tasks list. The button always confirms, and the dialog states the plan and the guarantee rather than asking "are you sure?" — which app, from which version to which, that every step snapshots first and waits for the app to confirm it is serving with no migration outstanding, that a failure stops the ladder on the last version that verified, and that it can take a long time because each release runs its own migration. Both delegated dispatchers (per-app Updates tab and the fleet Overview rows) learn the action, so the button works wherever the chip appears. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0679fd65b2
commit
046d69bad7
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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' }
|
||||
|
||||
@ -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 = `
|
||||
<div class="updater-detail-section">
|
||||
<p><strong>${this.escape(app)}</strong> will move from <strong>${this.escape(from)}</strong>
|
||||
to <strong>${this.escape(to)}</strong>, one release at a time.</p>
|
||||
<p class="updater-detail-meta">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.</p>
|
||||
<p class="updater-detail-meta">This can take a long time — each release runs its own database
|
||||
migration. Watch it in Tasks.</p>
|
||||
</div>`;
|
||||
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
|
||||
? `<div class="updater-detail-row"><span class="updater-detail-meta">A newer release line is available: <strong>${this.escape(a.newer_version)}</strong> (you track ${this.escape(a.channel || '—')}). Automatic updates keep you current within your line; to move, set <strong>Version</strong> on this app's Advanced config tab after checking the release notes.</span></div>`
|
||||
? `<div class="updater-detail-row"><span class="updater-detail-meta">A newer release line is available: <strong>${this.escape(a.newer_version)}</strong> (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.</span>
|
||||
<button class="updater-btn" data-updater-action="upgrade" data-app="${this.escape(a.name)}" data-version="${this.escape(a.newer_version)}">Upgrade to ${this.escape(a.newer_version)}</button></div>`
|
||||
: '';
|
||||
versionSection = `<div class="updater-detail-section"><h4>Version</h4>
|
||||
<div class="updater-detail-row">${badge} <span class="updater-row-ver">${cur}${avail ? ` <span class="updater-arrow">→</span> <strong>${avail}</strong>` : ''}</span></div>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user