Faithful brace-aware split of tasks-manager.js (2664->491 line base) into list-render, data-load, log-stream, row-expand, actions, modals, logs-modal, format — augmenting TasksManager.prototype. First removed 4 provably-dead duplicate defs (the earlier init/setupAutoRefresh/startLogStreaming/loadTaskLogs that the later defs already overrode — behavior-preserving). Methods relocated verbatim via a brace-aware Node extractor (handles strings/templates/comments/ regex, fixing the line-heuristic over-capture). Verified: all 60 (deduped) methods present exactly once, no dups, all 9 files node --check clean. Wired after the base in the task-system loader (async=false-ordered). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
295 lines
12 KiB
JavaScript
295 lines
12 KiB
JavaScript
// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base.
|
|
Object.assign(TasksManager.prototype, {
|
|
async retryTask(taskId) {
|
|
if (!confirm('Are you sure you want to retry this task?')) return;
|
|
|
|
try {
|
|
const task = this.tasks.find(t => t.id === taskId);
|
|
if (!task) {
|
|
throw new Error('Task not found');
|
|
}
|
|
|
|
// Create a new task with the same command
|
|
const newTask = await this.taskManager.createTask(
|
|
task.command,
|
|
task.type,
|
|
task.app,
|
|
task.config
|
|
);
|
|
|
|
//// // console.log(`✅ Task retried: ${newTask.id}`);
|
|
|
|
// Refresh tasks to show the new one
|
|
await this.loadTasks();
|
|
|
|
if (window.notificationSystem) {
|
|
// Use the source task's type icon — retrying a backup shows 💾 etc.
|
|
const typeIcon = this.getTaskTypeIcon ? this.getTaskTypeIcon(task)?.icon : '';
|
|
const customIcon = typeIcon ? `<span style="font-size:18px;line-height:1;">${typeIcon}</span>` : null;
|
|
window.notificationSystem.show('Task retried successfully', 'success', null, null, null, customIcon);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error retrying task:', error);
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.error(`Failed to retry task: ${error.message}`);
|
|
}
|
|
}
|
|
},
|
|
async deleteTask(taskId) {
|
|
// Get the latest known status before deciding what to do. The server
|
|
// refuses to delete tasks that are still running or queued (HTTP 409),
|
|
// and we used to just propagate that as a red banner — but the user is
|
|
// almost always trying to clean up a row that looked finished, or a
|
|
// genuinely active task they want gone. Either way the right answer is
|
|
// "cancel first, then delete", which is what we do here.
|
|
const cached = (window.taskEventBus && window.taskEventBus.getTask)
|
|
? window.taskEventBus.getTask(taskId)
|
|
: null;
|
|
let task = cached || (this.tasks && this.tasks.find(t => t.id === taskId));
|
|
if (!task && this.taskManager && this.taskManager.getTask) {
|
|
try { task = await this.taskManager.getTask(taskId); } catch {}
|
|
}
|
|
|
|
const isActive = task && (task.status === 'running' || task.status === 'queued' || task.status === 'pending');
|
|
|
|
const confirmed = await this._showDeleteTaskModal(task, isActive);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
if (isActive) {
|
|
// Ask the server to cancel. POST /cancel either flips status straight
|
|
// to `cancelled` (queued -> cancelled is synchronous) or drops a
|
|
// `<id>.cancel` marker the bash processor picks up within ~1s
|
|
// (running -> cancelled). We then wait for the terminal status via
|
|
// SSE before issuing the delete.
|
|
try { await this.taskManager.cancelTask(taskId); } catch {
|
|
// If cancel itself failed (e.g. already terminal), fall through
|
|
// and try the delete anyway.
|
|
}
|
|
await this._waitForTaskTerminal(taskId, 15_000);
|
|
}
|
|
|
|
// Try the polite delete first. If the task is still flagged as
|
|
// running/queued server-side (e.g. the bash processor is dead and
|
|
// never picked up the cancel marker), fall back to the force-delete
|
|
// override so the user isn't stuck with a permanently-undeletable row.
|
|
try {
|
|
await this.taskManager.deleteTask(taskId);
|
|
} catch (err) {
|
|
const looks409 = /\bHTTP 409\b/.test(err && err.message ? err.message : '');
|
|
if (!looks409) throw err;
|
|
await this.taskManager.deleteTask(taskId, { force: true });
|
|
}
|
|
|
|
// Remove from local array and re-render
|
|
this.tasks = this.tasks.filter(t => t.id !== taskId);
|
|
this.renderTasks();
|
|
this.updateStats();
|
|
this.updateSidebarCounts();
|
|
this.generateAppCategories();
|
|
|
|
if (window.notificationSystem) {
|
|
// Match the "<App> task <verb>" two-line shape used by started/
|
|
// completed/failed/cancelled so deletion reads as part of the same
|
|
// family. Type emoji falls back to 🗑️ since deletion is a 🗑️ action.
|
|
const { appName, actionTitle, displayName, icon, typeIcon } = this._taskNotificationDescriptor(task);
|
|
const emoji = typeIcon || '🗑️';
|
|
const customIcon = `<span style="font-size:18px;line-height:1;">${emoji}</span>`;
|
|
const body = displayName
|
|
? `<strong>${displayName}</strong><br>${actionTitle} task deleted.`
|
|
: `${actionTitle || 'Task'} deleted.`;
|
|
window.notificationSystem.show(body, 'info', appName, null, icon, customIcon);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting task:', error);
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.error(`Failed to delete task: ${error.message}`);
|
|
}
|
|
}
|
|
},
|
|
async clearAllTasks() {
|
|
// Routes to one of two modes depending on whether any rows are ticked.
|
|
// Both paths share _showClearAllModal — same UX, same modal, different
|
|
// input list + title.
|
|
const hasSelection = this.selectedTaskIds.size > 0;
|
|
const targetTasks = hasSelection
|
|
? this.tasks.filter(t => this.selectedTaskIds.has(t.id))
|
|
: this.tasks;
|
|
const result = await this._showClearAllModal(targetTasks, hasSelection ? 'selected' : 'all');
|
|
if (!result || !result.confirmed) return false;
|
|
await this.performClearAll({ cancelRunning: result.cancelRunning, targets: targetTasks });
|
|
if (hasSelection) {
|
|
// Drop any ids we just deleted from the selection set, then refresh
|
|
// the button label + master-checkbox state.
|
|
this.selectedTaskIds = new Set([...this.selectedTaskIds].filter(id => this.tasks.find(t => t.id === id)));
|
|
this._updateSelectionUI();
|
|
}
|
|
return true;
|
|
},
|
|
async performClearAll(opts) {
|
|
opts = opts || {};
|
|
const cancelRunning = !!opts.cancelRunning;
|
|
// Caller (clearAllTasks) passes the subset to operate on. Falls back
|
|
// to this.tasks for backward compatibility — old direct callers
|
|
// continue to mean "everything".
|
|
const universe = Array.isArray(opts.targets) ? opts.targets : (this.tasks || []);
|
|
|
|
// 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 = '<span style="font-size:18px;line-height:1;">🗑️</span>';
|
|
const progressNotification = window.notificationSystem.show(
|
|
'Clearing tasks...',
|
|
'info',
|
|
null,
|
|
null,
|
|
null,
|
|
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 = universe.filter(t => cancelRunning || !isActive(t));
|
|
const skipped = universe.filter(t => !targets.includes(t));
|
|
|
|
try {
|
|
// 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();
|
|
|
|
if (progressNotification && progressNotification.remove) {
|
|
progressNotification.remove();
|
|
}
|
|
|
|
if (window.notificationSystem) {
|
|
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);
|
|
|
|
if (progressNotification && progressNotification.remove) {
|
|
progressNotification.remove();
|
|
}
|
|
|
|
if (window.notificationSystem) {
|
|
window.notificationSystem.show(
|
|
`Failed to clear tasks: ${error.message}`,
|
|
'error',
|
|
null,
|
|
null,
|
|
null,
|
|
customIcon
|
|
);
|
|
}
|
|
}
|
|
},
|
|
// Selection helpers wired by the row + master checkboxes.
|
|
toggleTaskSelection(taskId, checked) {
|
|
if (checked) this.selectedTaskIds.add(taskId);
|
|
else this.selectedTaskIds.delete(taskId);
|
|
this._updateSelectionUI();
|
|
},
|
|
toggleSelectAll(checked) {
|
|
if (checked) {
|
|
// Tick every CURRENTLY VISIBLE row. We use the rendered checkboxes
|
|
// rather than this.tasks so category-filtered views only select
|
|
// what the user can see.
|
|
const boxes = document.querySelectorAll('#tasks-list [data-task-select]');
|
|
boxes.forEach((cb) => {
|
|
this.selectedTaskIds.add(cb.dataset.taskSelect);
|
|
cb.checked = true;
|
|
});
|
|
} else {
|
|
this.selectedTaskIds.clear();
|
|
document.querySelectorAll('#tasks-list [data-task-select]').forEach((cb) => { cb.checked = false; });
|
|
}
|
|
this._updateSelectionUI();
|
|
},
|
|
// Rewrites the Clear All button label + the master-checkbox indeterminate
|
|
// state to reflect the current selection. Cheap — only touches a few
|
|
// DOM nodes, safe to call from any selection-change path.
|
|
_updateSelectionUI() {
|
|
const n = this.selectedTaskIds.size;
|
|
const btn = document.getElementById('tasks-clear-btn');
|
|
const btnLabel = btn && btn.querySelector('.clear-btn-label');
|
|
if (btnLabel) btnLabel.textContent = n > 0 ? `Delete Selected (${n})` : 'Clear All';
|
|
if (btn) btn.title = n > 0 ? `Delete ${n} selected task${n === 1 ? '' : 's'}` : 'Clear All Tasks';
|
|
|
|
// Master checkbox: checked when ALL visible are picked, indeterminate
|
|
// when SOME are, unchecked when none.
|
|
const master = document.getElementById('tasks-select-all');
|
|
const visible = document.querySelectorAll('#tasks-list [data-task-select]');
|
|
if (master) {
|
|
if (n === 0 || visible.length === 0) {
|
|
master.checked = false;
|
|
master.indeterminate = false;
|
|
} else if (n >= visible.length) {
|
|
master.checked = true;
|
|
master.indeterminate = false;
|
|
} else {
|
|
master.checked = false;
|
|
master.indeterminate = true;
|
|
}
|
|
}
|
|
},
|
|
// Resolves once the task reaches completed/failed/cancelled, or after the
|
|
// timeout. Used by deleteTask so a cancel request has time to take effect
|
|
// before we issue the DELETE that would otherwise 409.
|
|
_waitForTaskTerminal(taskId, timeoutMs = 15_000) {
|
|
return new Promise((resolve) => {
|
|
// Already terminal in the bus cache? No need to wait.
|
|
const cached = (window.taskEventBus && window.taskEventBus.getTask) ? window.taskEventBus.getTask(taskId) : null;
|
|
if (cached && (cached.status === 'completed' || cached.status === 'failed' || cached.status === 'cancelled')) {
|
|
return resolve(cached);
|
|
}
|
|
|
|
let timer;
|
|
const cleanup = () => {
|
|
window.removeEventListener('taskCompleted', onComplete);
|
|
window.removeEventListener('taskUpdated', onUpdate);
|
|
if (timer) clearTimeout(timer);
|
|
};
|
|
const isTerminal = (s) => s === 'completed' || s === 'failed' || s === 'cancelled';
|
|
const onComplete = (e) => {
|
|
const t = e.detail && (e.detail.task || (e.detail.taskId === taskId ? { id: taskId, status: e.detail.status } : null));
|
|
const id = (t && t.id) || (e.detail && e.detail.taskId);
|
|
if (id !== taskId) return;
|
|
cleanup();
|
|
resolve(t);
|
|
};
|
|
const onUpdate = (e) => {
|
|
const t = e.detail && e.detail.task;
|
|
if (!t || t.id !== taskId) return;
|
|
if (isTerminal(t.status)) { cleanup(); resolve(t); }
|
|
};
|
|
window.addEventListener('taskCompleted', onComplete);
|
|
window.addEventListener('taskUpdated', onUpdate);
|
|
timer = setTimeout(() => { cleanup(); resolve(null); }, timeoutMs);
|
|
});
|
|
},
|
|
});
|