|
|
@@ -1246,6 +1246,11 @@
|
|
|
|
|
|
function closeModal(id) {
|
|
|
document.getElementById(id).classList.remove('active');
|
|
|
+ // 关闭任务日志 SSE 连接
|
|
|
+ if (id === 'taskDetailModal' && _taskLogSSE) {
|
|
|
+ _taskLogSSE.close();
|
|
|
+ _taskLogSSE = null;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 编辑主机
|
|
|
@@ -1543,28 +1548,72 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // SSE 流式日志连接管理
|
|
|
+ let _taskLogSSE = null;
|
|
|
+ let _taskLogOutput = '';
|
|
|
+
|
|
|
async function viewTaskOutput(id) {
|
|
|
const res = await api(`/tasks/${id}`);
|
|
|
if (res.code === 0 && res.data) {
|
|
|
const task = res.data;
|
|
|
const content = document.getElementById('taskDetailContent');
|
|
|
+
|
|
|
+ // 关闭之前的 SSE 连接
|
|
|
+ if (_taskLogSSE) {
|
|
|
+ _taskLogSSE.close();
|
|
|
+ _taskLogSSE = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ _taskLogOutput = task.output || '';
|
|
|
+
|
|
|
+ const isRunning = task.status === 'running';
|
|
|
+
|
|
|
content.innerHTML = `
|
|
|
<div style="margin-bottom: 15px;">
|
|
|
<strong>任务名称:</strong> ${task.name}<br>
|
|
|
- <strong>状态:</strong> <span class="status ${task.status}">${statusText(task.status)}</span><br>
|
|
|
+ <strong>状态:</strong> <span class="status ${task.status}">${statusText(task.status)}</span>
|
|
|
+ ${isRunning ? '<span class="spinner" style="margin-left:8px;"></span>' : ''}<br>
|
|
|
<strong>开始时间:</strong> ${task.start_time ? new Date(task.start_time).toLocaleString() : '-'}<br>
|
|
|
<strong>结束时间:</strong> ${task.end_time ? new Date(task.end_time).toLocaleString() : '-'}<br>
|
|
|
+ ${task.error ? `<strong style="color:#ff6b6b;">错误:</strong> <span style="color:#ff6b6b;">${escapeHtml(task.error)}</span><br>` : ''}
|
|
|
</div>
|
|
|
- <h3 style="color:#00d4aa;margin-bottom:10px;">执行结果</h3>
|
|
|
- <div class="terminal">
|
|
|
- ${(task.results || []).map(r => `
|
|
|
- <div class="line ${r.success ? 'success' : 'error'}">[${r.host}] ${r.success ? '✓' : '✗'} (${r.duration || 0}ms)</div>
|
|
|
- ${r.output ? `<div class="line">${escapeHtml(r.output)}</div>` : ''}
|
|
|
- ${r.error ? `<div class="line error">${escapeHtml(r.error)}</div>` : ''}
|
|
|
- `).join('') || '<div class="line info">暂无结果</div>'}
|
|
|
- </div>
|
|
|
+ <h3 style="color:#00d4aa;margin-bottom:10px;">执行日志 ${isRunning ? '<span style="color:#ffd93d;font-size:12px;">(实时更新中...)</span>' : ''}</h3>
|
|
|
+ <div class="terminal" id="taskLogTerminal" style="max-height:500px;overflow-y:auto;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;background:#0d1117;padding:15px;border-radius:8px;">${escapeHtml(_taskLogOutput) || '<span style="color:#8899a6;">等待输出...</span>'}</div>
|
|
|
`;
|
|
|
document.getElementById('taskDetailModal').classList.add('active');
|
|
|
+
|
|
|
+ // 运行中的任务用 SSE 实时推送
|
|
|
+ if (isRunning) {
|
|
|
+ const sseUrl = `${API_BASE}/tasks/${id}/stream`;
|
|
|
+ _taskLogSSE = new EventSource(sseUrl);
|
|
|
+ _taskLogSSE.addEventListener('log', function(e) {
|
|
|
+ _taskLogOutput += e.data;
|
|
|
+ const terminal = document.getElementById('taskLogTerminal');
|
|
|
+ if (terminal) {
|
|
|
+ terminal.textContent = _taskLogOutput;
|
|
|
+ terminal.scrollTop = terminal.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ _taskLogSSE.addEventListener('status', function(e) {
|
|
|
+ const statusEl = content.querySelector('.status');
|
|
|
+ if (statusEl) {
|
|
|
+ statusEl.className = `status ${e.data}`;
|
|
|
+ statusEl.textContent = statusText(e.data);
|
|
|
+ }
|
|
|
+ // 移除 spinner 和更新提示
|
|
|
+ const spinner = content.querySelector('.spinner');
|
|
|
+ if (spinner) spinner.remove();
|
|
|
+ const hint = content.querySelector('h3 span');
|
|
|
+ if (hint) hint.remove();
|
|
|
+ });
|
|
|
+ _taskLogSSE.addEventListener('error', function(e) {
|
|
|
+ // SSE 连接关闭(正常完成或断开)
|
|
|
+ if (_taskLogSSE) {
|
|
|
+ _taskLogSSE.close();
|
|
|
+ _taskLogSSE = null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
} else {
|
|
|
showToast('获取任务详情失败', 'error');
|
|
|
}
|