diff --git a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js index 395b824..4252c10 100755 --- a/containers/libreportal/frontend/js/components/tasks/tasks-manager.js +++ b/containers/libreportal/frontend/js/components/tasks/tasks-manager.js @@ -2318,38 +2318,116 @@ class TasksManager { } async clearAllTasks() { - // Use the confirmation dialog system if available, otherwise fallback to confirm + const result = await this._showClearAllModal(this.tasks); + if (!result || !result.confirmed) return false; + await this.performClearAll({ cancelRunning: result.cancelRunning }); + return true; + } + + // Confirmation modal for clearAllTasks. Same openEoModal shape as + // _showDeleteTaskModal so visual identity matches every other destructive + // confirmation (Uninstall, Delete Task, …). Adds a "Cancel running tasks + // too" toggle (off by default) — when off, running/queued tasks are + // skipped; when on, they're cancelled first then deleted. + // Resolves {confirmed, cancelRunning}. Cancel/backdrop/close → confirmed=false. + _showClearAllModal(tasks) { return new Promise((resolve) => { - if (window.showConfirmation) { - window.showConfirmation( - 'Clear All Tasks', - 'Are you sure you want to clear all tasks? This will delete all task history and cannot be undone.', - () => { - this.performClearAll(); - resolve(true); + const escHtml = (s) => String(s == null ? '' : s) + .replace(/&/g, '&').replace(//g, '>'); + + const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending'); + const total = (tasks || []).length; + const runningCount = (tasks || []).filter(isActive).length; + const terminalCount = total - runningCount; + + const bodyHtml = ` +
+
+ + + + + +
+
+

This cannot be undone

+

All selected tasks and their logs will be permanently removed.

+
+
+ ${window.eoBadgeRow ? window.eoBadgeRow([ + { label: `Total: ${total}`, variant: 'info' }, + ...(runningCount > 0 ? [{ label: `Running: ${runningCount}`, variant: 'warning' }] : []), + ...(terminalCount > 0 ? [{ label: `Terminal: ${terminalCount}`, variant: 'success' }] : []), + ]) : ''} + ${runningCount > 0 ? ` + + ` : ''} + `; + + let decided = false; + const finish = (val, modal) => { + if (decided) return; + decided = true; + if (modal) modal.close(); + resolve(val); + }; + + const m = window.openEoModal({ + id: 'clear-all-tasks-modal', + size: 'sm', + eyebrow: 'Delete Tasks', + title: total === 1 ? 'Delete 1 task?' : `Delete all ${total} tasks?`, + desc: 'Confirm to delete the selected tasks.', + body: bodyHtml, + actions: [ + { + label: 'Delete', + variant: 'danger', + onClick: (modal) => { + const cb = modal.bodyEl.querySelector('#clear-all-cancel-running'); + finish({ confirmed: true, cancelRunning: !!(cb && cb.checked) }, modal); + } }, - 'Yes, Clear All', - 'Cancel', - 'clear', - false - ); - } else { - // Fallback to native confirm - const confirmed = confirm('Are you sure you want to clear all tasks? This will delete all task history.'); - if (confirmed) { - this.performClearAll(); + { label: 'Cancel', variant: 'secondary', onClick: (modal) => finish({ confirmed: false }, modal) } + ], + onClose: () => finish({ confirmed: false }, null) + }); + + // Live-update the danger button's label so the user knows exactly + // what will happen as they flip the toggle. No-op when no running + // tasks (no toggle in the body in that case). + const cb = m.bodyEl.querySelector('#clear-all-cancel-running'); + const deleteBtn = m.contentEl.querySelector('.btn-danger'); + const updateLabel = () => { + if (!deleteBtn) return; + const cancelRunning = !!(cb && cb.checked); + if (runningCount > 0 && !cancelRunning) { + deleteBtn.textContent = `Delete ${terminalCount} (skip ${runningCount} running)`; + } else { + deleteBtn.textContent = total === 1 ? 'Delete Task' : `Delete ${total} Tasks`; } - resolve(confirmed); - } + }; + if (cb) cb.addEventListener('change', updateLabel); + updateLabel(); }); } - async performClearAll() { + async performClearAll(opts) { + opts = opts || {}; + const cancelRunning = !!opts.cancelRunning; + // Show progress notification with the trash type icon in the left slot // (this is conceptually a delete-all action, so 🗑️ matches the row icon). const customIcon = '🗑️'; const progressNotification = window.notificationSystem.show( - 'Clearing all tasks...', + 'Clearing tasks...', 'info', null, null, @@ -2357,44 +2435,55 @@ class TasksManager { customIcon ); + const isActive = (t) => t && (t.status === 'running' || t.status === 'queued' || t.status === 'pending'); + // Partition: which tasks we attempt now, which we skip silently. + const targets = (this.tasks || []).filter(t => cancelRunning || !isActive(t)); + const skipped = (this.tasks || []).filter(t => !targets.includes(t)); + try { - // Delete all tasks using the task manager - const deletePromises = this.tasks.map(task => - this.taskManager.deleteTask(task.id) - ); - - await Promise.all(deletePromises); - - // Clear local array and re-render - this.tasks = []; + // For active tasks (only reached when the toggle is on): cancel first, + // wait for terminal status, then delete. Mirrors the single-row + // deleteTask flow so the bash processor has a chance to honour the + // cancel marker before the DELETE arrives (otherwise we'd 409). + await Promise.all(targets.map(async (task) => { + if (cancelRunning && isActive(task)) { + try { await this.taskManager.cancelTask(task.id); } catch {} + await this._waitForTaskTerminal(task.id, 15_000); + } + try { + await this.taskManager.deleteTask(task.id); + } catch (err) { + const looks409 = /\bHTTP 409\b/.test(err && err.message ? err.message : ''); + if (!looks409) throw err; + await this.taskManager.deleteTask(task.id, { force: true }); + } + })); + + // Re-sync local state: keep anything we intentionally skipped. + const deletedIds = new Set(targets.map(t => t.id)); + this.tasks = this.tasks.filter(t => !deletedIds.has(t.id)); this.renderTasks(); this.updateStats(); this.updateSidebarCounts(); this.generateAppCategories(); - - // Remove progress notification and show success + if (progressNotification && progressNotification.remove) { progressNotification.remove(); } - + if (window.notificationSystem) { - window.notificationSystem.show( - 'All tasks cleared successfully', - 'success', - null, - null, - null, - customIcon - ); + const msg = skipped.length > 0 + ? `Deleted ${targets.length} tasks (skipped ${skipped.length} still running)` + : (targets.length === 1 ? 'Task deleted' : `Deleted ${targets.length} tasks`); + window.notificationSystem.show(msg, 'success', null, null, null, customIcon); } } catch (error) { console.error('Error clearing tasks:', error); - - // Remove progress notification and show error + if (progressNotification && progressNotification.remove) { progressNotification.remove(); } - + if (window.notificationSystem) { window.notificationSystem.show( `Failed to clear tasks: ${error.message}`,