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:
@@ -447,6 +447,8 @@ textarea {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeInUp 0.3s ease both;
|
||||
}
|
||||
|
||||
@@ -461,6 +463,7 @@ textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-card-top {
|
||||
@@ -529,6 +532,7 @@ textarea {
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -664,7 +668,7 @@ textarea {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
.btn.btn-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
@@ -679,6 +683,7 @@ textarea {
|
||||
.btn-sm.btn-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@@ -1967,6 +1972,7 @@ tbody tr:hover td {
|
||||
flex: 1;
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -3395,6 +3401,10 @@ tbody tr:hover td {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
#history-list {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
@@ -3406,6 +3416,8 @@ tbody tr:hover td {
|
||||
transition: all 0.2s;
|
||||
animation: fadeInUp 0.2s ease both;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-card:hover {
|
||||
@@ -3419,6 +3431,7 @@ tbody tr:hover td {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-card-identity {
|
||||
@@ -3435,6 +3448,7 @@ tbody tr:hover td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.history-card-task {
|
||||
@@ -3443,6 +3457,7 @@ tbody tr:hover td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.history-card-info {
|
||||
@@ -4022,3 +4037,210 @@ tbody tr:hover td {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f5f6f8;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #eef0f4;
|
||||
--bg-card: #ffffff;
|
||||
--bg-card-hover: #f8f9fb;
|
||||
--bg-input: #eef0f4;
|
||||
--text-primary: #1a1d23;
|
||||
--text-secondary: #4a5068;
|
||||
--text-tertiary: #7c8298;
|
||||
--border-color: #dde0e9;
|
||||
--border-light: #e8ebf0;
|
||||
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .sidebar { background: #ffffff; border-right: 1px solid var(--border-color); }
|
||||
[data-theme="light"] .sidebar .nav-item:hover,
|
||||
[data-theme="light"] .sidebar .nav-item.active { background: #eef0f4; }
|
||||
[data-theme="light"] .header { background: #ffffff; border-bottom: 1px solid var(--border-color); }
|
||||
[data-theme="light"] .card { background: #ffffff; border: 1px solid var(--border-color); }
|
||||
[data-theme="light"] .modal-content { background: #ffffff; border: 1px solid var(--border-color); }
|
||||
[data-theme="light"] .terminal-output { background: #1e1e2e; color: #cdd6f4; }
|
||||
[data-theme="light"] .input,
|
||||
[data-theme="light"] .select,
|
||||
[data-theme="light"] textarea { background: #eef0f4; border-color: #dde0e9; color: #1a1d23; }
|
||||
[data-theme="light"] .badge { opacity: 0.9; }
|
||||
[data-theme="light"] .table th { background: #eef0f4; }
|
||||
[data-theme="light"] .table td { border-color: #e8ebf0; }
|
||||
[data-theme="light"] .metrics-card { background: #ffffff; border: 1px solid var(--border-color); }
|
||||
|
||||
body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metrics-card {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-icon-dark { display: none; }
|
||||
.theme-icon-light { display: block; }
|
||||
[data-theme="light"] .theme-icon-dark { display: block; }
|
||||
[data-theme="light"] .theme-icon-light { display: none; }
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.charts-row--triple { grid-template-columns: repeat(3, 1fr); }
|
||||
.chart-container {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
.charts-row--triple .chart-container {
|
||||
height: auto;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.charts-row--triple .chart-container canvas {
|
||||
max-height: 200px;
|
||||
align-self: center;
|
||||
}
|
||||
.chart-container canvas {
|
||||
max-height: calc(100% - 2.5rem);
|
||||
}
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.chart-header h3 {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
.select-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.charts-row, .charts-row--triple { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.notification-wrapper { position: relative; }
|
||||
.notification-badge {
|
||||
position: absolute; top: -4px; right: -4px;
|
||||
background: var(--danger); color: white;
|
||||
font-size: 0.625rem; font-weight: 700;
|
||||
min-width: 18px; height: 18px; border-radius: 9px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0 4px; pointer-events: none;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
.notification-panel {
|
||||
position: absolute; top: calc(100% + 8px); right: 0;
|
||||
width: 380px; max-height: 480px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 1000;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.notification-panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.notification-panel-header h3 { font-size: 0.8125rem; font-weight: 600; margin: 0; }
|
||||
.notification-panel-actions { display: flex; gap: 0.25rem; }
|
||||
.notification-list { flex: 1; overflow-y: auto; max-height: 380px; }
|
||||
.notification-item {
|
||||
display: flex; gap: 0.75rem; padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
cursor: pointer; transition: background 0.15s;
|
||||
}
|
||||
.notification-item:hover { background: var(--bg-tertiary); }
|
||||
.notification-item.unread { background: rgba(99, 102, 241, 0.05); }
|
||||
.notification-item-icon {
|
||||
flex-shrink: 0; width: 32px; height: 32px; border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 14px;
|
||||
}
|
||||
.notification-item-icon.success { background: rgba(34, 197, 94, 0.1); color: var(--success); }
|
||||
.notification-item-icon.error { background: rgba(239, 68, 68, 0.1); color: var(--danger); }
|
||||
.notification-item-icon.info { background: rgba(99, 102, 241, 0.1); color: var(--primary); }
|
||||
.notification-item-content { flex: 1; min-width: 0; }
|
||||
.notification-item-title { font-size: 0.8125rem; font-weight: 600; color: var(--text-primary); }
|
||||
.notification-item-message { font-size: 0.75rem; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.notification-item-time { font-size: 0.6875rem; color: var(--text-tertiary); margin-top: 2px; }
|
||||
.notification-empty { padding: 2rem; text-align: center; color: var(--text-tertiary); font-size: 0.8125rem; }
|
||||
|
||||
.terminal-action-toolbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.terminal-toolbar-left, .terminal-toolbar-right { display: flex; align-items: center; gap: 0.25rem; }
|
||||
.terminal-toggle-label {
|
||||
display: flex; align-items: center; gap: 0.375rem;
|
||||
font-size: 0.75rem; color: var(--text-secondary); cursor: pointer; user-select: none;
|
||||
}
|
||||
.terminal-toggle-label input[type="checkbox"] { width: 14px; height: 14px; accent-color: var(--primary); }
|
||||
.terminal-search-bar {
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color); border-top: none;
|
||||
}
|
||||
.terminal-search-bar .input--sm { flex: 1; max-width: 300px; }
|
||||
.terminal-search-count { font-size: 0.75rem; color: var(--text-tertiary); min-width: 40px; text-align: center; }
|
||||
.terminal-search-highlight { background: rgba(250, 204, 21, 0.4); border-radius: 2px; }
|
||||
.terminal-search-highlight.active { background: rgba(250, 204, 21, 0.8); }
|
||||
|
||||
.skeleton-pulse {
|
||||
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-card-hover) 50%, var(--bg-tertiary) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
border-radius: 6px;
|
||||
}
|
||||
@keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.report-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.report-markdown {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.7;
|
||||
color: var(--text-primary);
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.report-toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.report-toast:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agents Orchestrator</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
@@ -104,13 +105,35 @@
|
||||
<span>Sistema</span>
|
||||
</button>
|
||||
<button class="btn btn--ghost btn--icon-text" id="import-agent-btn" type="button">
|
||||
<i data-lucide="upload"></i>
|
||||
<i data-lucide="file-down"></i>
|
||||
<span>Importar</span>
|
||||
</button>
|
||||
<button class="btn btn--primary btn--icon-text" id="new-agent-btn" type="button">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Novo Agente</span>
|
||||
</button>
|
||||
<button id="theme-toggle" class="btn btn--icon" title="Alternar tema">
|
||||
<i data-lucide="sun" class="theme-icon-light"></i>
|
||||
<i data-lucide="moon" class="theme-icon-dark"></i>
|
||||
</button>
|
||||
<div class="notification-wrapper">
|
||||
<button id="notification-bell" class="btn btn--icon" title="Notificações">
|
||||
<i data-lucide="bell"></i>
|
||||
<span id="notification-badge" class="notification-badge hidden">0</span>
|
||||
</button>
|
||||
<div id="notification-panel" class="notification-panel hidden">
|
||||
<div class="notification-panel-header">
|
||||
<h3>Notificações</h3>
|
||||
<div class="notification-panel-actions">
|
||||
<button id="mark-all-read" class="btn btn--ghost btn--sm">Marcar lidas</button>
|
||||
<button id="clear-notifications" class="btn btn--ghost btn--sm">Limpar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notification-list" class="notification-list">
|
||||
<div class="notification-empty">Nenhuma notificação</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -138,7 +161,7 @@
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--purple">
|
||||
<i data-lucide="play-circle"></i>
|
||||
<i data-lucide="circle-play"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Execuções Hoje</span>
|
||||
@@ -174,6 +197,46 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Execuções</h3>
|
||||
<select id="chart-period" class="select-sm">
|
||||
<option value="7">7 dias</option>
|
||||
<option value="14">14 dias</option>
|
||||
<option value="30">30 dias</option>
|
||||
</select>
|
||||
</div>
|
||||
<canvas id="executions-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Custo (USD)</h3>
|
||||
</div>
|
||||
<canvas id="cost-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="charts-row charts-row--triple">
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Status</h3>
|
||||
</div>
|
||||
<canvas id="status-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Top Agentes</h3>
|
||||
</div>
|
||||
<canvas id="agents-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>Taxa de Sucesso</h3>
|
||||
</div>
|
||||
<canvas id="success-rate-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -424,6 +487,32 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-action-toolbar">
|
||||
<div class="terminal-toolbar-left">
|
||||
<button id="terminal-search-toggle" class="btn btn--ghost btn--sm" title="Buscar (Ctrl+F)">
|
||||
<i data-lucide="search" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
<button id="terminal-download" class="btn btn--ghost btn--sm" title="Baixar saída">
|
||||
<i data-lucide="download" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
<button id="terminal-copy" class="btn btn--ghost btn--sm" title="Copiar saída">
|
||||
<i data-lucide="copy" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="terminal-toolbar-right">
|
||||
<label class="terminal-toggle-label">
|
||||
<input type="checkbox" id="terminal-autoscroll" checked>
|
||||
<span>Auto-scroll</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal-search-bar" class="terminal-search-bar hidden">
|
||||
<input type="text" id="terminal-search-input" placeholder="Buscar no terminal..." class="input input--sm">
|
||||
<span id="terminal-search-count" class="terminal-search-count">0/0</span>
|
||||
<button id="terminal-search-prev" class="btn btn--ghost btn--sm">▲</button>
|
||||
<button id="terminal-search-next" class="btn btn--ghost btn--sm">▼</button>
|
||||
<button id="terminal-search-close" class="btn btn--ghost btn--sm">✕</button>
|
||||
</div>
|
||||
<div class="approval-notification" id="approval-notification" hidden></div>
|
||||
<div class="terminal-output" id="terminal-output" role="log" aria-live="polite" aria-label="Saída do terminal">
|
||||
<div class="terminal-welcome">
|
||||
@@ -471,6 +560,10 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<button id="history-export-csv" class="btn btn--ghost btn--sm" title="Exportar CSV">
|
||||
<i data-lucide="file-spreadsheet" style="width:14px;height:14px"></i>
|
||||
Exportar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm btn-danger" id="history-clear-btn" type="button">
|
||||
<i data-lucide="trash-2"></i>
|
||||
Limpar Histórico
|
||||
@@ -552,6 +645,64 @@
|
||||
<span class="system-info-label">Tempo Online</span>
|
||||
<span class="system-info-value font-mono" id="info-uptime">Carregando...</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Tema Atual</span>
|
||||
<span class="system-info-value" id="info-current-theme">Escuro</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Atalhos do Teclado</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="system-info-list">
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Fechar modal</span>
|
||||
<span class="system-info-value font-mono">Esc</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Novo agente (em Agentes)</span>
|
||||
<span class="system-info-value font-mono">N</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Dashboard</span>
|
||||
<span class="system-info-value font-mono">1</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Agentes</span>
|
||||
<span class="system-info-value font-mono">2</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Tarefas</span>
|
||||
<span class="system-info-value font-mono">3</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Agendamentos</span>
|
||||
<span class="system-info-value font-mono">4</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Pipelines</span>
|
||||
<span class="system-info-value font-mono">5</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Terminal</span>
|
||||
<span class="system-info-value font-mono">6</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Histórico</span>
|
||||
<span class="system-info-value font-mono">7</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Webhooks</span>
|
||||
<span class="system-info-value font-mono">8</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Configurações</span>
|
||||
<span class="system-info-value font-mono">9</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -778,11 +929,11 @@
|
||||
<span>Revisão OWASP</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Refatorar o código para melhorar legibilidade, manutenibilidade e aderência às boas práticas. Manter comportamento funcional intacto.">
|
||||
<i data-lucide="wand-2"></i>
|
||||
<i data-lucide="wand"></i>
|
||||
<span>Refatorar Código</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Escrever testes unitários e de integração abrangentes. Garantir cobertura dos casos de sucesso, erro e edge cases.">
|
||||
<i data-lucide="test-tube-2"></i>
|
||||
<i data-lucide="test-tube"></i>
|
||||
<span>Escrever Testes</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Documentar o código com JSDoc/docstrings, README atualizado, exemplos de uso e descrição de APIs públicas.">
|
||||
@@ -994,7 +1145,7 @@
|
||||
<div class="modal modal--sm">
|
||||
<div class="modal-header">
|
||||
<div class="confirm-modal-icon" id="confirm-modal-icon">
|
||||
<i data-lucide="alert-triangle"></i>
|
||||
<i data-lucide="triangle-alert"></i>
|
||||
</div>
|
||||
<h2 class="modal-title" id="confirm-modal-title">Confirmar Ação</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="confirm-modal-overlay">
|
||||
@@ -1060,7 +1211,7 @@
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="import-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="import-confirm-btn">
|
||||
<i data-lucide="upload"></i>
|
||||
<i data-lucide="file-down"></i>
|
||||
<span>Importar</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1116,7 +1267,7 @@
|
||||
|
||||
<div class="toast-container" id="toast-container" aria-live="assertive" aria-atomic="false" role="region" aria-label="Notificações"></div>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||
<script src="js/lucide.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/components/toast.js"></script>
|
||||
@@ -1130,9 +1281,10 @@
|
||||
<script src="js/components/settings.js"></script>
|
||||
<script src="js/components/history.js"></script>
|
||||
<script src="js/components/webhooks.js"></script>
|
||||
<script src="js/components/notifications.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
Utils.refreshIcons();
|
||||
App.init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -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