Histórico persistente de execuções com visualização detalhada
- Novo executionsStore em db.js com cache in-memory e escrita debounced - Camada de cache (src/cache/index.js) com TTL e suporte opcional a Redis - Persistência de execuções de agentes e pipelines com metadados completos - Pipeline grava cada etapa com prompt, resultado, timestamps e status - 4 endpoints REST: listagem paginada com filtros, detalhe, exclusão individual e limpeza total - Componente frontend (history.js) com cards, filtros, paginação e modal de detalhe - Timeline visual para pipelines com prompts colapsáveis por etapa - Correção do executor: --max-turns em vez de --max-tokens, --permission-mode bypassPermissions - Refatoração do scheduler com persistência melhorada e graceful shutdown
This commit is contained in:
@@ -3295,3 +3295,429 @@ tbody tr:hover td {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.toolbar-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-agent {
|
||||
background-color: rgba(99, 102, 241, 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.badge-pipeline {
|
||||
background-color: rgba(139, 92, 246, 0.15);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
transition: all 0.2s;
|
||||
animation: fadeInUp 0.2s ease both;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-card:hover {
|
||||
border-color: var(--border-secondary);
|
||||
background-color: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.history-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.history-card-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-card-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-card-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history-card-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.history-card-task {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-card-duration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-card-duration i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.execution-result {
|
||||
background-color: #08080e;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: #c8c8d8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.execution-result--error {
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.execution-result--prompt {
|
||||
color: var(--text-secondary);
|
||||
background-color: var(--bg-tertiary);
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.execution-detail-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.execution-detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.execution-detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.execution-detail-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.execution-detail-value {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.execution-detail-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.execution-detail-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.execution-detail-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.execution-detail-task {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.pipeline-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.pipeline-step-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pipeline-step-connector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.pipeline-step-node {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 2px solid var(--border-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pipeline-step-node--completed {
|
||||
background-color: rgba(34, 197, 94, 0.15);
|
||||
border-color: var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.pipeline-step-node--error {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.pipeline-step-connector-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 24px;
|
||||
background-color: var(--border-secondary);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.pipeline-step-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pipeline-step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pipeline-step-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pipeline-step-agent {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pipeline-step-duration {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pipeline-step-duration i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.pipeline-step-prompt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pipeline-step-prompt-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.pipeline-step-prompt-toggle:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.pipeline-step-prompt-toggle[aria-expanded="true"] i {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.pipeline-step-prompt-toggle i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.pipeline-step-prompt-body {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pipeline-step-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pipeline-step-result-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pagination-page {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-input);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.input--sm {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@
|
||||
<span>Terminal</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="history">
|
||||
<i data-lucide="history"></i>
|
||||
<span>Histórico</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="settings">
|
||||
<i data-lucide="settings"></i>
|
||||
@@ -387,6 +393,36 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="history" class="section" aria-label="Histórico de Execuções" hidden>
|
||||
<div class="section-toolbar">
|
||||
<div class="toolbar-filters">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Buscar..." id="history-search" aria-label="Buscar no histórico" />
|
||||
</div>
|
||||
<select class="select" id="history-filter-type" aria-label="Filtrar por tipo">
|
||||
<option value="">Todos os tipos</option>
|
||||
<option value="agent">Agentes</option>
|
||||
<option value="pipeline">Pipelines</option>
|
||||
</select>
|
||||
<select class="select" id="history-filter-status" aria-label="Filtrar por status">
|
||||
<option value="">Todos os status</option>
|
||||
<option value="completed">Concluído</option>
|
||||
<option value="error">Erro</option>
|
||||
<option value="running">Em execução</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button class="btn btn-ghost btn-sm btn-danger" id="history-clear-btn" type="button">
|
||||
<i data-lucide="trash-2"></i>
|
||||
Limpar Histórico
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="history-list"></div>
|
||||
<div id="history-pagination"></div>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="section" aria-label="Configurações" hidden>
|
||||
<div class="settings-grid">
|
||||
<div class="card">
|
||||
@@ -864,6 +900,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="execution-detail-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="execution-detail-title" hidden>
|
||||
<div class="modal modal-lg">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="execution-detail-title">Detalhe da Execução</h2>
|
||||
<button class="modal-close" data-modal-close="execution-detail-modal-overlay" aria-label="Fechar" type="button">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="execution-detail-content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="confirm-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title" hidden>
|
||||
<div class="modal modal--sm">
|
||||
<div class="modal-header">
|
||||
@@ -954,6 +1003,7 @@
|
||||
<script src="js/components/schedules.js"></script>
|
||||
<script src="js/components/pipelines.js"></script>
|
||||
<script src="js/components/settings.js"></script>
|
||||
<script src="js/components/history.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
@@ -82,6 +82,13 @@ const API = {
|
||||
|
||||
executions: {
|
||||
recent(limit = 20) { return API.request('GET', `/executions/recent?limit=${limit}`); },
|
||||
history(params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return API.request('GET', `/executions/history${qs ? '?' + qs : ''}`);
|
||||
},
|
||||
get(id) { return API.request('GET', `/executions/history/${id}`); },
|
||||
delete(id) { return API.request('DELETE', `/executions/history/${id}`); },
|
||||
clearAll() { return API.request('DELETE', '/executions/history'); },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const App = {
|
||||
schedules: 'Agendamentos',
|
||||
pipelines: 'Pipelines',
|
||||
terminal: 'Terminal',
|
||||
history: 'Histórico',
|
||||
settings: 'Configurações',
|
||||
},
|
||||
|
||||
@@ -70,6 +71,7 @@ const App = {
|
||||
case 'tasks': await TasksUI.load(); break;
|
||||
case 'schedules': await SchedulesUI.load(); break;
|
||||
case 'pipelines': await PipelinesUI.load(); break;
|
||||
case 'history': await HistoryUI.load(); break;
|
||||
case 'settings': await SettingsUI.load(); break;
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -363,6 +365,32 @@ const App = {
|
||||
PipelinesUI.filter(document.getElementById('pipelines-search')?.value);
|
||||
});
|
||||
|
||||
on('history-search', 'input', () => {
|
||||
HistoryUI.filter(
|
||||
document.getElementById('history-search')?.value,
|
||||
document.getElementById('history-filter-type')?.value,
|
||||
document.getElementById('history-filter-status')?.value
|
||||
);
|
||||
});
|
||||
|
||||
on('history-filter-type', 'change', () => {
|
||||
HistoryUI.filter(
|
||||
document.getElementById('history-search')?.value,
|
||||
document.getElementById('history-filter-type')?.value,
|
||||
document.getElementById('history-filter-status')?.value
|
||||
);
|
||||
});
|
||||
|
||||
on('history-filter-status', 'change', () => {
|
||||
HistoryUI.filter(
|
||||
document.getElementById('history-search')?.value,
|
||||
document.getElementById('history-filter-type')?.value,
|
||||
document.getElementById('history-filter-status')?.value
|
||||
);
|
||||
});
|
||||
|
||||
on('history-clear-btn', 'click', () => HistoryUI.clearHistory());
|
||||
|
||||
document.getElementById('agents-grid')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
@@ -419,6 +447,16 @@ const App = {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('history-list')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
const { action, id } = btn.dataset;
|
||||
switch (action) {
|
||||
case 'view-execution': HistoryUI.viewDetail(id); break;
|
||||
case 'delete-execution': HistoryUI.deleteExecution(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-step-action]');
|
||||
if (!btn) return;
|
||||
|
||||
421
public/js/components/history.js
Normal file
421
public/js/components/history.js
Normal file
@@ -0,0 +1,421 @@
|
||||
const HistoryUI = {
|
||||
executions: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
pageSize: 20,
|
||||
_currentSearch: '',
|
||||
_currentType: '',
|
||||
_currentStatus: '',
|
||||
|
||||
async load() {
|
||||
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>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = HistoryUI.executions.map((exec) => HistoryUI._renderCard(exec)).join('');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [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 task = exec.type === 'pipeline'
|
||||
? (exec.input || '')
|
||||
: (exec.task || '');
|
||||
const date = HistoryUI._formatDate(exec.startedAt);
|
||||
const duration = HistoryUI._formatDuration(exec.startedAt, exec.endedAt);
|
||||
|
||||
return `
|
||||
<article class="history-card">
|
||||
<div class="history-card-header">
|
||||
<div class="history-card-identity">
|
||||
${typeBadge}
|
||||
<span class="history-card-name">${HistoryUI._escapeHtml(name)}</span>
|
||||
</div>
|
||||
<div class="history-card-status">
|
||||
${statusBadge}
|
||||
<span class="history-card-date">${date}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-card-meta">
|
||||
<span class="history-card-task">${HistoryUI._escapeHtml(task)}</span>
|
||||
<span class="history-card-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${duration}
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [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');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [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">${HistoryUI._escapeHtml(exec.result)}</div>`
|
||||
: '';
|
||||
|
||||
const errorBlock = exec.error
|
||||
? `<div class="execution-result execution-result--error" role="alert">${HistoryUI._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">${HistoryUI._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>` : ''}
|
||||
</div>
|
||||
${exec.task ? `
|
||||
<div class="execution-detail-section">
|
||||
<h3 class="execution-detail-section-title">Tarefa</h3>
|
||||
<p class="execution-detail-task">${HistoryUI._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">${HistoryUI._escapeHtml(step.agentName || step.agentId || 'Agente')}</span>
|
||||
${HistoryUI._statusBadge(step.status)}
|
||||
</div>
|
||||
<span class="pipeline-step-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${stepDuration}
|
||||
</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">${HistoryUI._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">${HistoryUI._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">${HistoryUI._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>
|
||||
</div>
|
||||
${exec.input ? `
|
||||
<div class="execution-detail-section">
|
||||
<h3 class="execution-detail-section-title">Input Inicial</h3>
|
||||
<p class="execution-detail-task">${HistoryUI._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">${HistoryUI._escapeHtml(exec.error)}</div>
|
||||
</div>` : ''}
|
||||
`;
|
||||
},
|
||||
|
||||
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'],
|
||||
};
|
||||
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',
|
||||
});
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
};
|
||||
|
||||
window.HistoryUI = HistoryUI;
|
||||
Reference in New Issue
Block a user