From a6b0fd1bccce1eb41f2a004bf4f8d3d04b0dd23f Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 21:17:37 +0100 Subject: [PATCH] fix(tasks): no ghost completion toasts for tasks the bus didn't witness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The task-event bus translates the backend's task.upsert SSE events into window-level taskCreated / taskUpdated / taskCompleted CustomEvents. It fired taskCompleted whenever a task's current status was terminal AND the previously-known status was not — including the case where the bus had never seen the task before at all (prevStatus undefined → wasTerminal false → "transition" detected). Why this misfired: the backend re-broadcasts the full task object on any inode change to the task file, not just on logical status changes. The periodic ownership/permission repair sweep (crontab_check_processor.sh) chowns the entire tasks directory, which bumps ctime on every task file and trips fs.watch, which broadcasts task.upsert for each one. If the page was loaded after a task had already finished, the bus saw that task for the first time as already terminal and fired a "task completed" toast — for tasks that completed minutes or hours earlier. Fix: when an upsert is for a task the bus has never seen AND that task is already terminal, bootstrap silently. We have no evidence the task transitioned now — it might have transitioned hours ago. The real running→terminal transition (bus knew about the task while it was running, then receives a terminal upsert) still notifies, which is what users actually want to know about. Signed-off-by: librelad --- .../js/components/task/task-event-bus.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/containers/libreportal/frontend/js/components/task/task-event-bus.js b/containers/libreportal/frontend/js/components/task/task-event-bus.js index 3b75415..27c8fed 100755 --- a/containers/libreportal/frontend/js/components/task/task-event-bus.js +++ b/containers/libreportal/frontend/js/components/task/task-event-bus.js @@ -119,13 +119,28 @@ class TaskEventBus { timestamp: Date.now() }; + const isTerminal = task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'; + const wasTerminal = prevStatus === 'completed' || prevStatus === 'failed' || prevStatus === 'cancelled'; + + // Bootstrap silently. When this is the first time we've ever seen a task + // AND it's already in a terminal state, we have NO evidence it + // transitioned just now — it might have completed seconds or hours ago. + // The backend re-broadcasts the file's contents on any inode change (e.g. + // the periodic ownership/permission sweep stats every task file), so + // firing taskCompleted here would surface a "ghost" completion + // notification for a task the user already saw finish 28 minutes ago. + // Just record it in our map and move on. + if (isNew && isTerminal) { + return; + } + if (isNew) { window.dispatchEvent(new CustomEvent('taskCreated', { detail })); } - const isTerminal = task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'; - const wasTerminal = prevStatus === 'completed' || prevStatus === 'failed' || prevStatus === 'cancelled'; - + // Genuine transition to terminal: we had a prior non-terminal record and + // the current upsert flips that to terminal. This is what users expect + // to be notified about. if (isTerminal && !wasTerminal) { window.dispatchEvent(new CustomEvent('taskCompleted', { detail })); } else if (!isNew) {