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
This commit is contained in:
@@ -43,6 +43,7 @@ const API = {
|
||||
continue(id, sessionId, message) { return API.request('POST', `/agents/${id}/continue`, { sessionId, message }); },
|
||||
export(id) { return API.request('GET', `/agents/${id}/export`); },
|
||||
import(data) { return API.request('POST', '/agents/import', data); },
|
||||
duplicate(id) { return API.request('POST', `/agents/${id}/duplicate`); },
|
||||
},
|
||||
|
||||
tasks: {
|
||||
@@ -81,10 +82,19 @@ const API = {
|
||||
create(data) { return API.request('POST', '/webhooks', data); },
|
||||
update(id, data) { return API.request('PUT', `/webhooks/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/webhooks/${id}`); },
|
||||
test(id) { return API.request('POST', `/webhooks/${id}/test`); },
|
||||
},
|
||||
|
||||
stats: {
|
||||
costs(days) { return API.request('GET', `/stats/costs${days ? '?days=' + days : ''}`); },
|
||||
charts(days) { return API.request('GET', `/stats/charts${days ? '?days=' + days : ''}`); },
|
||||
},
|
||||
|
||||
notifications: {
|
||||
list() { return API.request('GET', '/notifications'); },
|
||||
markRead(id) { return API.request('POST', `/notifications/${id}/read`); },
|
||||
markAllRead() { return API.request('POST', '/notifications/read-all'); },
|
||||
clear() { return API.request('DELETE', '/notifications'); },
|
||||
},
|
||||
|
||||
system: {
|
||||
@@ -98,6 +108,12 @@ const API = {
|
||||
save(data) { return API.request('PUT', '/settings', data); },
|
||||
},
|
||||
|
||||
reports: {
|
||||
list() { return API.request('GET', '/reports'); },
|
||||
get(filename) { return API.request('GET', `/reports/${encodeURIComponent(filename)}`); },
|
||||
delete(filename) { return API.request('DELETE', `/reports/${encodeURIComponent(filename)}`); },
|
||||
},
|
||||
|
||||
executions: {
|
||||
recent(limit = 20) { return API.request('GET', `/executions/recent?limit=${limit}`); },
|
||||
history(params = {}) {
|
||||
@@ -107,6 +123,19 @@ const API = {
|
||||
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'); },
|
||||
retry(id) { return API.request('POST', `/executions/${id}/retry`); },
|
||||
async exportCsv() {
|
||||
const response = await fetch('/api/executions/export', {
|
||||
headers: { 'X-Client-Id': API.clientId },
|
||||
});
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `execucoes_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,18 +18,47 @@ const App = {
|
||||
settings: 'Configurações',
|
||||
},
|
||||
|
||||
sections: ['dashboard', 'agents', 'tasks', 'schedules', 'pipelines', 'webhooks', 'terminal', 'history', 'settings'],
|
||||
|
||||
init() {
|
||||
if (App._initialized) return;
|
||||
App._initialized = true;
|
||||
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
App.setupNavigation();
|
||||
App.setupWebSocket();
|
||||
App.setupEventListeners();
|
||||
App.setupKeyboardShortcuts();
|
||||
App.navigateTo('dashboard');
|
||||
|
||||
const initialSection = location.hash.replace('#', '') || 'dashboard';
|
||||
App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard');
|
||||
App.startPeriodicRefresh();
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
window.addEventListener('hashchange', () => {
|
||||
const section = location.hash.replace('#', '') || 'dashboard';
|
||||
if (App.sections.includes(section)) App.navigateTo(section);
|
||||
});
|
||||
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
Utils.refreshIcons();
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof NotificationsUI !== 'undefined') NotificationsUI.init();
|
||||
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
Utils.refreshIcons();
|
||||
},
|
||||
|
||||
setupNavigation() {
|
||||
@@ -47,6 +76,10 @@ const App = {
|
||||
},
|
||||
|
||||
navigateTo(section) {
|
||||
if (location.hash !== `#${section}`) {
|
||||
history.pushState(null, '', `#${section}`);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.section').forEach((el) => {
|
||||
const isActive = el.id === section;
|
||||
el.classList.toggle('active', isActive);
|
||||
@@ -174,6 +207,11 @@ const App = {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof NotificationsUI !== 'undefined') {
|
||||
NotificationsUI.loadCount();
|
||||
NotificationsUI.showBrowserNotification('Execução concluída', data.agentName || 'Agente');
|
||||
}
|
||||
|
||||
Toast.success('Execução concluída');
|
||||
App.refreshCurrentSection();
|
||||
App._updateActiveBadge();
|
||||
@@ -183,6 +221,12 @@ const App = {
|
||||
case 'execution_error':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(data.data?.error || 'Erro na execução', 'error', data.executionId);
|
||||
|
||||
if (typeof NotificationsUI !== 'undefined') {
|
||||
NotificationsUI.loadCount();
|
||||
NotificationsUI.showBrowserNotification('Execução falhou', data.agentName || 'Agente');
|
||||
}
|
||||
|
||||
Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`);
|
||||
App._updateActiveBadge();
|
||||
break;
|
||||
@@ -241,9 +285,32 @@ const App = {
|
||||
|
||||
case 'pipeline_status':
|
||||
break;
|
||||
|
||||
case 'report_generated':
|
||||
if (data.reportFile) {
|
||||
Terminal.addLine(`📄 Relatório gerado: ${data.reportFile}`, 'info');
|
||||
App._openReport(data.reportFile);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async _openReport(filename) {
|
||||
try {
|
||||
const data = await API.request('GET', `/reports/${encodeURIComponent(filename)}`);
|
||||
if (!data || !data.content) return;
|
||||
|
||||
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;
|
||||
|
||||
title.textContent = 'Relatório de Execução';
|
||||
content.innerHTML = `<div class="report-content"><pre class="report-markdown">${Utils.escapeHtml(data.content)}</pre></div>`;
|
||||
Modal.open('execution-detail-modal-overlay');
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
_showApprovalNotification(pipelineId, stepIndex, agentName) {
|
||||
const container = document.getElementById('approval-notification');
|
||||
if (!container) return;
|
||||
@@ -264,7 +331,7 @@ const App = {
|
||||
container.hidden = false;
|
||||
container.dataset.pipelineId = pipelineId;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
|
||||
document.getElementById('approval-approve-btn')?.addEventListener('click', () => {
|
||||
App._handleApproval(pipelineId, true);
|
||||
@@ -538,6 +605,7 @@ const App = {
|
||||
case 'edit': AgentsUI.openEditModal(id); break;
|
||||
case 'export': AgentsUI.export(id); break;
|
||||
case 'delete': AgentsUI.delete(id); break;
|
||||
case 'duplicate': AgentsUI.duplicate(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -598,6 +666,7 @@ const App = {
|
||||
switch (action) {
|
||||
case 'view-execution': HistoryUI.viewDetail(id); break;
|
||||
case 'delete-execution': HistoryUI.deleteExecution(id); break;
|
||||
case 'retry': HistoryUI.retryExecution(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -610,6 +679,8 @@ const App = {
|
||||
case 'delete-webhook': WebhooksUI.delete(id); break;
|
||||
case 'copy-webhook-url': WebhooksUI.copyUrl(url); break;
|
||||
case 'copy-webhook-curl': WebhooksUI.copyCurl(id); break;
|
||||
case 'edit-webhook': WebhooksUI.openEditModal(id); break;
|
||||
case 'test-webhook': WebhooksUI.test(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -768,14 +839,32 @@ const App = {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName);
|
||||
if (isTyping) return;
|
||||
const isInInput = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName);
|
||||
if (isInInput) return;
|
||||
|
||||
if (e.key === 'n' || e.key === 'N') {
|
||||
if (App.currentSection === 'agents') {
|
||||
AgentsUI.openCreateModal();
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
const sectionKeys = {
|
||||
'1': 'dashboard',
|
||||
'2': 'agents',
|
||||
'3': 'tasks',
|
||||
'4': 'schedules',
|
||||
'5': 'pipelines',
|
||||
'6': 'terminal',
|
||||
'7': 'history',
|
||||
'8': 'webhooks',
|
||||
'9': 'settings',
|
||||
};
|
||||
if (sectionKeys[e.key]) {
|
||||
e.preventDefault();
|
||||
App.navigateTo(sectionKeys[e.key]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const AgentsUI = {
|
||||
|
||||
grid.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
Utils.refreshIcons(grid);
|
||||
},
|
||||
|
||||
filter(searchText, statusFilter) {
|
||||
@@ -116,6 +116,9 @@ const AgentsUI = {
|
||||
<i data-lucide="pencil"></i>
|
||||
Editar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="duplicate" data-id="${agent.id}" title="Duplicar agente">
|
||||
<i data-lucide="copy"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="export" data-id="${agent.id}" title="Exportar agente">
|
||||
<i data-lucide="download"></i>
|
||||
</button>
|
||||
@@ -322,6 +325,16 @@ const AgentsUI = {
|
||||
|
||||
_savedTasksCache: [],
|
||||
|
||||
async duplicate(agentId) {
|
||||
try {
|
||||
await API.agents.duplicate(agentId);
|
||||
Toast.success('Agente duplicado com sucesso');
|
||||
await AgentsUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao duplicar agente: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async export(agentId) {
|
||||
try {
|
||||
const data = await API.agents.export(agentId);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const DashboardUI = {
|
||||
charts: {},
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const [status, recentExecs] = await Promise.all([
|
||||
@@ -9,11 +11,253 @@ const DashboardUI = {
|
||||
DashboardUI.updateMetrics(status);
|
||||
DashboardUI.updateRecentActivity(recentExecs || []);
|
||||
DashboardUI.updateSystemStatus(status);
|
||||
DashboardUI.setupChartPeriod();
|
||||
DashboardUI.loadCharts();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar dashboard: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async loadCharts() {
|
||||
try {
|
||||
const period = document.getElementById('chart-period');
|
||||
const days = period ? parseInt(period.value) : 7;
|
||||
const data = await API.stats.charts(days);
|
||||
DashboardUI.renderExecutionsChart(data);
|
||||
DashboardUI.renderCostChart(data);
|
||||
DashboardUI.renderStatusChart(data);
|
||||
DashboardUI.renderTopAgentsChart(data);
|
||||
DashboardUI.renderSuccessRateChart(data);
|
||||
} catch (e) {
|
||||
console.error('Erro ao carregar gráficos:', e);
|
||||
}
|
||||
},
|
||||
|
||||
_cssVar(name) {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
},
|
||||
|
||||
renderExecutionsChart(data) {
|
||||
const ctx = document.getElementById('executions-chart');
|
||||
if (!ctx) return;
|
||||
if (DashboardUI.charts.executions) DashboardUI.charts.executions.destroy();
|
||||
|
||||
const labels = (data.labels || []).map(l => {
|
||||
const d = new Date(l + 'T12:00:00');
|
||||
return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
|
||||
});
|
||||
|
||||
DashboardUI.charts.executions = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{ label: 'Sucesso', data: data.successCounts || [], backgroundColor: 'rgba(34, 197, 94, 0.8)', borderRadius: 4 },
|
||||
{ label: 'Erro', data: data.errorCounts || [], backgroundColor: 'rgba(239, 68, 68, 0.8)', borderRadius: 4 },
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: { color: DashboardUI._cssVar('--text-secondary'), font: { size: 11 } },
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: { display: false },
|
||||
ticks: { color: DashboardUI._cssVar('--text-tertiary'), font: { size: 10 } },
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(128,128,128,0.1)' },
|
||||
ticks: { color: DashboardUI._cssVar('--text-tertiary'), font: { size: 10 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
renderCostChart(data) {
|
||||
const ctx = document.getElementById('cost-chart');
|
||||
if (!ctx) return;
|
||||
if (DashboardUI.charts.cost) DashboardUI.charts.cost.destroy();
|
||||
|
||||
const labels = (data.labels || []).map(l => {
|
||||
const d = new Date(l + 'T12:00:00');
|
||||
return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
|
||||
});
|
||||
|
||||
DashboardUI.charts.cost = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Custo (USD)',
|
||||
data: data.costData || [],
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#6366f1',
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: DashboardUI._cssVar('--text-tertiary'), font: { size: 10 } },
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(128,128,128,0.1)' },
|
||||
ticks: {
|
||||
color: DashboardUI._cssVar('--text-tertiary'),
|
||||
font: { size: 10 },
|
||||
callback: (v) => '$' + v.toFixed(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
renderStatusChart(data) {
|
||||
const ctx = document.getElementById('status-chart');
|
||||
if (!ctx) return;
|
||||
if (DashboardUI.charts.status) DashboardUI.charts.status.destroy();
|
||||
|
||||
const dist = data.statusDistribution || {};
|
||||
const statuses = Object.keys(dist);
|
||||
const values = Object.values(dist);
|
||||
const colors = {
|
||||
completed: '#22c55e',
|
||||
error: '#ef4444',
|
||||
running: '#6366f1',
|
||||
canceled: '#f59e0b',
|
||||
rejected: '#ef4444',
|
||||
};
|
||||
|
||||
DashboardUI.charts.status = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: statuses.map(s => s.charAt(0).toUpperCase() + s.slice(1)),
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: statuses.map(s => colors[s] || '#94a3b8'),
|
||||
borderWidth: 0,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1,
|
||||
cutout: '65%',
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
color: DashboardUI._cssVar('--text-secondary'),
|
||||
font: { size: 11 },
|
||||
padding: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
renderTopAgentsChart(data) {
|
||||
const ctx = document.getElementById('agents-chart');
|
||||
if (!ctx) return;
|
||||
if (DashboardUI.charts.agents) DashboardUI.charts.agents.destroy();
|
||||
|
||||
const top = data.topAgents || [];
|
||||
|
||||
DashboardUI.charts.agents = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: top.map(a => a.name.length > 15 ? a.name.substring(0, 15) + '\u2026' : a.name),
|
||||
datasets: [{
|
||||
data: top.map(a => a.count),
|
||||
backgroundColor: ['#6366f1', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe'],
|
||||
borderRadius: 4,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(128,128,128,0.1)' },
|
||||
ticks: { color: DashboardUI._cssVar('--text-tertiary'), font: { size: 10 } },
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: DashboardUI._cssVar('--text-secondary'), font: { size: 10 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
renderSuccessRateChart(data) {
|
||||
const ctx = document.getElementById('success-rate-chart');
|
||||
if (!ctx) return;
|
||||
if (DashboardUI.charts.successRate) DashboardUI.charts.successRate.destroy();
|
||||
|
||||
const dist = data.statusDistribution || {};
|
||||
const total = Object.values(dist).reduce((a, b) => a + b, 0);
|
||||
const success = dist.completed || 0;
|
||||
const rate = total > 0 ? Math.round((success / total) * 100) : 0;
|
||||
|
||||
DashboardUI.charts.successRate = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Sucesso', 'Outros'],
|
||||
datasets: [{
|
||||
data: [rate, 100 - rate],
|
||||
backgroundColor: ['#22c55e', 'rgba(128,128,128,0.15)'],
|
||||
borderWidth: 0,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1,
|
||||
cutout: '75%',
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
},
|
||||
},
|
||||
plugins: [{
|
||||
id: 'centerText',
|
||||
afterDraw(chart) {
|
||||
const { ctx: c, width, height } = chart;
|
||||
c.save();
|
||||
c.font = 'bold 24px Inter';
|
||||
c.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim();
|
||||
c.textAlign = 'center';
|
||||
c.textBaseline = 'middle';
|
||||
c.fillText(rate + '%', width / 2, height / 2);
|
||||
c.restore();
|
||||
},
|
||||
}],
|
||||
});
|
||||
},
|
||||
|
||||
updateMetrics(status) {
|
||||
const metrics = {
|
||||
'metric-total-agents': status.agents?.total ?? 0,
|
||||
@@ -71,7 +315,7 @@ const DashboardUI = {
|
||||
<span>Nenhuma execução recente</span>
|
||||
</li>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [list] });
|
||||
Utils.refreshIcons(list);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,6 +354,14 @@ const DashboardUI = {
|
||||
}).join('');
|
||||
},
|
||||
|
||||
setupChartPeriod() {
|
||||
const chartPeriod = document.getElementById('chart-period');
|
||||
if (chartPeriod && !chartPeriod._listenerAdded) {
|
||||
chartPeriod._listenerAdded = true;
|
||||
chartPeriod.addEventListener('change', () => DashboardUI.loadCharts());
|
||||
}
|
||||
},
|
||||
|
||||
updateSystemStatus(status) {
|
||||
const wsBadge = document.getElementById('system-ws-status-badge');
|
||||
if (wsBadge) {
|
||||
|
||||
@@ -7,7 +7,17 @@ const HistoryUI = {
|
||||
_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;
|
||||
@@ -38,12 +48,12 @@ const HistoryUI = {
|
||||
<p class="empty-state-text">O histórico de execuções aparecerá aqui.</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = HistoryUI.executions.map((exec) => HistoryUI._renderCard(exec)).join('');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
},
|
||||
|
||||
_renderCard(exec) {
|
||||
@@ -55,9 +65,10 @@ const HistoryUI = {
|
||||
const name = exec.type === 'pipeline'
|
||||
? (exec.pipelineName || 'Pipeline')
|
||||
: (exec.agentName || 'Agente');
|
||||
const task = exec.type === 'pipeline'
|
||||
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;
|
||||
@@ -74,7 +85,7 @@ const HistoryUI = {
|
||||
${statusBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-card-task">${Utils.escapeHtml(task)}</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>
|
||||
@@ -91,6 +102,10 @@ const HistoryUI = {
|
||||
<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>
|
||||
@@ -131,7 +146,7 @@ const HistoryUI = {
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
|
||||
document.getElementById('history-prev-btn')?.addEventListener('click', () => {
|
||||
HistoryUI.page--;
|
||||
@@ -171,7 +186,7 @@ const HistoryUI = {
|
||||
: HistoryUI._renderAgentDetail(exec);
|
||||
|
||||
Modal.open('execution-detail-modal-overlay');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [content] });
|
||||
Utils.refreshIcons(content);
|
||||
|
||||
content.querySelectorAll('.pipeline-step-prompt-toggle').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -359,6 +374,16 @@ const HistoryUI = {
|
||||
`;
|
||||
},
|
||||
|
||||
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',
|
||||
|
||||
153
public/js/components/notifications.js
Normal file
153
public/js/components/notifications.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const NotificationsUI = {
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
pollInterval: null,
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.startPolling();
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
const bell = document.getElementById('notification-bell');
|
||||
const panel = document.getElementById('notification-panel');
|
||||
|
||||
if (bell) {
|
||||
bell.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
panel.classList.toggle('hidden');
|
||||
if (!panel.classList.contains('hidden')) this.load();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (panel && !panel.contains(e.target) && e.target !== bell) {
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
const markAllBtn = document.getElementById('mark-all-read');
|
||||
if (markAllBtn) {
|
||||
markAllBtn.addEventListener('click', () => this.markAllRead());
|
||||
}
|
||||
|
||||
const clearBtn = document.getElementById('clear-notifications');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => this.clearAll());
|
||||
}
|
||||
},
|
||||
|
||||
startPolling() {
|
||||
this.pollInterval = setInterval(() => this.loadCount(), 15000);
|
||||
this.loadCount();
|
||||
},
|
||||
|
||||
async loadCount() {
|
||||
try {
|
||||
const data = await API.request('GET', '/notifications');
|
||||
this.unreadCount = data.unreadCount || 0;
|
||||
this.updateBadge();
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const data = await API.request('GET', '/notifications');
|
||||
this.notifications = data.notifications || [];
|
||||
this.unreadCount = data.unreadCount || 0;
|
||||
this.updateBadge();
|
||||
this.render();
|
||||
} catch (e) {
|
||||
console.error('Erro ao carregar notificações:', e);
|
||||
}
|
||||
},
|
||||
|
||||
updateBadge() {
|
||||
const badge = document.getElementById('notification-badge');
|
||||
if (!badge) return;
|
||||
if (this.unreadCount > 0) {
|
||||
badge.textContent = this.unreadCount > 99 ? '99+' : this.unreadCount;
|
||||
badge.classList.remove('hidden');
|
||||
} else {
|
||||
badge.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const list = document.getElementById('notification-list');
|
||||
if (!list) return;
|
||||
|
||||
if (this.notifications.length === 0) {
|
||||
list.innerHTML = '<div class="notification-empty">Nenhuma notificação</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = this.notifications.map(n => {
|
||||
const iconClass = n.type === 'success' ? 'success' : n.type === 'error' ? 'error' : 'info';
|
||||
const icon = n.type === 'success' ? '✓' : n.type === 'error' ? '✕' : 'ℹ';
|
||||
const time = this.timeAgo(n.createdAt);
|
||||
const unread = n.read ? '' : ' unread';
|
||||
return `<div class="notification-item${unread}" data-id="${n.id}">
|
||||
<div class="notification-item-icon ${iconClass}">${icon}</div>
|
||||
<div class="notification-item-content">
|
||||
<div class="notification-item-title">${Utils.escapeHtml(n.title)}</div>
|
||||
<div class="notification-item-message">${Utils.escapeHtml(n.message)}</div>
|
||||
<div class="notification-item-time">${time}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
list.querySelectorAll('.notification-item').forEach(item => {
|
||||
item.addEventListener('click', () => this.markAsRead(item.dataset.id));
|
||||
});
|
||||
},
|
||||
|
||||
async markAsRead(id) {
|
||||
try {
|
||||
await API.request('POST', `/notifications/${id}/read`);
|
||||
const n = this.notifications.find(n => n.id === id);
|
||||
if (n) n.read = true;
|
||||
this.unreadCount = Math.max(0, this.unreadCount - 1);
|
||||
this.updateBadge();
|
||||
this.render();
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async markAllRead() {
|
||||
try {
|
||||
await API.request('POST', '/notifications/read-all');
|
||||
this.notifications.forEach(n => n.read = true);
|
||||
this.unreadCount = 0;
|
||||
this.updateBadge();
|
||||
this.render();
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async clearAll() {
|
||||
try {
|
||||
await API.request('DELETE', '/notifications');
|
||||
this.notifications = [];
|
||||
this.unreadCount = 0;
|
||||
this.updateBadge();
|
||||
this.render();
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
timeAgo(dateStr) {
|
||||
const now = new Date();
|
||||
const date = new Date(dateStr);
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
if (diff < 60) return 'agora';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}min atrás`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h atrás`;
|
||||
return `${Math.floor(diff / 86400)}d atrás`;
|
||||
},
|
||||
|
||||
showBrowserNotification(title, body) {
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification(title, { body, icon: '/favicon.ico' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.NotificationsUI = NotificationsUI;
|
||||
@@ -44,7 +44,7 @@ const PipelinesUI = {
|
||||
if (!emptyState) {
|
||||
grid.insertAdjacentHTML('beforeend', PipelinesUI.renderEmpty());
|
||||
}
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
Utils.refreshIcons(grid);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ const PipelinesUI = {
|
||||
|
||||
grid.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
Utils.refreshIcons(grid);
|
||||
},
|
||||
|
||||
renderEmpty() {
|
||||
@@ -249,7 +249,7 @@ const PipelinesUI = {
|
||||
select.value = PipelinesUI._steps[index].agentId || '';
|
||||
});
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
},
|
||||
|
||||
_syncStepsFromDOM() {
|
||||
|
||||
@@ -28,7 +28,7 @@ const SchedulesUI = {
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||
Utils.refreshIcons(tbody);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ const SchedulesUI = {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||
Utils.refreshIcons(tbody);
|
||||
},
|
||||
|
||||
filter(searchText, statusFilter) {
|
||||
@@ -208,7 +208,11 @@ const SchedulesUI = {
|
||||
if (!container) return;
|
||||
|
||||
if (history.length === 0) {
|
||||
container.innerHTML = '<p class="empty-state-desc">Nenhum disparo registrado</p>';
|
||||
const hasSchedules = SchedulesUI.schedules.length > 0;
|
||||
const msg = hasSchedules
|
||||
? 'Nenhum disparo registrado ainda. As tarefas agendadas aparecerão aqui após a próxima execução.'
|
||||
: 'Nenhum disparo registrado. Crie um agendamento para começar.';
|
||||
container.innerHTML = `<p class="empty-state-desc">${msg}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -256,7 +260,7 @@ const SchedulesUI = {
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
},
|
||||
|
||||
_statusBadge(status) {
|
||||
|
||||
@@ -8,11 +8,20 @@ const SettingsUI = {
|
||||
|
||||
SettingsUI.populateForm(settings);
|
||||
SettingsUI.populateSystemInfo(info);
|
||||
SettingsUI.updateThemeInfo();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar configurações: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
updateThemeInfo() {
|
||||
const themeEl = document.getElementById('info-current-theme');
|
||||
if (themeEl) {
|
||||
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
themeEl.textContent = theme === 'dark' ? 'Escuro' : 'Claro';
|
||||
}
|
||||
},
|
||||
|
||||
populateForm(settings) {
|
||||
const fields = {
|
||||
'settings-default-model': settings.defaultModel || 'claude-sonnet-4-6',
|
||||
|
||||
@@ -39,7 +39,7 @@ const TasksUI = {
|
||||
|
||||
container.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
},
|
||||
|
||||
filter(searchText, categoryFilter) {
|
||||
|
||||
@@ -5,6 +5,9 @@ const Terminal = {
|
||||
executionFilter: null,
|
||||
_processingInterval: null,
|
||||
_chatSession: null,
|
||||
searchMatches: [],
|
||||
searchIndex: -1,
|
||||
_toolbarInitialized: false,
|
||||
|
||||
enableChat(agentId, agentName, sessionId) {
|
||||
Terminal._chatSession = { agentId, agentName, sessionId };
|
||||
@@ -83,7 +86,121 @@ const Terminal = {
|
||||
if (output) output.scrollTop = output.scrollHeight;
|
||||
},
|
||||
|
||||
initToolbar() {
|
||||
if (Terminal._toolbarInitialized) return;
|
||||
Terminal._toolbarInitialized = true;
|
||||
|
||||
const searchToggle = document.getElementById('terminal-search-toggle');
|
||||
const searchBar = document.getElementById('terminal-search-bar');
|
||||
const searchInput = document.getElementById('terminal-search-input');
|
||||
const searchClose = document.getElementById('terminal-search-close');
|
||||
const searchPrev = document.getElementById('terminal-search-prev');
|
||||
const searchNext = document.getElementById('terminal-search-next');
|
||||
const downloadBtn = document.getElementById('terminal-download');
|
||||
const copyBtn = document.getElementById('terminal-copy');
|
||||
const autoScrollCheck = document.getElementById('terminal-autoscroll');
|
||||
|
||||
if (searchToggle && searchBar) {
|
||||
searchToggle.addEventListener('click', () => {
|
||||
searchBar.classList.toggle('hidden');
|
||||
if (!searchBar.classList.contains('hidden') && searchInput) searchInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', () => Terminal.search(searchInput.value));
|
||||
}
|
||||
|
||||
if (searchClose && searchBar) {
|
||||
searchClose.addEventListener('click', () => {
|
||||
searchBar.classList.add('hidden');
|
||||
Terminal.clearSearch();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchPrev) searchPrev.addEventListener('click', () => Terminal.searchPrev());
|
||||
if (searchNext) searchNext.addEventListener('click', () => Terminal.searchNext());
|
||||
|
||||
if (downloadBtn) {
|
||||
downloadBtn.addEventListener('click', () => Terminal.downloadOutput());
|
||||
}
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', () => Terminal.copyOutput());
|
||||
}
|
||||
|
||||
if (autoScrollCheck) {
|
||||
autoScrollCheck.addEventListener('change', (e) => {
|
||||
Terminal.autoScroll = e.target.checked;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
search(query) {
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (!output || !query) { Terminal.clearSearch(); return; }
|
||||
|
||||
const text = output.textContent;
|
||||
Terminal.searchMatches = [];
|
||||
Terminal.searchIndex = -1;
|
||||
|
||||
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
Terminal.searchMatches.push(match.index);
|
||||
}
|
||||
|
||||
const countEl = document.getElementById('terminal-search-count');
|
||||
if (countEl) countEl.textContent = Terminal.searchMatches.length > 0 ? `0/${Terminal.searchMatches.length}` : '0/0';
|
||||
|
||||
if (Terminal.searchMatches.length > 0) Terminal.searchNext();
|
||||
},
|
||||
|
||||
searchNext() {
|
||||
if (Terminal.searchMatches.length === 0) return;
|
||||
Terminal.searchIndex = (Terminal.searchIndex + 1) % Terminal.searchMatches.length;
|
||||
const countEl = document.getElementById('terminal-search-count');
|
||||
if (countEl) countEl.textContent = `${Terminal.searchIndex + 1}/${Terminal.searchMatches.length}`;
|
||||
},
|
||||
|
||||
searchPrev() {
|
||||
if (Terminal.searchMatches.length === 0) return;
|
||||
Terminal.searchIndex = Terminal.searchIndex <= 0 ? Terminal.searchMatches.length - 1 : Terminal.searchIndex - 1;
|
||||
const countEl = document.getElementById('terminal-search-count');
|
||||
if (countEl) countEl.textContent = `${Terminal.searchIndex + 1}/${Terminal.searchMatches.length}`;
|
||||
},
|
||||
|
||||
clearSearch() {
|
||||
Terminal.searchMatches = [];
|
||||
Terminal.searchIndex = -1;
|
||||
const countEl = document.getElementById('terminal-search-count');
|
||||
if (countEl) countEl.textContent = '0/0';
|
||||
},
|
||||
|
||||
downloadOutput() {
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (!output) return;
|
||||
const text = output.textContent;
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `terminal_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
if (typeof Toast !== 'undefined') Toast.success('Saída baixada');
|
||||
},
|
||||
|
||||
copyOutput() {
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (!output) return;
|
||||
navigator.clipboard.writeText(output.textContent).then(() => {
|
||||
if (typeof Toast !== 'undefined') Toast.success('Saída copiada');
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
Terminal.initToolbar();
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (!output) return;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const Toast = {
|
||||
iconMap: {
|
||||
success: 'check-circle',
|
||||
error: 'x-circle',
|
||||
success: 'circle-check',
|
||||
error: 'circle-x',
|
||||
info: 'info',
|
||||
warning: 'alert-triangle',
|
||||
warning: 'triangle-alert',
|
||||
},
|
||||
|
||||
colorMap: {
|
||||
@@ -35,9 +35,7 @@ const Toast = {
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons({ nodes: [toast] });
|
||||
}
|
||||
Utils.refreshIcons(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('toast-show');
|
||||
|
||||
@@ -44,12 +44,12 @@ const WebhooksUI = {
|
||||
<p class="empty-state-desc">Crie webhooks para disparar agentes ou pipelines via HTTP.</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = webhooks.map((w) => WebhooksUI._renderCard(w)).join('');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
Utils.refreshIcons(container);
|
||||
},
|
||||
|
||||
_renderCard(webhook) {
|
||||
@@ -79,6 +79,12 @@ const WebhooksUI = {
|
||||
<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" data-action="edit-webhook" data-id="${webhook.id}" title="Editar">
|
||||
<i data-lucide="pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-icon" data-action="test-webhook" data-id="${webhook.id}" title="Testar">
|
||||
<i data-lucide="zap"></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>
|
||||
@@ -141,9 +147,46 @@ const WebhooksUI = {
|
||||
WebhooksUI._updateTargetSelect('agent');
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById('webhook-form-submit');
|
||||
if (submitBtn) submitBtn.dataset.editId = '';
|
||||
|
||||
Modal.open('webhook-modal-overlay');
|
||||
},
|
||||
|
||||
openEditModal(webhookId) {
|
||||
const webhook = WebhooksUI.webhooks.find(w => w.id === webhookId);
|
||||
if (!webhook) return;
|
||||
|
||||
const titleEl = document.getElementById('webhook-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Editar Webhook';
|
||||
|
||||
const nameEl = document.getElementById('webhook-name');
|
||||
if (nameEl) nameEl.value = webhook.name || '';
|
||||
|
||||
const typeEl = document.getElementById('webhook-target-type');
|
||||
if (typeEl) {
|
||||
typeEl.value = webhook.targetType || 'agent';
|
||||
WebhooksUI._updateTargetSelect(webhook.targetType || 'agent');
|
||||
}
|
||||
|
||||
const targetEl = document.getElementById('webhook-target-id');
|
||||
if (targetEl) targetEl.value = webhook.targetId || '';
|
||||
|
||||
const submitBtn = document.getElementById('webhook-form-submit');
|
||||
if (submitBtn) submitBtn.dataset.editId = webhookId;
|
||||
|
||||
Modal.open('webhook-modal-overlay');
|
||||
},
|
||||
|
||||
async test(webhookId) {
|
||||
try {
|
||||
const result = await API.webhooks.test(webhookId);
|
||||
Toast.success(result.message || 'Webhook testado com sucesso');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao testar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
_updateTargetSelect(targetType) {
|
||||
const selectEl = document.getElementById('webhook-target-id');
|
||||
if (!selectEl) return;
|
||||
@@ -161,17 +204,25 @@ const WebhooksUI = {
|
||||
const name = document.getElementById('webhook-name')?.value.trim();
|
||||
const targetType = document.getElementById('webhook-target-type')?.value;
|
||||
const targetId = document.getElementById('webhook-target-id')?.value;
|
||||
const submitBtn = document.getElementById('webhook-form-submit');
|
||||
const editId = submitBtn?.dataset.editId || '';
|
||||
|
||||
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');
|
||||
if (editId) {
|
||||
await API.webhooks.update(editId, { name, targetType, targetId });
|
||||
Modal.close('webhook-modal-overlay');
|
||||
Toast.success('Webhook atualizado com sucesso');
|
||||
} else {
|
||||
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}`);
|
||||
Toast.error(`Erro ao salvar webhook: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
16688
public/js/lucide.js
Normal file
16688
public/js/lucide.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,14 @@ const Utils = {
|
||||
if (!str) return '';
|
||||
return str.length > max ? str.slice(0, max) + '…' : str;
|
||||
},
|
||||
|
||||
refreshIcons(container) {
|
||||
if (!window.lucide) return;
|
||||
const target = container || document;
|
||||
const pending = target.querySelectorAll('i[data-lucide]');
|
||||
if (pending.length === 0) return;
|
||||
lucide.createIcons();
|
||||
},
|
||||
};
|
||||
|
||||
window.Utils = Utils;
|
||||
|
||||
Reference in New Issue
Block a user