feat(webui): follow an update into its task, and come back when it lands
Pressing Update produced a toast and nothing else. The row did not change, nothing on the page moved, and the work was real but invisible — so the button read as though it had not done anything. Update, Update all / Update selected, and the stepped Upgrade now carry you to the task they started and return you to the Updates tab when it reaches a terminal state. Correlated by task ID, not by app name: routeAction resolves to the created task, so a second update started elsewhere cannot bring this one back early, and a completion for an unrelated app cannot either. Only completed/failed/cancelled end the wait — taskUpdated also fires mid-run, and treating that as done would bounce the user out of a task still in progress. Navigation is deliberately NOT locked, which is the one part of the request I did not build. The task is a background job with its own snapshot and rollback; it does not need watching, and a lock would strand someone here if a task ever hung. Leaving is therefore treated as a choice — the return only fires while the user is still on a tasks page, so nobody is yanked back from somewhere they chose to be. Both listeners are removed on the first terminal event, and a 30-minute timeout removes them anyway, so a task that never reports a terminal status cannot leak a pair of window listeners per click. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
dbe3153af9
commit
5f95a0744a
@ -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.</p>
|
||||
</div>`;
|
||||
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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user