librelad 1b0040dbf1 refactor(tasks): decompose tasks-manager god-file into 8 responsibility files
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>
2026-05-30 14:53:19 +01:00

373 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = '<span style="color: #888;">Waiting for logs...</span>';
}
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 => `<div class="log-entry">${this.taskManager.parseAnsiColors(line)}</div>`)
.join('');
return true;
};
logsContainer.innerHTML = '<div class="log-entry">🔄 Loading logs...</div>';
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 = `<pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(logContent)}</pre>`;
if (isScrolledToBottom) logsContainer.scrollTop = logsContainer.scrollHeight;
return;
}
}
if (renderInMemory()) return;
logsContainer.innerHTML = '<div class="log-entry"> No logs available for this task.</div>';
} catch (error) {
console.error(`❌ Error loading logs for task ${taskId}:`, error);
const logsContainer = document.getElementById(`logs-${taskId}`);
if (logsContainer) {
logsContainer.innerHTML = '<div class="log-entry">❌ Failed to load logs.</div>';
}
}
},
// 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 = `
<div class="loading-output">Loading output...</div>
`;
// Check if task has output
if (task.output && task.output.trim()) {
outputElement.innerHTML = `
<h4>📤 Output</h4>
<pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(task.output)}</pre>
`;
} else if (task.error && task.error.trim()) {
outputElement.innerHTML = `
<h4>❌ Error</h4>
<pre class="error-content">${this.escapeHtml(task.error)}</pre>
`;
} 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 = `
<pre class="output-content terminal-style">${this.taskManager.parseAnsiColors(logContent)}</pre>
`;
} else {
outputElement.innerHTML = `
<h4> Information</h4>
<div class="info-content">No output available for this task.</div>
`;
}
} else {
outputElement.innerHTML = `
<h4> Information</h4>
<div class="info-content">No output available for this task.</div>
`;
}
}
} 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 = `
<h4>❌ Error</h4>
<div class="error-content">Failed to load task output: ${error.message}</div>
`;
}
}
},
// 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 =>
`<div class="log-entry">${this.parseAnsiColors(line)}</div>`
).join('');
// Auto-scroll to bottom
liveLogsElement.scrollTop = liveLogsElement.scrollHeight;
} else {
// console.log(`🔄 Log file is empty for task ${taskId}`);
liveLogsElement.innerHTML = '<div class="log-entry">🔄 Waiting for logs...</div>';
}
} else {
console.warn(`⚠️ Failed to read log file for task ${taskId}: ${response.status}`);
liveLogsElement.innerHTML = '<div class="log-entry">⚠️ Unable to read logs</div>';
}
} catch (error) {
// Silently handle errors
console.warn(`⚠️ Error reading live logs for task ${taskId}:`, error);
liveLogsElement.innerHTML = '<div class="log-entry">❌ Error loading logs</div>';
}
},
// 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 = `
<div class="task-logs">
<div class="log-container terminal-style" id="logs-${taskId}" style="height: 200px; overflow-y: auto; position: relative;">
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(10, 18, 36, 0.85); display: flex; align-items: center; justify-content: center; z-index: 10;"><div class="loading-spinner" style="width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.18); border-top: 2px solid #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px;"></div>Loading logs...</div></div>
</div>
</div>
`;
// 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);
}
} ,
});