Files
Agents-Orchestrator/public/js/components/history.js
Frederico Castro da22154f66 Evolução da plataforma: dashboard com gráficos, notificações, relatórios automáticos, ícones Lucide local e melhorias gerais
- Dashboard com 5 gráficos Chart.js (execuções, status, custo, agentes, pipelines)
- Sistema de notificações com polling, badge e Browser Notification API
- Relatórios MD automáticos para execuções de agentes e pipelines (data/reports/)
- Lucide local (v0.475.0) com nomes de ícones atualizados e refreshIcons centralizado
- Correção de ícones icon-only (padding CSS sobrescrito por btn-sm)
- Cards de agentes e pipelines com botões alinhados na base (flex column)
- Terminal com busca, download, cópia e auto-scroll toggle
- Histórico com export CSV, retry, paginação e truncamento de texto
- Webhooks com edição e teste inline
- Duplicação de agentes e export/import JSON
- Rate limiting, CORS, correlação de requests e health check no backend
- Escrita atômica em JSON (temp + rename) e store de notificações
- Tema claro/escuro com toggle e persistência em localStorage
- Atalhos de teclado 1-9 para navegação entre seções
2026-02-26 20:41:17 -03:00

465 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
const HistoryUI = {
executions: [],
total: 0,
page: 0,
pageSize: 20,
_currentSearch: '',
_currentType: '',
_currentStatus: '',
_exportListenerAdded: false,
async load() {
if (!HistoryUI._exportListenerAdded) {
HistoryUI._exportListenerAdded = true;
const exportBtn = document.getElementById('history-export-csv');
if (exportBtn) {
exportBtn.addEventListener('click', () => API.executions.exportCsv());
}
}
const params = { limit: HistoryUI.pageSize, offset: HistoryUI.page * HistoryUI.pageSize };
if (HistoryUI._currentType) params.type = HistoryUI._currentType;
if (HistoryUI._currentStatus) params.status = HistoryUI._currentStatus;
if (HistoryUI._currentSearch) params.search = HistoryUI._currentSearch;
try {
const data = await API.executions.history(params);
HistoryUI.executions = data.items || [];
HistoryUI.total = data.total || 0;
HistoryUI.render();
HistoryUI._renderPagination();
} catch (err) {
Toast.error(`Erro ao carregar histórico: ${err.message}`);
}
},
render() {
const container = document.getElementById('history-list');
if (!container) return;
if (HistoryUI.executions.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">
<i data-lucide="history"></i>
</div>
<h3 class="empty-state-title">Nenhuma execução encontrada</h3>
<p class="empty-state-text">O histórico de execuções aparecerá aqui.</p>
</div>
`;
Utils.refreshIcons(container);
return;
}
container.innerHTML = HistoryUI.executions.map((exec) => HistoryUI._renderCard(exec)).join('');
Utils.refreshIcons(container);
},
_renderCard(exec) {
const typeBadge = exec.type === 'pipeline'
? '<span class="badge badge-pipeline">Pipeline</span>'
: '<span class="badge badge-agent">Agente</span>';
const statusBadge = HistoryUI._statusBadge(exec.status);
const name = exec.type === 'pipeline'
? (exec.pipelineName || 'Pipeline')
: (exec.agentName || 'Agente');
const taskRaw = exec.type === 'pipeline'
? (exec.input || '')
: (exec.task || '');
const task = taskRaw.length > 150 ? taskRaw.slice(0, 150) + '…' : taskRaw;
const date = HistoryUI._formatDate(exec.startedAt);
const duration = HistoryUI._formatDuration(exec.startedAt, exec.endedAt);
const cost = exec.costUsd || exec.totalCostUsd || 0;
const costHtml = cost > 0
? `<span class="history-card-cost"><i data-lucide="dollar-sign" aria-hidden="true"></i>$${cost.toFixed(4)}</span>`
: '';
return `
<article class="history-card">
<div class="history-card-header">
<div class="history-card-identity">
${typeBadge}
<span class="history-card-name">${Utils.escapeHtml(name)}</span>
${statusBadge}
</div>
</div>
<div class="history-card-task" title="${Utils.escapeHtml(taskRaw)}">${Utils.escapeHtml(task)}</div>
<div class="history-card-info">
<span class="history-card-date">
<i data-lucide="calendar" aria-hidden="true"></i>
${date}
</span>
<span class="history-card-duration">
<i data-lucide="clock" aria-hidden="true"></i>
${duration}
</span>
${costHtml}
</div>
<div class="history-card-actions">
<button class="btn btn-ghost btn-sm" data-action="view-execution" data-id="${exec.id}" type="button">
<i data-lucide="eye"></i>
Ver detalhes
</button>
${(exec.status === 'error' || exec.status === 'canceled') ? `
<button class="btn btn-ghost btn-sm" data-action="retry" data-id="${exec.id}" type="button" title="Reexecutar">
<i data-lucide="refresh-cw"></i>
</button>` : ''}
<button class="btn btn-ghost btn-sm btn-danger" data-action="delete-execution" data-id="${exec.id}" type="button" aria-label="Excluir execução">
<i data-lucide="trash-2"></i>
</button>
</div>
</article>
`;
},
_renderPagination() {
const container = document.getElementById('history-pagination');
if (!container) return;
const totalPages = Math.ceil(HistoryUI.total / HistoryUI.pageSize);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
const hasPrev = HistoryUI.page > 0;
const hasNext = HistoryUI.page < totalPages - 1;
const start = HistoryUI.page * HistoryUI.pageSize + 1;
const end = Math.min((HistoryUI.page + 1) * HistoryUI.pageSize, HistoryUI.total);
container.innerHTML = `
<div class="pagination">
<span class="pagination-info">${start}${end} de ${HistoryUI.total}</span>
<div class="pagination-controls">
<button class="btn btn-ghost btn-sm" id="history-prev-btn" type="button" ${hasPrev ? '' : 'disabled'}>
<i data-lucide="chevron-left"></i>
Anterior
</button>
<span class="pagination-page">Página ${HistoryUI.page + 1} de ${totalPages}</span>
<button class="btn btn-ghost btn-sm" id="history-next-btn" type="button" ${hasNext ? '' : 'disabled'}>
Próxima
<i data-lucide="chevron-right"></i>
</button>
</div>
</div>
`;
Utils.refreshIcons(container);
document.getElementById('history-prev-btn')?.addEventListener('click', () => {
HistoryUI.page--;
HistoryUI.load();
});
document.getElementById('history-next-btn')?.addEventListener('click', () => {
HistoryUI.page++;
HistoryUI.load();
});
},
filter(search, type, status) {
HistoryUI._currentSearch = search || '';
HistoryUI._currentType = type || '';
HistoryUI._currentStatus = status || '';
HistoryUI.page = 0;
HistoryUI.load();
},
async viewDetail(id) {
try {
const exec = await API.executions.get(id);
const modal = document.getElementById('execution-detail-modal-overlay');
const title = document.getElementById('execution-detail-title');
const content = document.getElementById('execution-detail-content');
if (!modal || !title || !content) return;
const name = exec.type === 'pipeline'
? (exec.pipelineName || 'Pipeline')
: (exec.agentName || 'Agente');
title.textContent = name;
content.innerHTML = exec.type === 'pipeline'
? HistoryUI._renderPipelineDetail(exec)
: HistoryUI._renderAgentDetail(exec);
Modal.open('execution-detail-modal-overlay');
Utils.refreshIcons(content);
content.querySelectorAll('.pipeline-step-prompt-toggle').forEach((btn) => {
btn.addEventListener('click', () => {
const stepCard = btn.closest('.pipeline-step-detail');
const promptBody = stepCard?.querySelector('.pipeline-step-prompt-body');
if (!promptBody) return;
const isHidden = promptBody.hidden;
promptBody.hidden = !isHidden;
btn.setAttribute('aria-expanded', String(isHidden));
});
});
} catch (err) {
Toast.error(`Erro ao carregar execução: ${err.message}`);
}
},
_renderAgentDetail(exec) {
const duration = HistoryUI._formatDuration(exec.startedAt, exec.endedAt);
const startDate = HistoryUI._formatDate(exec.startedAt);
const endDate = exec.endedAt ? HistoryUI._formatDate(exec.endedAt) : '—';
const resultBlock = exec.result
? `<div class="execution-result" role="region" aria-label="Resultado da execução">${Utils.escapeHtml(exec.result)}</div>`
: '';
const errorBlock = exec.error
? `<div class="execution-result execution-result--error" role="alert">${Utils.escapeHtml(exec.error)}</div>`
: '';
return `
<div class="execution-detail-meta">
<div class="execution-detail-row">
<span class="execution-detail-label">Agente</span>
<span class="execution-detail-value">${Utils.escapeHtml(exec.agentName || exec.agentId || '—')}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Status</span>
<span class="execution-detail-value">${HistoryUI._statusBadge(exec.status)}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Início</span>
<span class="execution-detail-value">${startDate}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Fim</span>
<span class="execution-detail-value">${endDate}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Duração</span>
<span class="execution-detail-value">${duration}</span>
</div>
${exec.exitCode !== undefined && exec.exitCode !== null ? `
<div class="execution-detail-row">
<span class="execution-detail-label">Exit Code</span>
<span class="execution-detail-value font-mono">${exec.exitCode}</span>
</div>` : ''}
${exec.costUsd || exec.totalCostUsd ? `
<div class="execution-detail-row">
<span class="execution-detail-label">Custo</span>
<span class="execution-detail-value cost-value">$${(exec.costUsd || exec.totalCostUsd || 0).toFixed(4)}</span>
</div>` : ''}
${exec.numTurns ? `
<div class="execution-detail-row">
<span class="execution-detail-label">Turnos</span>
<span class="execution-detail-value font-mono">${exec.numTurns}</span>
</div>` : ''}
</div>
${exec.task ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Tarefa</h3>
<p class="execution-detail-task">${Utils.escapeHtml(exec.task)}</p>
</div>` : ''}
${resultBlock ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Resultado</h3>
${resultBlock}
</div>` : ''}
${errorBlock ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Erro</h3>
${errorBlock}
</div>` : ''}
`;
},
_renderPipelineDetail(exec) {
const duration = HistoryUI._formatDuration(exec.startedAt, exec.endedAt);
const startDate = HistoryUI._formatDate(exec.startedAt);
const endDate = exec.endedAt ? HistoryUI._formatDate(exec.endedAt) : '—';
const steps = Array.isArray(exec.steps) ? exec.steps : [];
const stepsHtml = steps.map((step, index) => {
const stepDuration = HistoryUI._formatDuration(step.startedAt, step.endedAt);
const isLast = index === steps.length - 1;
return `
<div class="pipeline-step-item">
<div class="pipeline-step-connector">
<div class="pipeline-step-node ${step.status === 'error' ? 'pipeline-step-node--error' : step.status === 'completed' ? 'pipeline-step-node--completed' : ''}">
<span>${step.stepIndex + 1}</span>
</div>
${isLast ? '' : '<div class="pipeline-step-connector-line" aria-hidden="true"></div>'}
</div>
<div class="pipeline-step-detail">
<div class="pipeline-step-header">
<div class="pipeline-step-identity">
<span class="pipeline-step-agent">${Utils.escapeHtml(step.agentName || step.agentId || 'Agente')}</span>
${HistoryUI._statusBadge(step.status)}
</div>
<span class="pipeline-step-meta-group">
<span class="pipeline-step-duration">
<i data-lucide="clock" aria-hidden="true"></i>
${stepDuration}
</span>
${step.costUsd ? `<span class="pipeline-step-cost">$${step.costUsd.toFixed(4)}</span>` : ''}
</span>
</div>
${step.prompt ? `
<div class="pipeline-step-prompt">
<button class="pipeline-step-prompt-toggle" type="button" aria-expanded="false">
<i data-lucide="chevron-right"></i>
Prompt utilizado
</button>
<div class="pipeline-step-prompt-body" hidden>
<div class="execution-result execution-result--prompt">${Utils.escapeHtml(step.prompt)}</div>
</div>
</div>` : ''}
${step.result ? `
<div class="pipeline-step-result">
<span class="pipeline-step-result-label">Resultado</span>
<div class="execution-result">${Utils.escapeHtml(step.result)}</div>
</div>` : ''}
${step.status === 'error' ? `
<div class="execution-result execution-result--error">Passo falhou.</div>` : ''}
</div>
</div>
`;
}).join('');
return `
<div class="execution-detail-meta">
<div class="execution-detail-row">
<span class="execution-detail-label">Pipeline</span>
<span class="execution-detail-value">${Utils.escapeHtml(exec.pipelineName || exec.pipelineId || '—')}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Status</span>
<span class="execution-detail-value">${HistoryUI._statusBadge(exec.status)}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Início</span>
<span class="execution-detail-value">${startDate}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Fim</span>
<span class="execution-detail-value">${endDate}</span>
</div>
<div class="execution-detail-row">
<span class="execution-detail-label">Duração</span>
<span class="execution-detail-value">${duration}</span>
</div>
${exec.totalCostUsd ? `
<div class="execution-detail-row">
<span class="execution-detail-label">Custo Total</span>
<span class="execution-detail-value cost-value">$${(exec.totalCostUsd || 0).toFixed(4)}</span>
</div>` : ''}
</div>
${exec.input ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Input Inicial</h3>
<p class="execution-detail-task">${Utils.escapeHtml(exec.input)}</p>
</div>` : ''}
${steps.length > 0 ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Passos do Pipeline</h3>
<div class="pipeline-timeline">
${stepsHtml}
</div>
</div>` : ''}
${exec.error ? `
<div class="execution-detail-section">
<h3 class="execution-detail-section-title">Erro</h3>
<div class="execution-result execution-result--error">${Utils.escapeHtml(exec.error)}</div>
</div>` : ''}
`;
},
async retryExecution(id) {
try {
await API.executions.retry(id);
Toast.success('Execução reiniciada');
App.navigateTo('terminal');
} catch (err) {
Toast.error(`Erro ao reexecutar: ${err.message}`);
}
},
async deleteExecution(id) {
const confirmed = await Modal.confirm(
'Excluir execução',
'Tem certeza que deseja excluir esta execução do histórico? Esta ação não pode ser desfeita.'
);
if (!confirmed) return;
try {
await API.executions.delete(id);
Toast.success('Execução excluída do histórico');
await HistoryUI.load();
} catch (err) {
Toast.error(`Erro ao excluir execução: ${err.message}`);
}
},
async clearHistory() {
const confirmed = await Modal.confirm(
'Limpar histórico',
'Tem certeza que deseja excluir todo o histórico de execuções? Esta ação não pode ser desfeita.'
);
if (!confirmed) return;
try {
await API.executions.clearAll();
Toast.success('Histórico limpo com sucesso');
HistoryUI.page = 0;
await HistoryUI.load();
} catch (err) {
Toast.error(`Erro ao limpar histórico: ${err.message}`);
}
},
_statusBadge(status) {
const map = {
running: ['badge-running', 'Em execução'],
completed: ['badge-active', 'Concluído'],
error: ['badge-error', 'Erro'],
awaiting_approval: ['badge-warning', 'Aguardando'],
rejected: ['badge-error', 'Rejeitado'],
canceled: ['badge-inactive', 'Cancelado'],
};
const [cls, label] = map[status] || ['badge-inactive', status || 'Desconhecido'];
return `<span class="badge ${cls}">${label}</span>`;
},
_formatDuration(start, end) {
if (!start) return '—';
const startMs = new Date(start).getTime();
const endMs = end ? new Date(end).getTime() : Date.now();
const totalSeconds = Math.floor((endMs - startMs) / 1000);
if (totalSeconds < 0) return '—';
if (totalSeconds < 60) return `${totalSeconds}s`;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m ${seconds}s`;
},
_formatDate(iso) {
if (!iso) return '—';
const date = new Date(iso);
return date.toLocaleString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
},
};
window.HistoryUI = HistoryUI;