Correções de bugs, layout de cards e webhook test funcional
- Pipeline cancel/approve/reject corrigido com busca bidirecional - Secrets injetados no executor via cleanEnv - Versionamento automático ao atualizar agentes - writeJsonAsync com log de erro - Removido asyncHandler.js (código morto) - Restaurado permissionMode padrão bypassPermissions - Ícones dos cards alinhados à direita com wrapper - Botão Editar convertido para ícone nos cards - Webhook test agora dispara execução real do agente/pipeline - Corrigido App.navigateTo no teste de webhook
This commit is contained in:
@@ -529,12 +529,19 @@ textarea {
|
||||
|
||||
.agent-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.agent-actions-icons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -683,7 +690,9 @@ textarea {
|
||||
.btn-sm.btn-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@@ -3243,6 +3252,7 @@ tbody tr:hover td {
|
||||
|
||||
.agent-card-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--border-primary);
|
||||
@@ -4268,3 +4278,235 @@ body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metr
|
||||
.report-toast:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ─── Secrets Management ─── */
|
||||
|
||||
.form-divider {
|
||||
height: 1px;
|
||||
background-color: var(--border-primary);
|
||||
margin: 24px 0 20px;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-section-title i,
|
||||
.form-section-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.secrets-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.secret-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.secret-item:hover {
|
||||
border-color: var(--border-secondary);
|
||||
}
|
||||
|
||||
.secret-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.secret-value-placeholder {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.secrets-add-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.secrets-add-form .input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.secrets-add-form .input:first-child {
|
||||
max-width: 220px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ─── Version History Timeline ─── */
|
||||
|
||||
.versions-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.version-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.version-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--border-secondary);
|
||||
border: 2px solid var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.version-dot--active {
|
||||
background-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
|
||||
.version-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
background-color: var(--border-primary);
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.version-content {
|
||||
flex: 1;
|
||||
padding-bottom: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.version-number {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.version-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.version-changes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.version-field-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background-color: var(--accent-glow);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.version-changelog {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.version-item--latest .version-content {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ─── Light theme overrides for new elements ─── */
|
||||
|
||||
[data-theme="light"] .secret-item {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .secret-item:hover {
|
||||
border-color: #c8ccd6;
|
||||
}
|
||||
|
||||
[data-theme="light"] .version-dot {
|
||||
border-color: var(--bg-secondary);
|
||||
background-color: #c8ccd6;
|
||||
}
|
||||
|
||||
[data-theme="light"] .version-dot--active {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
[data-theme="light"] .version-line {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .version-field-badge {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-divider {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .secrets-add-form .input {
|
||||
background-color: var(--bg-input);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ─── Responsive adjustments ─── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.secrets-add-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.secrets-add-form .input:first-child {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,6 +838,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Retry em caso de falha</label>
|
||||
<div class="toggle-wrapper">
|
||||
<input type="checkbox" class="toggle-input" id="agent-retry-toggle" name="retryOnFailure" role="switch" />
|
||||
<label class="toggle-label" for="agent-retry-toggle">
|
||||
<span class="toggle-thumb"></span>
|
||||
<span class="toggle-text-on">Sim</span>
|
||||
<span class="toggle-text-off">Não</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="agent-retry-max-group" style="display:none;">
|
||||
<label class="form-label" for="agent-retry-max">Máximo de tentativas</label>
|
||||
<select class="select" id="agent-retry-max" name="maxRetries">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3" selected>3</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="agent-tags-input">Tags</label>
|
||||
<div class="tags-input-wrapper" id="agent-tags-wrapper">
|
||||
@@ -852,6 +874,24 @@
|
||||
</div>
|
||||
<input type="hidden" id="agent-tags" name="tags" value="[]" />
|
||||
</div>
|
||||
|
||||
<div class="form-divider"></div>
|
||||
<div class="form-section" id="agent-secrets-section" hidden>
|
||||
<h3 class="form-section-title">
|
||||
<i data-lucide="key-round"></i>
|
||||
Variáveis de Ambiente (Secrets)
|
||||
</h3>
|
||||
<p class="form-hint mb-12">Secrets são injetados como variáveis de ambiente na execução. Valores nunca são exibidos após salvos.</p>
|
||||
<div id="agent-secrets-list" class="secrets-list"></div>
|
||||
<div class="secrets-add-form">
|
||||
<input type="text" class="input" id="agent-secret-name" placeholder="NOME_DA_VARIAVEL" autocomplete="off" />
|
||||
<input type="password" class="input" id="agent-secret-value" placeholder="valor secreto" autocomplete="new-password" />
|
||||
<button type="button" class="btn btn--primary btn--sm" id="agent-secret-add-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
Adicionar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -861,6 +901,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="agent-versions-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="agent-versions-title" hidden>
|
||||
<div class="modal modal--lg">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="agent-versions-title">Histórico de Versões</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="agent-versions-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="agent-versions-content">
|
||||
<div class="empty-state">
|
||||
<i data-lucide="history"></i>
|
||||
<p>Carregando versões...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="execute-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="execute-modal-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
|
||||
@@ -46,6 +46,17 @@ const API = {
|
||||
duplicate(id) { return API.request('POST', `/agents/${id}/duplicate`); },
|
||||
},
|
||||
|
||||
secrets: {
|
||||
list(agentId) { return API.request('GET', `/agents/${agentId}/secrets`); },
|
||||
create(agentId, data) { return API.request('POST', `/agents/${agentId}/secrets`, data); },
|
||||
delete(agentId, name) { return API.request('DELETE', `/agents/${agentId}/secrets/${encodeURIComponent(name)}`); },
|
||||
},
|
||||
|
||||
versions: {
|
||||
list(agentId) { return API.request('GET', `/agents/${agentId}/versions`); },
|
||||
restore(agentId, version) { return API.request('POST', `/agents/${agentId}/versions/${version}/restore`); },
|
||||
},
|
||||
|
||||
tasks: {
|
||||
list() { return API.request('GET', '/tasks'); },
|
||||
create(data) { return API.request('POST', '/tasks', data); },
|
||||
|
||||
@@ -241,6 +241,15 @@ const App = {
|
||||
App._updateActiveBadge();
|
||||
break;
|
||||
|
||||
case 'execution_retry':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(
|
||||
`Retry ${data.attempt || '?'}/${data.maxRetries || '?'} — próxima tentativa em ${data.nextRetryIn || '?'}s. Motivo: ${data.reason || 'erro na execução'}`,
|
||||
'warning',
|
||||
data.executionId
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pipeline_step_output': {
|
||||
Terminal.stopProcessing();
|
||||
const stepEvtType = data.data?.type || 'chunk';
|
||||
@@ -652,6 +661,7 @@ const App = {
|
||||
case 'export': AgentsUI.export(id); break;
|
||||
case 'delete': AgentsUI.delete(id); break;
|
||||
case 'duplicate': AgentsUI.duplicate(id); break;
|
||||
case 'versions': AgentsUI.openVersionsModal(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -112,19 +112,23 @@ const AgentsUI = {
|
||||
<i data-lucide="play"></i>
|
||||
Executar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" data-action="edit" data-id="${agent.id}">
|
||||
<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>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete" data-id="${agent.id}" title="Excluir agente">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
<div class="agent-actions-icons">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="edit" data-id="${agent.id}" title="Editar agente">
|
||||
<i data-lucide="pencil"></i>
|
||||
</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>
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="versions" data-id="${agent.id}" title="Histórico de versões">
|
||||
<i data-lucide="history"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete" data-id="${agent.id}" title="Excluir agente">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -158,7 +162,23 @@ const AgentsUI = {
|
||||
const permissionMode = document.getElementById('agent-permission-mode');
|
||||
if (permissionMode) permissionMode.value = '';
|
||||
|
||||
const retryToggle = document.getElementById('agent-retry-toggle');
|
||||
if (retryToggle) retryToggle.checked = false;
|
||||
|
||||
const retryMaxGroup = document.getElementById('agent-retry-max-group');
|
||||
if (retryMaxGroup) retryMaxGroup.style.display = 'none';
|
||||
|
||||
const retryMax = document.getElementById('agent-retry-max');
|
||||
if (retryMax) retryMax.value = '3';
|
||||
|
||||
const secretsSection = document.getElementById('agent-secrets-section');
|
||||
if (secretsSection) secretsSection.hidden = true;
|
||||
|
||||
const secretsList = document.getElementById('agent-secrets-list');
|
||||
if (secretsList) secretsList.innerHTML = '';
|
||||
|
||||
Modal.open('agent-modal-overlay');
|
||||
AgentsUI._setupModalListeners();
|
||||
},
|
||||
|
||||
async openEditModal(agentId) {
|
||||
@@ -199,7 +219,23 @@ const AgentsUI = {
|
||||
).join('');
|
||||
}
|
||||
|
||||
const retryToggle = document.getElementById('agent-retry-toggle');
|
||||
const retryOnFailure = agent.config && agent.config.retryOnFailure;
|
||||
if (retryToggle) retryToggle.checked = !!retryOnFailure;
|
||||
|
||||
const retryMaxGroup = document.getElementById('agent-retry-max-group');
|
||||
if (retryMaxGroup) retryMaxGroup.style.display = retryOnFailure ? '' : 'none';
|
||||
|
||||
const retryMax = document.getElementById('agent-retry-max');
|
||||
if (retryMax) retryMax.value = (agent.config && agent.config.maxRetries) || '3';
|
||||
|
||||
const secretsSection = document.getElementById('agent-secrets-section');
|
||||
if (secretsSection) secretsSection.hidden = false;
|
||||
|
||||
AgentsUI._loadSecrets(agent.id);
|
||||
|
||||
Modal.open('agent-modal-overlay');
|
||||
AgentsUI._setupModalListeners();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar agente: ${err.message}`);
|
||||
}
|
||||
@@ -237,6 +273,8 @@ const AgentsUI = {
|
||||
allowedTools: document.getElementById('agent-allowed-tools')?.value.trim() || '',
|
||||
maxTurns: parseInt(document.getElementById('agent-max-turns')?.value) || 0,
|
||||
permissionMode: document.getElementById('agent-permission-mode')?.value || '',
|
||||
retryOnFailure: !!document.getElementById('agent-retry-toggle')?.checked,
|
||||
maxRetries: parseInt(document.getElementById('agent-retry-max')?.value) || 3,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -406,6 +444,223 @@ const AgentsUI = {
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
_setupModalListeners() {
|
||||
const retryToggle = document.getElementById('agent-retry-toggle');
|
||||
const retryMaxGroup = document.getElementById('agent-retry-max-group');
|
||||
|
||||
if (retryToggle && !retryToggle._listenerAdded) {
|
||||
retryToggle._listenerAdded = true;
|
||||
retryToggle.addEventListener('change', () => {
|
||||
if (retryMaxGroup) retryMaxGroup.style.display = retryToggle.checked ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
const addSecretBtn = document.getElementById('agent-secret-add-btn');
|
||||
if (addSecretBtn && !addSecretBtn._listenerAdded) {
|
||||
addSecretBtn._listenerAdded = true;
|
||||
addSecretBtn.addEventListener('click', () => {
|
||||
const agentId = document.getElementById('agent-form-id')?.value;
|
||||
if (agentId) {
|
||||
AgentsUI._addSecret(agentId);
|
||||
} else {
|
||||
Toast.warning('Salve o agente primeiro para adicionar secrets');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async _loadSecrets(agentId) {
|
||||
const list = document.getElementById('agent-secrets-list');
|
||||
if (!list) return;
|
||||
|
||||
try {
|
||||
const secrets = await API.secrets.list(agentId);
|
||||
const items = Array.isArray(secrets) ? secrets : (secrets?.secrets || []);
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<p class="text-muted text-sm">Nenhum secret configurado.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = items.map(s => `
|
||||
<div class="secret-item">
|
||||
<span class="secret-name font-mono">${Utils.escapeHtml(s.name || s)}</span>
|
||||
<span class="secret-value-placeholder">••••••••</span>
|
||||
<button type="button" class="btn btn-ghost btn-icon btn-sm btn-danger" data-secret-delete="${Utils.escapeHtml(s.name || s)}" data-agent-id="${agentId}" title="Remover secret">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
Utils.refreshIcons(list);
|
||||
|
||||
list.querySelectorAll('[data-secret-delete]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
AgentsUI._deleteSecret(btn.dataset.agentId, btn.dataset.secretDelete);
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
list.innerHTML = '<p class="text-muted text-sm">Erro ao carregar secrets.</p>';
|
||||
}
|
||||
},
|
||||
|
||||
async _addSecret(agentId) {
|
||||
const nameEl = document.getElementById('agent-secret-name');
|
||||
const valueEl = document.getElementById('agent-secret-value');
|
||||
const name = nameEl?.value.trim();
|
||||
const value = valueEl?.value;
|
||||
|
||||
if (!name) {
|
||||
Toast.warning('Nome do secret é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
Toast.warning('Valor do secret é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await API.secrets.create(agentId, { name, value });
|
||||
Toast.success(`Secret "${name}" salvo`);
|
||||
if (nameEl) nameEl.value = '';
|
||||
if (valueEl) valueEl.value = '';
|
||||
AgentsUI._loadSecrets(agentId);
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao salvar secret: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async _deleteSecret(agentId, secretName) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Remover secret',
|
||||
`Tem certeza que deseja remover o secret "${secretName}"?`
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.secrets.delete(agentId, secretName);
|
||||
Toast.success(`Secret "${secretName}" removido`);
|
||||
AgentsUI._loadSecrets(agentId);
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao remover secret: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async openVersionsModal(agentId) {
|
||||
const agent = AgentsUI.agents.find(a => a.id === agentId);
|
||||
const titleEl = document.getElementById('agent-versions-title');
|
||||
const contentEl = document.getElementById('agent-versions-content');
|
||||
|
||||
if (titleEl) titleEl.textContent = `Versões — ${agent?.agent_name || agent?.name || 'Agente'}`;
|
||||
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = '<div class="flex flex-center gap-8"><div class="spinner"></div><span class="text-secondary">Carregando versões...</span></div>';
|
||||
}
|
||||
|
||||
Modal.open('agent-versions-modal-overlay');
|
||||
|
||||
try {
|
||||
const versions = await API.versions.list(agentId);
|
||||
const items = Array.isArray(versions) ? versions : (versions?.versions || []);
|
||||
|
||||
if (!contentEl) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
contentEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i data-lucide="history"></i></div>
|
||||
<h3 class="empty-state-title">Sem histórico de versões</h3>
|
||||
<p class="empty-state-desc">As alterações neste agente serão registradas aqui automaticamente.</p>
|
||||
</div>`;
|
||||
Utils.refreshIcons(contentEl);
|
||||
return;
|
||||
}
|
||||
|
||||
contentEl.innerHTML = `
|
||||
<div class="versions-timeline">
|
||||
${items.map((v, i) => {
|
||||
const date = v.changedAt ? new Date(v.changedAt).toLocaleString('pt-BR') : '—';
|
||||
const changedFields = AgentsUI._getChangedFields(v);
|
||||
const isLatest = i === 0;
|
||||
|
||||
return `
|
||||
<div class="version-item ${isLatest ? 'version-item--latest' : ''}">
|
||||
<div class="version-node">
|
||||
<div class="version-dot ${isLatest ? 'version-dot--active' : ''}"></div>
|
||||
${i < items.length - 1 ? '<div class="version-line"></div>' : ''}
|
||||
</div>
|
||||
<div class="version-content">
|
||||
<div class="version-header">
|
||||
<span class="version-number">v${v.version || items.length - i}</span>
|
||||
<span class="version-date">${date}</span>
|
||||
${!isLatest ? `<button class="btn btn-ghost btn-sm" data-restore-version="${v.version || items.length - i}" data-agent-id="${agentId}" type="button">
|
||||
<i data-lucide="undo-2"></i> Restaurar
|
||||
</button>` : '<span class="badge badge-active">Atual</span>'}
|
||||
</div>
|
||||
${changedFields ? `<div class="version-changes">${changedFields}</div>` : ''}
|
||||
${v.changelog ? `<p class="version-changelog">${Utils.escapeHtml(v.changelog)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>`;
|
||||
|
||||
Utils.refreshIcons(contentEl);
|
||||
|
||||
contentEl.querySelectorAll('[data-restore-version]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const version = btn.dataset.restoreVersion;
|
||||
const aid = btn.dataset.agentId;
|
||||
const confirmed = await Modal.confirm(
|
||||
'Restaurar versão',
|
||||
`Deseja restaurar a versão v${version} deste agente? A configuração atual será substituída.`
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.versions.restore(aid, version);
|
||||
Toast.success(`Versão v${version} restaurada`);
|
||||
Modal.close('agent-versions-modal-overlay');
|
||||
await AgentsUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao restaurar versão: ${err.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon"><i data-lucide="alert-circle"></i></div>
|
||||
<h3 class="empty-state-title">Erro ao carregar versões</h3>
|
||||
<p class="empty-state-desc">${Utils.escapeHtml(err.message)}</p>
|
||||
</div>`;
|
||||
Utils.refreshIcons(contentEl);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getChangedFields(version) {
|
||||
if (!version.config) return '';
|
||||
const fieldLabels = {
|
||||
systemPrompt: 'System Prompt',
|
||||
model: 'Modelo',
|
||||
workingDirectory: 'Diretório',
|
||||
allowedTools: 'Ferramentas',
|
||||
maxTurns: 'Max Turns',
|
||||
permissionMode: 'Permission Mode',
|
||||
retryOnFailure: 'Retry',
|
||||
};
|
||||
|
||||
const fields = Object.keys(version.config || {}).filter(k => fieldLabels[k]);
|
||||
if (fields.length === 0) return '';
|
||||
|
||||
return fields.map(f =>
|
||||
`<span class="version-field-badge">${fieldLabels[f] || f}</span>`
|
||||
).join('');
|
||||
},
|
||||
};
|
||||
|
||||
window.AgentsUI = AgentsUI;
|
||||
|
||||
@@ -369,6 +369,18 @@ const DashboardUI = {
|
||||
wsBadge.textContent = wsConnected ? 'Conectado' : 'Desconectado';
|
||||
wsBadge.className = `badge ${wsConnected ? 'badge--green' : 'badge--red'}`;
|
||||
}
|
||||
|
||||
const claudeBadge = document.getElementById('system-claude-status-badge');
|
||||
if (claudeBadge) {
|
||||
API.system.info().then((info) => {
|
||||
const available = info.claudeVersion && info.claudeVersion !== 'N/A';
|
||||
claudeBadge.textContent = available ? info.claudeVersion : 'Indisponível';
|
||||
claudeBadge.className = `badge ${available ? 'badge--green' : 'badge--red'}`;
|
||||
}).catch(() => {
|
||||
claudeBadge.textContent = 'Indisponível';
|
||||
claudeBadge.className = 'badge badge--red';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_statusBadgeClass(status) {
|
||||
|
||||
@@ -119,13 +119,14 @@ const PipelinesUI = {
|
||||
<i data-lucide="play"></i>
|
||||
Executar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" data-action="edit-pipeline" data-id="${pipeline.id}">
|
||||
<i data-lucide="pencil"></i>
|
||||
Editar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete-pipeline" data-id="${pipeline.id}" title="Excluir pipeline">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
<div class="agent-actions-icons">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="edit-pipeline" data-id="${pipeline.id}" title="Editar pipeline">
|
||||
<i data-lucide="pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete-pipeline" data-id="${pipeline.id}" title="Excluir pipeline">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -181,7 +181,10 @@ const WebhooksUI = {
|
||||
async test(webhookId) {
|
||||
try {
|
||||
const result = await API.webhooks.test(webhookId);
|
||||
Toast.success(result.message || 'Webhook testado com sucesso');
|
||||
Toast.success(result.message || 'Webhook disparado com sucesso');
|
||||
if (result.executionId || result.pipelineId) {
|
||||
App.navigateTo('terminal');
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao testar webhook: ${err.message}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user