diff --git a/containers/libreportal/frontend/components/apps/tools/js/tools-manager.js b/containers/libreportal/frontend/components/apps/tools/js/tools-manager.js index 7ceafdd..1b38a46 100644 --- a/containers/libreportal/frontend/components/apps/tools/js/tools-manager.js +++ b/containers/libreportal/frontend/components/apps/tools/js/tools-manager.js @@ -828,19 +828,22 @@ class ToolsManager { } // Wait for one specific tool task to finish, then surface its outcome - // without leaving the Tools tab. + // without leaving the Tools tab. `pending` is the small spinner toast raised + // by _dispatch; it comes down the moment we have a result to show. // - // list_users is left alone: _maybeOpenUserListModal already listens for the - // same event and opens the interactive account list, which is a better - // result view than a summary could be. - _watchToolTask(tool, taskId) { - if (!taskId) return; + // list_users is left alone on success: _maybeOpenUserListModal already listens + // for the same event and opens the interactive account list, which is a better + // result view than a summary could be. A failed run has no account list to + // open, so it falls through to the normal result modal like every other tool. + _watchToolTask(tool, taskId, pending) { + if (!taskId) { pending?.dismiss(); return; } const onDone = (ev) => { const d = ev?.detail || {}; const id = d.taskId || d.id || d.task?.id; if (id !== taskId) return; // not ours — keep listening window.removeEventListener('taskCompleted', onDone); - if (tool.id === 'list_users') return; + pending?.dismiss(); + if (tool.id === 'list_users' && d.status === 'completed') return; this._showToolResult(tool, taskId, d.status); }; window.addEventListener('taskCompleted', onDone); @@ -910,6 +913,15 @@ class ToolsManager { } return; } + // One small spinner line for the whole run, in place of the standard + // "task started!" / "task completed!" pair (suppressed for the `tool` + // action in LP_BACKGROUND_TASKS). A tool is a few seconds of work whose + // answer arrives in a modal — two full-size toasts around that read as + // ceremony, and the started one is already stale by the time it's read. + const pending = window.notificationSystem + ? window.notificationSystem.showPending(`Running ${escapeHtml(tool.label || tool.id)}…`) + : null; + try { const task = await window.tasksManager.router.routeAction('tool', { appName: this.currentApp, @@ -929,9 +941,10 @@ class ToolsManager { // // So: stay put, and bring the result to them. The log is still one click // away from the completion notice for anything that needs the detail. - this._watchToolTask(tool, task && task.id); + this._watchToolTask(tool, task && task.id, pending); } catch (err) { console.error('Tool dispatch failed', err); + pending?.dismiss(); if (window.notificationSystem) { window.notificationSystem.error(`Tool failed: ${err.message}`); } diff --git a/containers/libreportal/frontend/components/tasks/js/tasks-manager.js b/containers/libreportal/frontend/components/tasks/js/tasks-manager.js index bbafde5..a8d1b90 100755 --- a/containers/libreportal/frontend/components/tasks/js/tasks-manager.js +++ b/containers/libreportal/frontend/components/tasks/js/tasks-manager.js @@ -420,8 +420,10 @@ class TasksManager { const bg = window.LP_BACKGROUND_TASKS; if (bg && bg.match(task.type, task.command)) { const handLaunched = bg.pending.delete(String(task.id)); - if (handLaunched && (task.status === 'completed' || task.status === 'failed')) { - const m = bg.messages(task.type); + const m = bg.messages(task.type); + // `silent` actions (app tools) report their own outcome in a modal — + // even the one-line finish toast would be saying it twice. + if (!m.silent && handLaunched && (task.status === 'completed' || task.status === 'failed')) { window.notificationSystem.show( task.status === 'completed' ? m.done : m.fail, task.status === 'completed' ? 'success' : 'error' diff --git a/containers/libreportal/frontend/core/notifications/js/notifications.js b/containers/libreportal/frontend/core/notifications/js/notifications.js index 3e2fa42..a8b2ce4 100755 --- a/containers/libreportal/frontend/core/notifications/js/notifications.js +++ b/containers/libreportal/frontend/core/notifications/js/notifications.js @@ -36,6 +36,41 @@ class NotificationSystem { return notification; } + /** + * Small "working on it" toast for short actions that report their own + * result (e.g. app tools, which end in a result modal). + * + * Deliberately unlike show(): compact, spinner instead of a status icon, no + * app icon or action button, never written to localStorage (a pending state + * is meaningless after a reload) and no 10s auto-dismiss — the caller closes + * it when the work finishes. A long safety timeout still clears it if the + * caller never calls back (processor restart, tab left open). + * + * Returns a handle: { element, dismiss() }. dismiss() is idempotent. + */ + showPending(message, options = {}) { + const notification = document.createElement('div'); + notification.className = 'notification notification-pending'; + notification.innerHTML = ` +