Continuação de conversa no terminal, histórico de agendamentos, webhooks e melhorias gerais
- Terminal com input de chat: após execução, permite continuar conversa com o agente via --resume do CLI, mantendo contexto da sessão (sessionId persistido) - Nova rota POST /api/agents/:id/continue para retomar sessões - Executor com função resume() para spawnar claude com --resume <sessionId> - Histórico de agendamentos agora busca do executionsStore (persistente) com dados completos: agente, tarefa, status, duração, custo e link para detalhes no modal - Execuções de agendamento tagueadas com source:'schedule' e scheduleId - Correção da expressão cron duplicada na UI de agendamentos - cronToHuman trata expressões com minuto específico (ex: 37 3 * * * → Todo dia às 03:37) - Botão "Copiar cURL" nos cards de webhook com payload de exemplo contextual - Webhooks component (webhooks.js) adicionado ao repositório
This commit is contained in:
@@ -29,6 +29,18 @@ const DashboardUI = {
|
||||
const current = parseInt(el.textContent, 10) || 0;
|
||||
DashboardUI._animateCount(el, current, target);
|
||||
}
|
||||
|
||||
const costEl = document.getElementById('metric-cost-today');
|
||||
if (costEl) {
|
||||
const cost = status.costs?.today ?? 0;
|
||||
costEl.textContent = `$${cost.toFixed(4)}`;
|
||||
}
|
||||
|
||||
const webhooksEl = document.getElementById('metric-webhooks');
|
||||
if (webhooksEl) {
|
||||
const current = parseInt(webhooksEl.textContent, 10) || 0;
|
||||
DashboardUI._animateCount(webhooksEl, current, status.webhooks?.active ?? 0);
|
||||
}
|
||||
},
|
||||
|
||||
_animateCount(el, from, to) {
|
||||
@@ -77,6 +89,10 @@ const DashboardUI = {
|
||||
const date = exec.startedAt
|
||||
? new Date(exec.startedAt).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
|
||||
: '';
|
||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||
const costHtml = cost > 0
|
||||
? `<span class="activity-item-cost">$${cost.toFixed(4)}</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<li class="activity-item">
|
||||
@@ -85,6 +101,7 @@ const DashboardUI = {
|
||||
<span class="activity-item-task">${taskText.length > 80 ? taskText.slice(0, 80) + '...' : taskText}</span>
|
||||
</div>
|
||||
<div class="activity-item-meta">
|
||||
${costHtml}
|
||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||
<span class="activity-item-time">${date} ${time}</span>
|
||||
</div>
|
||||
@@ -107,7 +124,9 @@ const DashboardUI = {
|
||||
running: 'badge--blue',
|
||||
completed: 'badge--green',
|
||||
error: 'badge--red',
|
||||
cancelled: 'badge--gray',
|
||||
canceled: 'badge--gray',
|
||||
awaiting_approval: 'badge--yellow',
|
||||
rejected: 'badge--red',
|
||||
};
|
||||
return map[status] || 'badge--gray';
|
||||
},
|
||||
@@ -117,7 +136,9 @@ const DashboardUI = {
|
||||
running: 'Em execução',
|
||||
completed: 'Concluído',
|
||||
error: 'Erro',
|
||||
cancelled: 'Cancelado',
|
||||
canceled: 'Cancelado',
|
||||
awaiting_approval: 'Aguardando',
|
||||
rejected: 'Rejeitado',
|
||||
};
|
||||
return map[status] || status || 'Desconhecido';
|
||||
},
|
||||
|
||||
@@ -60,6 +60,10 @@ const HistoryUI = {
|
||||
: (exec.task || '');
|
||||
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">
|
||||
@@ -75,9 +79,12 @@ const HistoryUI = {
|
||||
</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 class="history-card-duration-group">
|
||||
<span class="history-card-duration">
|
||||
<i data-lucide="clock" aria-hidden="true"></i>
|
||||
${duration}
|
||||
</span>
|
||||
${costHtml}
|
||||
</span>
|
||||
</div>
|
||||
<div class="history-card-actions">
|
||||
@@ -222,6 +229,16 @@ const HistoryUI = {
|
||||
<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">
|
||||
@@ -265,9 +282,12 @@ const HistoryUI = {
|
||||
<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 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 ? `
|
||||
@@ -314,6 +334,11 @@ const HistoryUI = {
|
||||
<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">
|
||||
@@ -373,6 +398,9 @@ const HistoryUI = {
|
||||
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>`;
|
||||
|
||||
@@ -3,6 +3,7 @@ const PipelinesUI = {
|
||||
agents: [],
|
||||
_editingId: null,
|
||||
_steps: [],
|
||||
_pendingApprovals: new Map(),
|
||||
|
||||
async load() {
|
||||
try {
|
||||
@@ -84,10 +85,13 @@ const PipelinesUI = {
|
||||
const flowHtml = steps.map((step, index) => {
|
||||
const agentName = step.agentName || step.agentId || 'Agente';
|
||||
const isLast = index === steps.length - 1;
|
||||
const approvalIcon = step.requiresApproval && index > 0
|
||||
? '<i data-lucide="shield-check" style="width:10px;height:10px;color:var(--warning)"></i> '
|
||||
: '';
|
||||
return `
|
||||
<span class="pipeline-step-badge">
|
||||
<span class="pipeline-step-number">${index + 1}</span>
|
||||
${agentName}
|
||||
${approvalIcon}${agentName}
|
||||
</span>
|
||||
${!isLast ? '<span class="pipeline-flow-arrow">→</span>' : ''}
|
||||
`;
|
||||
@@ -130,8 +134,8 @@ const PipelinesUI = {
|
||||
openCreateModal() {
|
||||
PipelinesUI._editingId = null;
|
||||
PipelinesUI._steps = [
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
{ agentId: '', inputTemplate: '', requiresApproval: false },
|
||||
{ agentId: '', inputTemplate: '', requiresApproval: false },
|
||||
];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
@@ -156,7 +160,7 @@ const PipelinesUI = {
|
||||
|
||||
PipelinesUI._editingId = pipelineId;
|
||||
PipelinesUI._steps = Array.isArray(pipeline.steps)
|
||||
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '' }))
|
||||
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '', requiresApproval: !!s.requiresApproval }))
|
||||
: [];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
@@ -198,6 +202,15 @@ const PipelinesUI = {
|
||||
? '<div class="pipeline-step-connector"><i data-lucide="arrow-down" style="width:14px;height:14px"></i></div>'
|
||||
: '';
|
||||
|
||||
const approvalChecked = step.requiresApproval ? 'checked' : '';
|
||||
const approvalHtml = index > 0
|
||||
? `<label class="pipeline-step-approval">
|
||||
<input type="checkbox" data-step-field="requiresApproval" data-step-index="${index}" ${approvalChecked} />
|
||||
<i data-lucide="shield-check" style="width:12px;height:12px"></i>
|
||||
<span>Requer aprovação</span>
|
||||
</label>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="pipeline-step-row" data-step-index="${index}">
|
||||
<span class="pipeline-step-number-lg">${index + 1}</span>
|
||||
@@ -213,6 +226,7 @@ const PipelinesUI = {
|
||||
data-step-field="inputTemplate"
|
||||
data-step-index="${index}"
|
||||
>${step.inputTemplate || ''}</textarea>
|
||||
${approvalHtml}
|
||||
</div>
|
||||
<div class="pipeline-step-actions">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-up" data-step-index="${index}" title="Mover para cima" ${isFirst ? 'disabled' : ''}>
|
||||
@@ -246,14 +260,18 @@ const PipelinesUI = {
|
||||
const index = parseInt(el.dataset.stepIndex, 10);
|
||||
const field = el.dataset.stepField;
|
||||
if (PipelinesUI._steps[index] !== undefined) {
|
||||
PipelinesUI._steps[index][field] = el.value;
|
||||
if (el.type === 'checkbox') {
|
||||
PipelinesUI._steps[index][field] = el.checked;
|
||||
} else {
|
||||
PipelinesUI._steps[index][field] = el.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStep() {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
PipelinesUI._steps.push({ agentId: '', inputTemplate: '' });
|
||||
PipelinesUI._steps.push({ agentId: '', inputTemplate: '', requiresApproval: false });
|
||||
PipelinesUI.renderSteps();
|
||||
},
|
||||
|
||||
@@ -299,6 +317,7 @@ const PipelinesUI = {
|
||||
steps: PipelinesUI._steps.map((s) => ({
|
||||
agentId: s.agentId,
|
||||
inputTemplate: s.inputTemplate || '',
|
||||
requiresApproval: !!s.requiresApproval,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -347,12 +366,16 @@ const PipelinesUI = {
|
||||
const inputEl = document.getElementById('pipeline-execute-input');
|
||||
if (inputEl) inputEl.value = '';
|
||||
|
||||
const workdirEl = document.getElementById('pipeline-execute-workdir');
|
||||
if (workdirEl) workdirEl.value = '';
|
||||
|
||||
Modal.open('pipeline-execute-modal-overlay');
|
||||
},
|
||||
|
||||
async _executeFromModal() {
|
||||
const pipelineId = document.getElementById('pipeline-execute-id')?.value;
|
||||
const input = document.getElementById('pipeline-execute-input')?.value.trim();
|
||||
const workingDirectory = document.getElementById('pipeline-execute-workdir')?.value.trim() || '';
|
||||
|
||||
if (!input) {
|
||||
Toast.warning('O input inicial é obrigatório');
|
||||
@@ -360,7 +383,7 @@ const PipelinesUI = {
|
||||
}
|
||||
|
||||
try {
|
||||
await API.pipelines.execute(pipelineId, input);
|
||||
await API.pipelines.execute(pipelineId, input, workingDirectory);
|
||||
Modal.close('pipeline-execute-modal-overlay');
|
||||
App.navigateTo('terminal');
|
||||
Toast.info('Pipeline iniciado');
|
||||
|
||||
@@ -47,8 +47,7 @@ const SchedulesUI = {
|
||||
<td>${schedule.agentName || '—'}</td>
|
||||
<td class="schedule-task-cell" title="${schedule.taskDescription || ''}">${schedule.taskDescription || '—'}</td>
|
||||
<td>
|
||||
<span title="${cronExpr}">${humanCron}</span>
|
||||
<small class="font-mono">${cronExpr}</small>
|
||||
<code class="font-mono">${cronExpr}</code>
|
||||
</td>
|
||||
<td>${nextRun}</td>
|
||||
<td><span class="badge ${statusClass}">${statusLabel}</span></td>
|
||||
@@ -214,19 +213,84 @@ const SchedulesUI = {
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<ul class="activity-list">
|
||||
${history.slice(0, 20).map((h) => `
|
||||
<li class="activity-item">
|
||||
<div class="activity-item-info">
|
||||
<span class="activity-item-agent">${h.cronExpr}</span>
|
||||
</div>
|
||||
<div class="activity-item-meta">
|
||||
<span class="activity-item-time">${new Date(h.firedAt).toLocaleString('pt-BR')}</span>
|
||||
</div>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Agente</th>
|
||||
<th scope="col">Tarefa</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Data</th>
|
||||
<th scope="col">Duração</th>
|
||||
<th scope="col">Custo</th>
|
||||
<th scope="col" aria-label="Ações"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${history.map((exec) => {
|
||||
const status = SchedulesUI._statusBadge(exec.status);
|
||||
const date = exec.startedAt ? new Date(exec.startedAt).toLocaleString('pt-BR') : '—';
|
||||
const duration = SchedulesUI._formatDuration(exec.startedAt, exec.endedAt);
|
||||
const cost = exec.costUsd || exec.totalCostUsd || 0;
|
||||
const costStr = cost > 0 ? `$${cost.toFixed(4)}` : '—';
|
||||
const taskStr = SchedulesUI._escapeHtml(SchedulesUI._truncate(exec.task || '', 60));
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${SchedulesUI._escapeHtml(exec.agentName || '—')}</td>
|
||||
<td title="${SchedulesUI._escapeHtml(exec.task || '')}">${taskStr}</td>
|
||||
<td>${status}</td>
|
||||
<td>${date}</td>
|
||||
<td>${duration}</td>
|
||||
<td class="font-mono">${costStr}</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm" data-action="view-schedule-exec" data-id="${exec.id}" type="button" title="Ver resultado">
|
||||
<i data-lucide="eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_statusBadge(status) {
|
||||
const map = {
|
||||
running: ['badge-running', 'Executando'],
|
||||
completed: ['badge-active', 'Concluído'],
|
||||
error: ['badge-error', 'Erro'],
|
||||
};
|
||||
const [cls, label] = map[status] || ['badge-inactive', status || '—'];
|
||||
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`;
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
},
|
||||
|
||||
_truncate(str, max) {
|
||||
if (!str || str.length <= max) return str;
|
||||
return str.slice(0, max) + '…';
|
||||
},
|
||||
|
||||
cronToHuman(expression) {
|
||||
@@ -262,8 +326,10 @@ const SchedulesUI = {
|
||||
|
||||
if (minute.startsWith('*/')) return `A cada ${minute.slice(2)} minutos`;
|
||||
if (hour.startsWith('*/') && minute === '0') return `A cada ${hour.slice(2)} horas`;
|
||||
if (minute === '0' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
return `Todo dia às ${hour.padStart(2, '0')}h`;
|
||||
if (hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
const h = hour.padStart(2, '0');
|
||||
if (minute === '0') return `Todo dia às ${h}h`;
|
||||
return `Todo dia às ${h}:${minute.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return expression;
|
||||
|
||||
@@ -4,6 +4,31 @@ const Terminal = {
|
||||
autoScroll: true,
|
||||
executionFilter: null,
|
||||
_processingInterval: null,
|
||||
_chatSession: null,
|
||||
|
||||
enableChat(agentId, agentName, sessionId) {
|
||||
Terminal._chatSession = { agentId, agentName, sessionId };
|
||||
const bar = document.getElementById('terminal-input-bar');
|
||||
const ctx = document.getElementById('terminal-input-context');
|
||||
const input = document.getElementById('terminal-input');
|
||||
if (bar) bar.hidden = false;
|
||||
if (ctx) ctx.textContent = `Conversando com: ${agentName}`;
|
||||
if (input) { input.value = ''; input.focus(); }
|
||||
},
|
||||
|
||||
disableChat() {
|
||||
Terminal._chatSession = null;
|
||||
const bar = document.getElementById('terminal-input-bar');
|
||||
if (bar) bar.hidden = true;
|
||||
},
|
||||
|
||||
getChatSession() {
|
||||
return Terminal._chatSession;
|
||||
},
|
||||
|
||||
updateSessionId(sessionId) {
|
||||
if (Terminal._chatSession) Terminal._chatSession.sessionId = sessionId;
|
||||
},
|
||||
|
||||
addLine(content, type = 'default', executionId = null) {
|
||||
const time = new Date();
|
||||
|
||||
257
public/js/components/webhooks.js
Normal file
257
public/js/components/webhooks.js
Normal file
@@ -0,0 +1,257 @@
|
||||
const WebhooksUI = {
|
||||
webhooks: [],
|
||||
agents: [],
|
||||
pipelines: [],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const [webhooks, agents, pipelines] = await Promise.all([
|
||||
API.webhooks.list(),
|
||||
API.agents.list(),
|
||||
API.pipelines.list(),
|
||||
]);
|
||||
WebhooksUI.webhooks = Array.isArray(webhooks) ? webhooks : [];
|
||||
WebhooksUI.agents = Array.isArray(agents) ? agents : [];
|
||||
WebhooksUI.pipelines = Array.isArray(pipelines) ? pipelines : [];
|
||||
WebhooksUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar webhooks: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
filter(searchText) {
|
||||
const search = (searchText || '').toLowerCase();
|
||||
const filtered = WebhooksUI.webhooks.filter((w) => {
|
||||
const name = (w.name || '').toLowerCase();
|
||||
return !search || name.includes(search);
|
||||
});
|
||||
WebhooksUI.render(filtered);
|
||||
},
|
||||
|
||||
render(filteredWebhooks) {
|
||||
const container = document.getElementById('webhooks-list');
|
||||
if (!container) return;
|
||||
|
||||
const webhooks = filteredWebhooks || WebhooksUI.webhooks;
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="webhook"></i>
|
||||
</div>
|
||||
<h3 class="empty-state-title">Nenhum webhook cadastrado</h3>
|
||||
<p class="empty-state-desc">Crie webhooks para disparar agentes ou pipelines via HTTP.</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = webhooks.map((w) => WebhooksUI._renderCard(w)).join('');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_renderCard(webhook) {
|
||||
const typeBadge = webhook.targetType === 'pipeline'
|
||||
? '<span class="badge badge--purple">Pipeline</span>'
|
||||
: '<span class="badge badge--blue">Agente</span>';
|
||||
|
||||
const statusBadge = webhook.active
|
||||
? '<span class="badge badge-active">Ativo</span>'
|
||||
: '<span class="badge badge-inactive">Inativo</span>';
|
||||
|
||||
const targetName = WebhooksUI._resolveTargetName(webhook);
|
||||
const hookUrl = `${window.location.origin}/hook/${webhook.token}`;
|
||||
const lastTrigger = webhook.lastTriggeredAt
|
||||
? new Date(webhook.lastTriggeredAt).toLocaleString('pt-BR')
|
||||
: 'Nunca';
|
||||
|
||||
return `
|
||||
<article class="webhook-card">
|
||||
<div class="webhook-card-header">
|
||||
<div class="webhook-card-identity">
|
||||
<span class="webhook-card-name">${WebhooksUI._escapeHtml(webhook.name)}</span>
|
||||
${typeBadge}
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="webhook-card-actions">
|
||||
<button class="btn btn-ghost btn-sm btn-icon" data-action="toggle-webhook" data-id="${webhook.id}" title="${webhook.active ? 'Desativar' : 'Ativar'}">
|
||||
<i data-lucide="${webhook.active ? 'pause' : 'play'}"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-icon btn-danger" data-action="delete-webhook" data-id="${webhook.id}" title="Excluir">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webhook-card-body">
|
||||
<div class="webhook-card-target">
|
||||
<span class="webhook-card-label">Destino</span>
|
||||
<span class="webhook-card-value">${WebhooksUI._escapeHtml(targetName)}</span>
|
||||
</div>
|
||||
<div class="webhook-card-url">
|
||||
<span class="webhook-card-label">URL</span>
|
||||
<div class="webhook-url-field">
|
||||
<code class="webhook-url-code">${hookUrl}</code>
|
||||
<button class="btn btn-ghost btn-sm btn-icon" data-action="copy-webhook-url" data-url="${hookUrl}" title="Copiar URL">
|
||||
<i data-lucide="copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webhook-card-curl">
|
||||
<button class="btn btn-ghost btn-sm btn-icon-text" data-action="copy-webhook-curl" data-id="${webhook.id}" title="Copiar comando cURL">
|
||||
<i data-lucide="terminal"></i>
|
||||
<span>Copiar cURL</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="webhook-card-meta">
|
||||
<span class="webhook-meta-item">
|
||||
<i data-lucide="activity" style="width:12px;height:12px"></i>
|
||||
${webhook.triggerCount || 0} disparos
|
||||
</span>
|
||||
<span class="webhook-meta-item">
|
||||
<i data-lucide="clock" style="width:12px;height:12px"></i>
|
||||
Último: ${lastTrigger}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
},
|
||||
|
||||
_resolveTargetName(webhook) {
|
||||
if (webhook.targetType === 'agent') {
|
||||
const agent = WebhooksUI.agents.find((a) => a.id === webhook.targetId);
|
||||
return agent ? (agent.agent_name || agent.name) : webhook.targetId;
|
||||
}
|
||||
const pl = WebhooksUI.pipelines.find((p) => p.id === webhook.targetId);
|
||||
return pl ? pl.name : webhook.targetId;
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
const titleEl = document.getElementById('webhook-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Novo Webhook';
|
||||
|
||||
const nameEl = document.getElementById('webhook-name');
|
||||
if (nameEl) nameEl.value = '';
|
||||
|
||||
const typeEl = document.getElementById('webhook-target-type');
|
||||
if (typeEl) {
|
||||
typeEl.value = 'agent';
|
||||
WebhooksUI._updateTargetSelect('agent');
|
||||
}
|
||||
|
||||
Modal.open('webhook-modal-overlay');
|
||||
},
|
||||
|
||||
_updateTargetSelect(targetType) {
|
||||
const selectEl = document.getElementById('webhook-target-id');
|
||||
if (!selectEl) return;
|
||||
|
||||
if (targetType === 'agent') {
|
||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||
WebhooksUI.agents.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`).join('');
|
||||
} else {
|
||||
selectEl.innerHTML = '<option value="">Selecionar pipeline...</option>' +
|
||||
WebhooksUI.pipelines.map((p) => `<option value="${p.id}">${p.name}</option>`).join('');
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
const name = document.getElementById('webhook-name')?.value.trim();
|
||||
const targetType = document.getElementById('webhook-target-type')?.value;
|
||||
const targetId = document.getElementById('webhook-target-id')?.value;
|
||||
|
||||
if (!name) { Toast.warning('Nome do webhook é obrigatório'); return; }
|
||||
if (!targetId) { Toast.warning('Selecione um destino'); return; }
|
||||
|
||||
try {
|
||||
await API.webhooks.create({ name, targetType, targetId });
|
||||
Modal.close('webhook-modal-overlay');
|
||||
Toast.success('Webhook criado com sucesso');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao criar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async toggleActive(webhookId) {
|
||||
const webhook = WebhooksUI.webhooks.find((w) => w.id === webhookId);
|
||||
if (!webhook) return;
|
||||
|
||||
try {
|
||||
await API.webhooks.update(webhookId, { active: !webhook.active });
|
||||
Toast.success(webhook.active ? 'Webhook desativado' : 'Webhook ativado');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao atualizar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(webhookId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Excluir webhook',
|
||||
'Tem certeza que deseja excluir este webhook? Integrações que usam esta URL deixarão de funcionar.'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.webhooks.delete(webhookId);
|
||||
Toast.success('Webhook excluído');
|
||||
await WebhooksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao excluir webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async copyUrl(url) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
Toast.success('URL copiada');
|
||||
} catch {
|
||||
Toast.error('Não foi possível copiar a URL');
|
||||
}
|
||||
},
|
||||
|
||||
async copyCurl(webhookId) {
|
||||
const webhook = WebhooksUI.webhooks.find((w) => w.id === webhookId);
|
||||
if (!webhook) return;
|
||||
|
||||
const hookUrl = `${window.location.origin}/hook/${webhook.token}`;
|
||||
const targetName = WebhooksUI._resolveTargetName(webhook);
|
||||
|
||||
let payload;
|
||||
if (webhook.targetType === 'pipeline') {
|
||||
payload = JSON.stringify({
|
||||
input: 'Texto de entrada para o pipeline',
|
||||
workingDirectory: '/caminho/do/projeto (opcional)',
|
||||
}, null, 2);
|
||||
} else {
|
||||
payload = JSON.stringify({
|
||||
task: 'Descreva a tarefa a ser executada',
|
||||
instructions: 'Instruções adicionais (opcional)',
|
||||
}, null, 2);
|
||||
}
|
||||
|
||||
const curl = `curl -X POST '${hookUrl}' \\\n -H 'Content-Type: application/json' \\\n -d '${payload}'`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(curl);
|
||||
Toast.success('cURL copiado');
|
||||
} catch {
|
||||
Toast.error('Não foi possível copiar o cURL');
|
||||
}
|
||||
},
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
};
|
||||
|
||||
window.WebhooksUI = WebhooksUI;
|
||||
Reference in New Issue
Block a user