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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user