// Auto-extracted from tasks-manager.js (verbatim) — augments TasksManager.prototype. Loaded after the base. Object.assign(TasksManager.prototype, { // SSE-driven log streaming. Subscribes to `taskLog` events from the bus and // appends incoming chunks. Initial backlog is fetched once via the API. // // Resilient to DOM replacement: the logs container can be wiped out by // `renderTasks()` (the 30s auto-refresh) or by `loadTaskLogs()` (toggle // re-open). Instead of capturing a `preElement` reference once, `render()` // re-locates / re-creates it on every call from the cumulative `buffered` // string. Idempotent: if already streaming, a second call just re-renders. async startLogStreaming(taskId, task) { if (!this.activeLogStreams) this.activeLogStreams = new Map(); if (this.activeLogStreams.has(taskId)) { const existing = this.activeLogStreams.get(taskId); if (existing && typeof existing.render === 'function') existing.render(); return; } const state = { buffered: '' }; const render = () => { const logsContainer = document.getElementById(`logs-${taskId}`); if (!logsContainer) return; const overlay = logsContainer.querySelector('div[style*="position: absolute"]'); if (overlay) overlay.remove(); let preElement = logsContainer.querySelector('pre.output-content'); if (!preElement) { logsContainer.innerHTML = ''; preElement = document.createElement('pre'); preElement.className = 'output-content terminal-style'; logsContainer.appendChild(preElement); } const atBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10; if (state.buffered) { preElement.innerHTML = this.taskManager.parseAnsiColors(state.buffered); } else { preElement.innerHTML = 'Waiting for logs...'; } if (atBottom) logsContainer.scrollTop = logsContainer.scrollHeight; }; // Initial backlog via the API. try { const initial = await this.taskManager.readFullTaskLog(taskId); if (initial && initial.length) state.buffered = initial; } catch { /* fall through to placeholder */ } render(); const onLog = (event) => { const detail = event.detail || {}; if (detail.id !== taskId || typeof detail.chunk !== 'string') return; state.buffered += detail.chunk; render(); }; window.addEventListener('taskLog', onLog); // SSE catch-up: when the backend restarts mid-task (e.g., libreportal // recreates itself during a CrowdSec install), the SSE event source // drops. EventSource auto-reconnects, but task.log events emitted // during the gap are lost. taskBusReady fires after every reconnect — // pull the missed bytes via the existing /:id/log?position=N endpoint // and splice them in. Skips on the initial connect (no gap). let initialReadyFired = false; const onBusReady = async () => { if (!initialReadyFired) { initialReadyFired = true; return; } try { const res = await fetch(`/api/tasks/${encodeURIComponent(taskId)}/log?position=${state.buffered.length}`); if (!res.ok) return; const missed = await res.text(); if (missed) { state.buffered += missed; render(); } } catch { /* network blip, next ready will retry */ } }; window.addEventListener('taskBusReady', onBusReady); this.activeLogStreams.set(taskId, { stream: { stop: () => { window.removeEventListener('taskLog', onLog); window.removeEventListener('taskBusReady', onBusReady); this.activeLogStreams.delete(taskId); } }, render, isPaused: false }); }, // Stop log streaming for a task stopLogStreaming(taskId) { if (this.activeLogStreams && this.activeLogStreams.has(taskId)) { const streamData = this.activeLogStreams.get(taskId); streamData.stream.stop(); this.activeLogStreams.delete(taskId); // console.log(`⏹️ Stopped log streaming for task ${taskId}`); } }, // Load task logs automatically async loadTaskLogs(taskId) { try { const logsContainer = document.getElementById(`logs-${taskId}`); if (!logsContainer) { console.warn(`⚠️ Logs container not found for task ${taskId}`); return; } const task = this.tasks.find(t => t.id === taskId); const inMemoryLog = (task && Array.isArray(task.log) && task.log.length > 0) ? task.log : null; const renderInMemory = () => { if (!inMemoryLog) return false; logsContainer.innerHTML = inMemoryLog .map(line => `
${this.taskManager.parseAnsiColors(line)}
`) .join(''); return true; }; logsContainer.innerHTML = '
🔄 Loading logs...
'; const isScrolledToBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10; const logResponse = await fetch(`/read-file?path=tasks/${taskId}.log`); if (logResponse.ok) { const logContent = await logResponse.text(); if (logContent.trim()) { logsContainer.innerHTML = `
${this.taskManager.parseAnsiColors(logContent)}
`; if (isScrolledToBottom) logsContainer.scrollTop = logsContainer.scrollHeight; return; } } if (renderInMemory()) return; logsContainer.innerHTML = '
ℹ️ No logs available for this task.
'; } catch (error) { console.error(`❌ Error loading logs for task ${taskId}:`, error); const logsContainer = document.getElementById(`logs-${taskId}`); if (logsContainer) { logsContainer.innerHTML = '
❌ Failed to load logs.
'; } } }, // Load task output on demand async loadTaskOutput(taskId) { try { // Read the task file to get output const task = await this.taskManager.getTask(taskId); if (!task) return; const outputElement = document.querySelector(`[data-task-id="${taskId}"] .task-output`); if (!outputElement) return; // Show loading state outputElement.innerHTML = `
Loading output...
`; // Check if task has output if (task.output && task.output.trim()) { outputElement.innerHTML = `

