Tarefas executáveis, broadcast global para agendamentos e dashboard persistente
- Tarefas agora são templates executáveis com botão play e seleção de agente - Dropdown de tarefas salvas no modal de execução para reutilização rápida - Broadcast global no manager para execuções agendadas via cron aparecerem no terminal - Dashboard atividade recente agora consulta executionsStore persistente - Suporte a exibição de pipelines e agentes na atividade recente
This commit is contained in:
@@ -674,6 +674,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="execute-saved-task">Tarefa Salva</label>
|
||||||
|
<select class="select" id="execute-saved-task" name="savedTask">
|
||||||
|
<option value="">Digitar manualmente...</option>
|
||||||
|
</select>
|
||||||
|
<p class="form-hint">Selecione uma tarefa salva ou digite manualmente abaixo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="execute-task-desc">
|
<label class="form-label" for="execute-task-desc">
|
||||||
Descrição da Tarefa
|
Descrição da Tarefa
|
||||||
|
|||||||
@@ -278,6 +278,19 @@ const App = {
|
|||||||
App._handleExecute();
|
App._handleExecute();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
on('execute-saved-task', 'change', (e) => {
|
||||||
|
const taskId = e.target.value;
|
||||||
|
if (!taskId) return;
|
||||||
|
const task = (AgentsUI._savedTasksCache || []).find((t) => t.id === taskId);
|
||||||
|
if (!task) return;
|
||||||
|
const taskEl = document.getElementById('execute-task-desc');
|
||||||
|
if (taskEl) {
|
||||||
|
const parts = [task.name];
|
||||||
|
if (task.description) parts.push(task.description);
|
||||||
|
taskEl.value = parts.join('\n\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
on('tasks-new-btn', 'click', () => TasksUI.openCreateModal());
|
on('tasks-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||||
on('tasks-empty-new-btn', 'click', () => TasksUI.openCreateModal());
|
on('tasks-empty-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||||
|
|
||||||
@@ -412,6 +425,7 @@ const App = {
|
|||||||
const { action, id } = btn.dataset;
|
const { action, id } = btn.dataset;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case 'execute-task': TasksUI.execute(id); break;
|
||||||
case 'edit-task': TasksUI.openEditModal(id); break;
|
case 'edit-task': TasksUI.openEditModal(id); break;
|
||||||
case 'delete-task': TasksUI.delete(id); break;
|
case 'delete-task': TasksUI.delete(id); break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,12 +294,34 @@ const AgentsUI = {
|
|||||||
const instructionsEl = document.getElementById('execute-instructions');
|
const instructionsEl = document.getElementById('execute-instructions');
|
||||||
if (instructionsEl) instructionsEl.value = '';
|
if (instructionsEl) instructionsEl.value = '';
|
||||||
|
|
||||||
|
AgentsUI._loadSavedTasks();
|
||||||
|
|
||||||
Modal.open('execute-modal-overlay');
|
Modal.open('execute-modal-overlay');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async _loadSavedTasks() {
|
||||||
|
const savedTaskSelect = document.getElementById('execute-saved-task');
|
||||||
|
if (!savedTaskSelect) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tasks = await API.tasks.list();
|
||||||
|
savedTaskSelect.innerHTML = '<option value="">Digitar manualmente...</option>' +
|
||||||
|
tasks.map((t) => {
|
||||||
|
const label = t.category ? `[${t.category.toUpperCase()}] ${t.name}` : t.name;
|
||||||
|
return `<option value="${t.id}">${label}</option>`;
|
||||||
|
}).join('');
|
||||||
|
AgentsUI._savedTasksCache = tasks;
|
||||||
|
} catch {
|
||||||
|
savedTaskSelect.innerHTML = '<option value="">Digitar manualmente...</option>';
|
||||||
|
AgentsUI._savedTasksCache = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_savedTasksCache: [],
|
||||||
|
|
||||||
async export(agentId) {
|
async export(agentId) {
|
||||||
try {
|
try {
|
||||||
const data = await API.agents.export(agentId);
|
const data = await API.agents.export(agentId);
|
||||||
|
|||||||
@@ -66,19 +66,27 @@ const DashboardUI = {
|
|||||||
list.innerHTML = executions.map((exec) => {
|
list.innerHTML = executions.map((exec) => {
|
||||||
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
||||||
const statusLabel = DashboardUI._statusLabel(exec.status);
|
const statusLabel = DashboardUI._statusLabel(exec.status);
|
||||||
|
const name = exec.agentName || exec.pipelineName || exec.agentId || 'Execução';
|
||||||
|
const taskText = exec.task || exec.input || '';
|
||||||
|
const typeBadge = exec.type === 'pipeline'
|
||||||
|
? '<span class="badge badge--purple" style="font-size:0.6rem;padding:1px 5px;">Pipeline</span> '
|
||||||
|
: '';
|
||||||
const time = exec.startedAt
|
const time = exec.startedAt
|
||||||
? new Date(exec.startedAt).toLocaleTimeString('pt-BR')
|
? new Date(exec.startedAt).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
||||||
: '—';
|
: '—';
|
||||||
|
const date = exec.startedAt
|
||||||
|
? new Date(exec.startedAt).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
|
||||||
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<li class="activity-item">
|
<li class="activity-item">
|
||||||
<div class="activity-item-info">
|
<div class="activity-item-info">
|
||||||
<span class="activity-item-agent">${exec.agentName || exec.agentId || 'Agente'}</span>
|
<span class="activity-item-agent">${typeBadge}${name}</span>
|
||||||
<span class="activity-item-task">${exec.task || ''}</span>
|
<span class="activity-item-task">${taskText.length > 80 ? taskText.slice(0, 80) + '...' : taskText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="activity-item-meta">
|
<div class="activity-item-meta">
|
||||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||||
<span class="activity-item-time">${time}</span>
|
<span class="activity-item-time">${date} ${time}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ const TasksUI = {
|
|||||||
${createdAt}
|
${createdAt}
|
||||||
</span>
|
</span>
|
||||||
<div class="task-card-actions">
|
<div class="task-card-actions">
|
||||||
|
<button class="btn btn-primary btn-sm" data-action="execute-task" data-id="${task.id}" title="Executar tarefa">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn--ghost btn--sm" data-action="edit-task" data-id="${task.id}" title="Editar tarefa">
|
<button class="btn btn--ghost btn--sm" data-action="edit-task" data-id="${task.id}" title="Editar tarefa">
|
||||||
<i data-lucide="pencil"></i>
|
<i data-lucide="pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -209,6 +212,49 @@ const TasksUI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async execute(taskId) {
|
||||||
|
const task = TasksUI.tasks.find((t) => t.id === taskId);
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agents = await API.agents.list();
|
||||||
|
const activeAgents = agents.filter((a) => a.status === 'active');
|
||||||
|
|
||||||
|
if (activeAgents.length === 0) {
|
||||||
|
Toast.warning('Nenhum agente ativo disponível para executar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectEl = document.getElementById('execute-agent-select');
|
||||||
|
if (selectEl) {
|
||||||
|
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
|
activeAgents.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`).join('');
|
||||||
|
selectEl.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenId = document.getElementById('execute-agent-id');
|
||||||
|
if (hiddenId) hiddenId.value = '';
|
||||||
|
|
||||||
|
const taskEl = document.getElementById('execute-task-desc');
|
||||||
|
if (taskEl) {
|
||||||
|
const parts = [task.name];
|
||||||
|
if (task.description) parts.push(task.description);
|
||||||
|
taskEl.value = parts.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructionsEl = document.getElementById('execute-instructions');
|
||||||
|
if (instructionsEl) instructionsEl.value = '';
|
||||||
|
|
||||||
|
await AgentsUI._loadSavedTasks();
|
||||||
|
const savedTaskSelect = document.getElementById('execute-saved-task');
|
||||||
|
if (savedTaskSelect) savedTaskSelect.value = task.id;
|
||||||
|
|
||||||
|
Modal.open('execute-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao abrir execução: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_categoryClass(category) {
|
_categoryClass(category) {
|
||||||
const map = {
|
const map = {
|
||||||
'code-review': 'badge--blue',
|
'code-review': 'badge--blue',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { dirname, join } from 'path';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import apiRouter, { setWsBroadcast, setWsBroadcastTo } from './src/routes/api.js';
|
import apiRouter, { setWsBroadcast, setWsBroadcastTo } from './src/routes/api.js';
|
||||||
import * as manager from './src/agents/manager.js';
|
import * as manager from './src/agents/manager.js';
|
||||||
|
import { setGlobalBroadcast } from './src/agents/manager.js';
|
||||||
import { cancelAllExecutions } from './src/agents/executor.js';
|
import { cancelAllExecutions } from './src/agents/executor.js';
|
||||||
import { flushAllStores } from './src/store/db.js';
|
import { flushAllStores } from './src/store/db.js';
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ function broadcastTo(clientId, message) {
|
|||||||
|
|
||||||
setWsBroadcast(broadcast);
|
setWsBroadcast(broadcast);
|
||||||
setWsBroadcastTo(broadcastTo);
|
setWsBroadcastTo(broadcastTo);
|
||||||
|
setGlobalBroadcast(broadcast);
|
||||||
|
|
||||||
function gracefulShutdown(signal) {
|
function gracefulShutdown(signal) {
|
||||||
console.log(`\nSinal ${signal} recebido. Encerrando servidor...`);
|
console.log(`\nSinal ${signal} recebido. Encerrando servidor...`);
|
||||||
@@ -103,7 +105,7 @@ function gracefulShutdown(signal) {
|
|||||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
|
||||||
manager.restoreSchedules(broadcast);
|
manager.restoreSchedules();
|
||||||
|
|
||||||
httpServer.listen(PORT, () => {
|
httpServer.listen(PORT, () => {
|
||||||
console.log(`Painel administrativo disponível em http://localhost:${PORT}`);
|
console.log(`Painel administrativo disponível em http://localhost:${PORT}`);
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ const DEFAULT_CONFIG = {
|
|||||||
const MAX_RECENT = 200;
|
const MAX_RECENT = 200;
|
||||||
const recentExecBuffer = [];
|
const recentExecBuffer = [];
|
||||||
|
|
||||||
|
let globalBroadcast = null;
|
||||||
|
|
||||||
|
export function setGlobalBroadcast(fn) {
|
||||||
|
globalBroadcast = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWsCallback(wsCallback) {
|
||||||
|
return wsCallback || globalBroadcast || null;
|
||||||
|
}
|
||||||
|
|
||||||
let dailyExecutionCount = 0;
|
let dailyExecutionCount = 0;
|
||||||
let dailyCountDate = new Date().toDateString();
|
let dailyCountDate = new Date().toDateString();
|
||||||
|
|
||||||
@@ -100,6 +110,7 @@ export function executeTask(agentId, task, instructions, wsCallback) {
|
|||||||
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||||
if (agent.status !== 'active') throw new Error(`Agente ${agentId} está inativo`);
|
if (agent.status !== 'active') throw new Error(`Agente ${agentId} está inativo`);
|
||||||
|
|
||||||
|
const cb = getWsCallback(wsCallback);
|
||||||
const taskText = typeof task === 'string' ? task : task.description;
|
const taskText = typeof task === 'string' ? task : task.description;
|
||||||
const startedAt = new Date().toISOString();
|
const startedAt = new Date().toISOString();
|
||||||
|
|
||||||
@@ -127,13 +138,13 @@ export function executeTask(agentId, task, instructions, wsCallback) {
|
|||||||
{ description: task, instructions },
|
{ description: task, instructions },
|
||||||
{
|
{
|
||||||
onData: (parsed, execId) => {
|
onData: (parsed, execId) => {
|
||||||
if (wsCallback) wsCallback({ type: 'execution_output', executionId: execId, agentId, data: parsed });
|
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
|
||||||
},
|
},
|
||||||
onError: (err, execId) => {
|
onError: (err, execId) => {
|
||||||
const endedAt = new Date().toISOString();
|
const endedAt = new Date().toISOString();
|
||||||
updateExecutionRecord(agentId, execId, { status: 'error', error: err.message, endedAt });
|
updateExecutionRecord(agentId, execId, { status: 'error', error: err.message, endedAt });
|
||||||
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
|
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
|
||||||
if (wsCallback) wsCallback({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
|
if (cb) cb({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
|
||||||
},
|
},
|
||||||
onComplete: (result, execId) => {
|
onComplete: (result, execId) => {
|
||||||
const endedAt = new Date().toISOString();
|
const endedAt = new Date().toISOString();
|
||||||
@@ -144,7 +155,7 @@ export function executeTask(agentId, task, instructions, wsCallback) {
|
|||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
endedAt,
|
endedAt,
|
||||||
});
|
});
|
||||||
if (wsCallback) wsCallback({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -205,7 +216,7 @@ export function scheduleTask(agentId, taskDescription, cronExpression, wsCallbac
|
|||||||
schedulesStore.save(items);
|
schedulesStore.save(items);
|
||||||
|
|
||||||
scheduler.schedule(scheduleId, cronExpression, () => {
|
scheduler.schedule(scheduleId, cronExpression, () => {
|
||||||
executeTask(agentId, taskDescription, null, wsCallback);
|
executeTask(agentId, taskDescription, null, null);
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
return { scheduleId, agentId, agentName: agent.agent_name, taskDescription, cronExpression };
|
return { scheduleId, agentId, agentName: agent.agent_name, taskDescription, cronExpression };
|
||||||
@@ -223,7 +234,7 @@ export function updateScheduleTask(scheduleId, data, wsCallback) {
|
|||||||
const cronExpression = data.cronExpression || stored.cronExpression;
|
const cronExpression = data.cronExpression || stored.cronExpression;
|
||||||
|
|
||||||
scheduler.updateSchedule(scheduleId, cronExpression, () => {
|
scheduler.updateSchedule(scheduleId, cronExpression, () => {
|
||||||
executeTask(agentId, taskDescription, null, wsCallback);
|
executeTask(agentId, taskDescription, null, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
schedulesStore.update(scheduleId, { agentId, agentName: agent.agent_name, taskDescription, cronExpression });
|
schedulesStore.update(scheduleId, { agentId, agentName: agent.agent_name, taskDescription, cronExpression });
|
||||||
@@ -266,10 +277,10 @@ export function importAgent(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function restoreSchedules(wsCallback) {
|
export function restoreSchedules() {
|
||||||
scheduler.restoreSchedules((agentId, taskDescription) => {
|
scheduler.restoreSchedules((agentId, taskDescription) => {
|
||||||
try {
|
try {
|
||||||
executeTask(agentId, taskDescription, null, wsCallback);
|
executeTask(agentId, taskDescription, null, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`[manager] Erro ao executar tarefa agendada: ${err.message}`);
|
console.log(`[manager] Erro ao executar tarefa agendada: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,7 +436,9 @@ router.get('/executions/active', (req, res) => {
|
|||||||
router.get('/executions/recent', (req, res) => {
|
router.get('/executions/recent', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const limit = parseInt(req.query.limit) || 20;
|
const limit = parseInt(req.query.limit) || 20;
|
||||||
res.json(manager.getRecentExecutions(limit));
|
const items = executionsStore.getAll();
|
||||||
|
items.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
|
||||||
|
res.json(items.slice(0, limit));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user