diff --git a/containers/libreportal/frontend/components/updater/js/updater-page.js b/containers/libreportal/frontend/components/updater/js/updater-page.js index 97194fd..b97d53f 100644 --- a/containers/libreportal/frontend/components/updater/js/updater-page.js +++ b/containers/libreportal/frontend/components/updater/js/updater-page.js @@ -308,7 +308,7 @@ class UpdaterPage { } applyUpdate(app) { if (!app) return; - this.dispatch('updater_apply', { app }, `Updating ${app} (a recovery snapshot is taken first)…`); + this.dispatch('updater_apply', { app }, `Updating ${app} (a recovery snapshot is taken first)…`, { follow: true }); } applyAll() { const list = this.apps.filter(a => a.update_available).map(a => a.name); @@ -325,7 +325,7 @@ class UpdaterPage { const want = new Set(names || []); const list = this.apps.filter(a => a.update_available && want.has(a.name)).map(a => a.name); if (!list.length) { this.toast('Everything is up to date.', 'info'); return; } - this.dispatch('updater_apply_all', { apps: list.join(',') }, `Updating ${list.length} app(s) — each is snapshotted first…`); + this.dispatch('updater_apply_all', { apps: list.join(',') }, `Updating ${list.length} app(s) — each is snapshotted first…`, { follow: true }); } rollback(app) { if (!app) return; @@ -354,7 +354,7 @@ class UpdaterPage { migration. Watch it in Tasks.

`; const go = () => this.dispatch('updater_upgrade', { app, version: version || '' }, - `Upgrading ${app} to ${to}, one release at a time…`); + `Upgrading ${app} to ${to}, one release at a time…`, { follow: true }); if (window.showConfirmation) { window.showConfirmation(`Upgrade ${label} to ${to}?`, '', go, 'Start upgrade', 'Cancel', 'warning', false, '', body, `/core/icons/apps/${app}.svg`); @@ -371,11 +371,12 @@ class UpdaterPage { this.dispatch('artifact_revert', { id }, `Reverting hotfix ${id}…`); } - dispatch(action, params, note) { + dispatch(action, params, note, opts) { const route = this.services.tasks && this.services.tasks.route; if (route && typeof route.routeAction === 'function') { - route.routeAction(action, params || {}); + const started = route.routeAction(action, params || {}); this.toast(note || 'Working…', 'info'); + if (opts && opts.follow) this.followTask(started); } else if (typeof route === 'function') { route(action, params || {}); this.toast(note || 'Working…', 'info'); @@ -384,6 +385,54 @@ class UpdaterPage { } } + // Take the user to the task they just started, and bring them back when it + // lands. Pressing Update used to produce only a toast: the row did not change, + // nothing on the page moved, and the work was real but invisible. + // + // Correlated by task id rather than by app name — routeAction resolves to the + // created task — so a second update started elsewhere cannot return this one + // early, and a completion for some unrelated app cannot either. + // + // Deliberately does NOT lock navigation. The task is a background job with its + // own snapshot and rollback; it does not need to be watched, and a lock would + // strand the user on this page if it ever hung. So if they navigate away, that + // is a choice: the return only fires while they are still on a tasks page. + followTask(started) { + if (!window.spaClean || typeof window.spaClean.navigate !== 'function') return; + const back = window.location.pathname.startsWith('/apps/overview') + ? window.location.pathname + : '/apps/overview/updates'; + + Promise.resolve(started).then((task) => { + const id = task && (task.id || task.taskId); + if (!id) return; // no id, nothing to follow or match + window.spaClean.navigate(window.taskPath ? window.taskPath('all', id) : `/tasks/all/${id}`); + + let done = false; + const finish = () => { + if (done) return; + done = true; + window.removeEventListener('taskCompleted', onEvent); + window.removeEventListener('taskUpdated', onEvent); + clearTimeout(timer); + }; + const onEvent = (e) => { + const d = e && e.detail; + if (!d || String(d.taskId) !== String(id)) return; + const st = (d.status || (d.task && d.task.status) || '').toLowerCase(); + // taskUpdated also fires mid-run; only a terminal state ends the wait. + if (st && !['completed', 'failed', 'cancelled'].includes(st)) return; + finish(); + if (!window.location.pathname.startsWith('/tasks')) return; // they left; leave them + window.spaClean.navigate(back); + }; + window.addEventListener('taskCompleted', onEvent); + window.addEventListener('taskUpdated', onEvent); + // A task that never reports terminal must not leak two listeners forever. + const timer = setTimeout(finish, 30 * 60 * 1000); + }).catch(() => { /* the dispatch itself already surfaced the failure */ }); + } + toast(msg, type) { const n = this.services.notify; if (n && typeof n.show === 'function') n.show(msg, type || 'info');