📤 Output

${this.taskManager.parseAnsiColors(task.output)}
`; } else if (task.error && task.error.trim()) { outputElement.innerHTML = `

❌ Error

${this.escapeHtml(task.error)}
`; } else { // Try to read from log file const logResponse = await fetch(`/read-file?path=tasks/${taskId}.log`); if (logResponse.ok) { const logContent = await logResponse.text(); if (logContent.trim()) { outputElement.innerHTML = `
${this.taskManager.parseAnsiColors(logContent)}
`; } else { outputElement.innerHTML = `

ℹ️ Information

No output available for this task.
`; } } else { outputElement.innerHTML = `

ℹ️ Information

No output available for this task.
`; } } } catch (error) { console.error(`❌ Error loading task output for ${taskId}:`, error); const outputElement = document.querySelector(`[data-task-id="${taskId}"] .task-output`); if (outputElement) { outputElement.innerHTML = `

❌ Error

Failed to load task output: ${error.message}
`; } } }, // Start global live log updater - simple 2-second updates for all running tasks startGlobalLiveLogUpdater() { // console.log(`🔄 Starting global live log updater`); // Update every 2 seconds setInterval(async () => { // console.log(`🔄 Global updater running - checking tasks...`); // Find all running tasks const runningTasks = this.tasks.filter(task => task.status === 'running'); // console.log(`🔄 Found ${runningTasks.length} running tasks:`, runningTasks.map(t => t.id)); if (runningTasks.length > 0) { // console.log(`🔄 Updating live logs for ${runningTasks.length} running tasks`); // Update each running task's live logs for (const task of runningTasks) { // console.log(`🔄 About to update live logs for task ${task.id}`); await this.updateLiveLogsSimple(task.id); } } else { // console.log(`🔄 No running tasks found, skipping live log updates`); } }, 2000); // Every 2 seconds }, // Simple live log update - no complex polling logic async updateLiveLogsSimple(taskId) { const liveLogsElement = document.getElementById(`live-logs-${taskId}`); if (!liveLogsElement) { // console.log(`⚠️ Live logs element not found for task ${taskId}`); return; // Silently skip if element not found } try { // console.log(`🔄 Reading log file for task ${taskId}`); // Read the log file content const response = await fetch(`/read-file?path=tasks/${taskId}.log`); // console.log(`🔄 Log file response status: ${response.status} for task ${taskId}`); if (response.ok) { const logContent = await response.text(); // console.log(`🔄 Log file content length: ${logContent.length} chars for task ${taskId}`); // console.log(`🔄 First 100 chars of log content: "${logContent.substring(0, 100)}..."`); if (logContent.trim()) { // Split into lines and display const lines = logContent.split('\n').filter(line => line.trim()); // console.log(`🔄 Displaying ${lines.length} log lines for task ${taskId}`); liveLogsElement.innerHTML = lines.map(line => `
${this.parseAnsiColors(line)}
` ).join(''); // Auto-scroll to bottom liveLogsElement.scrollTop = liveLogsElement.scrollHeight; } else { // console.log(`🔄 Log file is empty for task ${taskId}`); liveLogsElement.innerHTML = '
🔄 Waiting for logs...
'; } } else { console.warn(`⚠️ Failed to read log file for task ${taskId}: ${response.status}`); liveLogsElement.innerHTML = '
⚠️ Unable to read logs
'; } } catch (error) { // Silently handle errors console.warn(`⚠️ Error reading live logs for task ${taskId}:`, error); liveLogsElement.innerHTML = '
❌ Error loading logs
'; } }, // Update task structure for live logs updateTaskStructure(taskId, task) { const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); if (!taskElement) return; const detailsElement = taskElement.querySelector('.task-details'); if (!detailsElement) return; // console.log(`🔄 Updating task structure for ${taskId} to show simplified logs`); // Check if logs container already exists const existingLogs = detailsElement.querySelector('.task-logs .log-container'); if (existingLogs) { // console.log(`🔄 Logs container already exists for ${taskId}`); return; // Already exists, no need to update } // Add simplified logs section for running tasks if (task.status === 'running') { const logsHtml = `
Loading logs...
`; // Insert logs section at the bottom of details detailsElement.insertAdjacentHTML('beforeend', logsHtml); // console.log(`✅ Added simplified logs section for task ${taskId}`); // Auto-load logs this.loadTaskLogs(taskId); } }, // Update task display in real-time updateTaskDisplay(task) { const taskElement = document.querySelector(`[data-task-id="${task.id}"]`); if (!taskElement) { // The monitored task isn't always rendered (different tab, list filtered out, // task already removed). Silently skip — this is the normal case. return; } // console.log(`🔄 Found task element:`, taskElement); // Update status and content const statusElement = taskElement.querySelector('.task-status'); const contentElement = taskElement.querySelector('.task-content'); if (statusElement) { const statusClass = `status-${task.status || 'unknown'}`; statusElement.className = `task-status ${statusClass}`; statusElement.innerHTML = `${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}`; } else { console.warn(`⚠️ Status element not found for task ${task.id}`); } // Mirror the status into the details panel's metadata block too — that // copy of the status was previously left stale until the page reloaded. const detailsStatus = taskElement.querySelector(`#details-${task.id} .task-meta .status-running, #details-${task.id} .task-meta .status-queued, #details-${task.id} .task-meta .status-pending, #details-${task.id} .task-meta .status-completed, #details-${task.id} .task-meta .status-failed, #details-${task.id} .task-meta .status-cancelled, #details-${task.id} .task-meta [class^="status-"]`); if (detailsStatus) { detailsStatus.className = `status-${task.status || 'unknown'}`; detailsStatus.innerHTML = `${this.getStatusIcon(task.status)} ${task.status ? task.status.toUpperCase() : 'UNKNOWN'}`; } if (contentElement) { contentElement.textContent = task.command; } // console.log(`🔄 Updated task ${task.id} display: ${task.status}`); }, // Update highlighted task status and UI async updateHighlightedTaskStatus(taskId) { try { // Use lightweight summary for status updates const task = await this.taskManager.getTaskSummary(taskId); if (!task) return; // console.log(`🔄 Updating highlighted task ${taskId} status: ${task.status}`); // Update task display this.updateTaskDisplay(task); // If task completed or failed, always load output if ((task.status === 'completed' || task.status === 'failed')) { // console.log(`🔄 Task ${taskId} is ${task.status}, loading output...`); const details = document.getElementById(`details-${taskId}`); if (details && details.style.display === 'block') { // Load output regardless of current content this.loadTaskOutput(taskId); } else if (details) { // If details aren't open, mark for loading when opened details.setAttribute('data-load-output-on-open', 'true'); } } } catch (error) { console.error(`❌ Error updating highlighted task status for ${taskId}:`, error); } } , });