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 = ` +
+
+
${message}
+
`; + this.addNotificationToContainer(notification); + + let closed = false; + const dismiss = () => { + if (closed) return; + closed = true; + clearTimeout(safety); + notification.classList.add('notification-hide'); + setTimeout(() => notification.remove(), 300); + }; + const safety = setTimeout(dismiss, options.timeout || 5 * 60 * 1000); + + return { element: notification, dismiss }; + } + /** * Create notification element with consistent structure */ diff --git a/containers/libreportal/frontend/core/tasks/js/task-actions.js b/containers/libreportal/frontend/core/tasks/js/task-actions.js index cadc2e2..4272b9c 100755 --- a/containers/libreportal/frontend/core/tasks/js/task-actions.js +++ b/containers/libreportal/frontend/core/tasks/js/task-actions.js @@ -533,9 +533,14 @@ window.TaskActions = TaskActions; // Classified by task action/type; the command regex also catches backend // variants (e.g. `libreportal updater check auto`). Add a `byAction` entry to // make another self-surfacing action quiet — the mechanism is generic. +// A `silent: true` entry goes one step further: no started toast and no finish +// line at all, for actions that surface their own result in place (app tools +// open a result modal). Those show a small "…" pending toast of their own while +// the task runs — see tools-manager.js. window.LP_BACKGROUND_TASKS = window.LP_BACKGROUND_TASKS || { byAction: { updater_check: { done: 'Apps checked for updates & vulnerabilities.', fail: 'Update check failed.' }, + tool: { silent: true }, }, commandRe: /^libreportal\s+updater\s+check\b/, pending: new Set(), // ids of hand-launched background tasks awaiting a quiet finish toast diff --git a/containers/libreportal/frontend/core/theme/css/base.css b/containers/libreportal/frontend/core/theme/css/base.css index 4228923..33b96e9 100755 --- a/containers/libreportal/frontend/core/theme/css/base.css +++ b/containers/libreportal/frontend/core/theme/css/base.css @@ -2810,6 +2810,32 @@ html[data-theme="nebula"]::after { color: var(--status-danger); } +/* Compact "working on it" toast (notificationSystem.showPending) — sized to + its text rather than the 350px+ status toasts, since it carries one short + line and no action button. */ +.notification-pending { + min-width: 0; + width: fit-content; + max-width: min(420px, calc(100vw - 40px)); + padding: 12px 16px; +} + +.notification-pending .notification-message { + font-size: 13px; + color: var(--text-secondary, var(--text-muted)); + margin-top: 0; +} + +.notification-spinner { + display: block; + width: 16px; + height: 16px; + border: 2px solid rgba(var(--text-rgb), 0.15); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + /* Mobile positioning */ @media (max-width: 768px) { .notification-container {