Evolução da plataforma: dashboard com gráficos, notificações, relatórios automáticos, ícones Lucide local e melhorias gerais
- Dashboard com 5 gráficos Chart.js (execuções, status, custo, agentes, pipelines) - Sistema de notificações com polling, badge e Browser Notification API - Relatórios MD automáticos para execuções de agentes e pipelines (data/reports/) - Lucide local (v0.475.0) com nomes de ícones atualizados e refreshIcons centralizado - Correção de ícones icon-only (padding CSS sobrescrito por btn-sm) - Cards de agentes e pipelines com botões alinhados na base (flex column) - Terminal com busca, download, cópia e auto-scroll toggle - Histórico com export CSV, retry, paginação e truncamento de texto - Webhooks com edição e teste inline - Duplicação de agentes e export/import JSON - Rate limiting, CORS, correlação de requests e health check no backend - Escrita atômica em JSON (temp + rename) e store de notificações - Tema claro/escuro com toggle e persistência em localStorage - Atalhos de teclado 1-9 para navegação entre seções
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { agentsStore, schedulesStore, executionsStore } from '../store/db.js';
|
||||
import { agentsStore, schedulesStore, executionsStore, notificationsStore } from '../store/db.js';
|
||||
import * as executor from './executor.js';
|
||||
import * as scheduler from './scheduler.js';
|
||||
import { generateAgentReport } from '../reports/generator.js';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
model: 'claude-sonnet-4-6',
|
||||
@@ -25,6 +26,14 @@ function getWsCallback(wsCallback) {
|
||||
return wsCallback || globalBroadcast || null;
|
||||
}
|
||||
|
||||
function createNotification(type, title, message, metadata = {}) {
|
||||
notificationsStore.create({
|
||||
type, title, message, metadata,
|
||||
read: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
let dailyExecutionCount = 0;
|
||||
let dailyCountDate = new Date().toDateString();
|
||||
|
||||
@@ -145,6 +154,7 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
|
||||
const endedAt = new Date().toISOString();
|
||||
updateExecutionRecord(agentId, execId, { status: 'error', error: err.message, endedAt });
|
||||
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
|
||||
createNotification('error', 'Execução falhou', `Agente "${agent.agent_name}" encontrou um erro`, { agentId, executionId: execId });
|
||||
if (cb) cb({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
|
||||
},
|
||||
onComplete: (result, execId) => {
|
||||
@@ -161,6 +171,14 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
|
||||
numTurns: result.numTurns || 0,
|
||||
sessionId: result.sessionId || '',
|
||||
});
|
||||
createNotification('success', 'Execução concluída', `Agente "${agent.agent_name}" finalizou a tarefa`, { agentId, executionId: execId });
|
||||
try {
|
||||
const updated = executionsStore.getById(historyRecord.id);
|
||||
if (updated) {
|
||||
const report = generateAgentReport(updated);
|
||||
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) {}
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||
},
|
||||
}
|
||||
@@ -290,6 +308,13 @@ export function continueConversation(agentId, sessionId, message, wsCallback) {
|
||||
numTurns: result.numTurns || 0,
|
||||
sessionId: result.sessionId || sessionId,
|
||||
});
|
||||
try {
|
||||
const updated = executionsStore.getById(historyRecord.id);
|
||||
if (updated) {
|
||||
const report = generateAgentReport(updated);
|
||||
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) {}
|
||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { pipelinesStore, agentsStore, executionsStore } from '../store/db.js';
|
||||
import * as executor from './executor.js';
|
||||
import { mem } from '../cache/index.js';
|
||||
import { generatePipelineReport } from '../reports/generator.js';
|
||||
|
||||
const activePipelines = new Map();
|
||||
const AGENT_MAP_TTL = 30_000;
|
||||
@@ -265,8 +266,15 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
||||
totalCostUsd: totalCost,
|
||||
});
|
||||
|
||||
if (!pipelineState.canceled && wsCallback) {
|
||||
wsCallback({ type: 'pipeline_complete', pipelineId, results, totalCostUsd: totalCost });
|
||||
if (!pipelineState.canceled) {
|
||||
try {
|
||||
const updated = executionsStore.getById(historyRecord.id);
|
||||
if (updated) {
|
||||
const report = generatePipelineReport(updated);
|
||||
if (wsCallback) wsCallback({ type: 'report_generated', pipelineId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) {}
|
||||
if (wsCallback) wsCallback({ type: 'pipeline_complete', pipelineId, results, totalCostUsd: totalCost });
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -300,6 +308,15 @@ export function cancelPipeline(pipelineId) {
|
||||
}
|
||||
if (state.currentExecutionId) executor.cancel(state.currentExecutionId);
|
||||
activePipelines.delete(pipelineId);
|
||||
|
||||
const allExecs = executionsStore.getAll();
|
||||
const idx = allExecs.findIndex(e => e.pipelineId === pipelineId && (e.status === 'running' || e.status === 'awaiting_approval'));
|
||||
if (idx !== -1) {
|
||||
allExecs[idx].status = 'canceled';
|
||||
allExecs[idx].endedAt = new Date().toISOString();
|
||||
executionsStore.save(allExecs);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
188
src/reports/generator.js
Normal file
188
src/reports/generator.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPORTS_DIR = join(__dirname, '..', '..', 'data', 'reports');
|
||||
|
||||
function ensureDir() {
|
||||
if (!existsSync(REPORTS_DIR)) mkdirSync(REPORTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function sanitizeFilename(name) {
|
||||
return name.replace(/[^a-zA-Z0-9À-ÿ_-]/g, '_').slice(0, 60);
|
||||
}
|
||||
|
||||
function timestamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleString('pt-BR', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(startIso, endIso) {
|
||||
if (!startIso || !endIso) return '—';
|
||||
const ms = new Date(endIso) - new Date(startIso);
|
||||
if (ms < 0) return '—';
|
||||
const s = Math.floor(ms / 1000);
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
const h = Math.floor(m / 60);
|
||||
if (h > 0) return `${h}h ${m % 60}m ${s % 60}s`;
|
||||
return `${m}m ${s % 60}s`;
|
||||
}
|
||||
|
||||
export function generateAgentReport(execution) {
|
||||
ensureDir();
|
||||
|
||||
const name = execution.agentName || 'Agente';
|
||||
const filename = `agente_${sanitizeFilename(name)}_${timestamp()}.md`;
|
||||
const filepath = join(REPORTS_DIR, filename);
|
||||
|
||||
const status = execution.status === 'completed' ? '✅ Concluído' : '❌ Erro';
|
||||
const cost = (execution.costUsd || execution.totalCostUsd || 0).toFixed(4);
|
||||
|
||||
const lines = [
|
||||
`# Relatório de Execução — ${name}`,
|
||||
'',
|
||||
`**Data:** ${formatDate(execution.startedAt)}`,
|
||||
`**Status:** ${status}`,
|
||||
`**Duração:** ${formatDuration(execution.startedAt, execution.endedAt)}`,
|
||||
`**Custo:** $${cost}`,
|
||||
`**Turnos:** ${execution.numTurns || '—'}`,
|
||||
`**Session ID:** \`${execution.sessionId || '—'}\``,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Tarefa',
|
||||
'',
|
||||
execution.task || '_(sem tarefa definida)_',
|
||||
'',
|
||||
];
|
||||
|
||||
if (execution.instructions) {
|
||||
lines.push('## Instruções Adicionais', '', execution.instructions, '');
|
||||
}
|
||||
|
||||
lines.push('---', '', '## Resultado', '');
|
||||
|
||||
if (execution.status === 'error' && execution.error) {
|
||||
lines.push('### Erro', '', '```', execution.error, '```', '');
|
||||
}
|
||||
|
||||
if (execution.result) {
|
||||
lines.push(execution.result);
|
||||
} else {
|
||||
lines.push('_(sem resultado textual)_');
|
||||
}
|
||||
|
||||
lines.push('', '---', '', `_Relatório gerado automaticamente em ${formatDate(new Date().toISOString())}_`);
|
||||
|
||||
writeFileSync(filepath, lines.join('\n'), 'utf-8');
|
||||
return { filename, filepath };
|
||||
}
|
||||
|
||||
export function generatePipelineReport(execution) {
|
||||
ensureDir();
|
||||
|
||||
const name = execution.pipelineName || 'Pipeline';
|
||||
const filename = `pipeline_${sanitizeFilename(name)}_${timestamp()}.md`;
|
||||
const filepath = join(REPORTS_DIR, filename);
|
||||
|
||||
const status = execution.status === 'completed' ? '✅ Concluído'
|
||||
: execution.status === 'error' ? '❌ Erro'
|
||||
: execution.status === 'canceled' ? '⚠️ Cancelado'
|
||||
: execution.status;
|
||||
|
||||
const totalCost = (execution.totalCostUsd || 0).toFixed(4);
|
||||
const steps = Array.isArray(execution.steps) ? execution.steps : [];
|
||||
|
||||
const lines = [
|
||||
`# Relatório de Pipeline — ${name}`,
|
||||
'',
|
||||
`**Data:** ${formatDate(execution.startedAt)}`,
|
||||
`**Status:** ${status}`,
|
||||
`**Duração:** ${formatDuration(execution.startedAt, execution.endedAt)}`,
|
||||
`**Custo Total:** $${totalCost}`,
|
||||
`**Passos:** ${steps.length}`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Input Inicial',
|
||||
'',
|
||||
execution.input || '_(sem input)_',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## Execução dos Passos',
|
||||
'',
|
||||
];
|
||||
|
||||
steps.forEach((step, i) => {
|
||||
const stepStatus = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '⏳';
|
||||
const stepCost = (step.costUsd || 0).toFixed(4);
|
||||
const stepDuration = formatDuration(step.startedAt, step.endedAt);
|
||||
|
||||
lines.push(
|
||||
`### Passo ${i + 1} — ${step.agentName || 'Agente'} ${stepStatus}`,
|
||||
'',
|
||||
`| Propriedade | Valor |`,
|
||||
`|-------------|-------|`,
|
||||
`| Status | ${step.status || '—'} |`,
|
||||
`| Duração | ${stepDuration} |`,
|
||||
`| Custo | $${stepCost} |`,
|
||||
`| Turnos | ${step.numTurns || '—'} |`,
|
||||
'',
|
||||
);
|
||||
|
||||
if (step.prompt) {
|
||||
lines.push(
|
||||
'<details>',
|
||||
'<summary>Prompt utilizado</summary>',
|
||||
'',
|
||||
'```',
|
||||
step.prompt,
|
||||
'```',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
if (step.result) {
|
||||
lines.push('**Resultado:**', '', step.result, '');
|
||||
} else if (step.status === 'error') {
|
||||
lines.push('**Erro:** Passo falhou durante a execução.', '');
|
||||
}
|
||||
|
||||
if (i < steps.length - 1) {
|
||||
lines.push('---', '');
|
||||
}
|
||||
});
|
||||
|
||||
if (execution.error) {
|
||||
lines.push('---', '', '## Erro da Pipeline', '', '```', execution.error, '```', '');
|
||||
}
|
||||
|
||||
const lastStep = steps[steps.length - 1];
|
||||
if (execution.status === 'completed' && lastStep?.result) {
|
||||
lines.push(
|
||||
'---',
|
||||
'',
|
||||
'## Resultado Final',
|
||||
'',
|
||||
lastStep.result,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
lines.push('---', '', `_Relatório gerado automaticamente em ${formatDate(new Date().toISOString())}_`);
|
||||
|
||||
writeFileSync(filepath, lines.join('\n'), 'utf-8');
|
||||
return { filename, filepath };
|
||||
}
|
||||
@@ -4,12 +4,18 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import * as manager from '../agents/manager.js';
|
||||
import { tasksStore, settingsStore, executionsStore, webhooksStore } from '../store/db.js';
|
||||
import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore } from '../store/db.js';
|
||||
import * as scheduler from '../agents/scheduler.js';
|
||||
import * as pipeline from '../agents/pipeline.js';
|
||||
import { getBinPath, updateMaxConcurrent } from '../agents/executor.js';
|
||||
import { invalidateAgentMapCache } from '../agents/pipeline.js';
|
||||
import { cached } from '../cache/index.js';
|
||||
import { readdirSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __apiDirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPORTS_DIR = join(__apiDirname, '..', '..', 'data', 'reports');
|
||||
|
||||
const router = Router();
|
||||
export const hookRouter = Router();
|
||||
@@ -163,6 +169,25 @@ router.get('/agents/:id/export', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/agents/:id/duplicate', async (req, res) => {
|
||||
try {
|
||||
const agent = manager.getAgentById(req.params.id);
|
||||
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||
const { id, created_at, updated_at, executions, ...rest } = agent;
|
||||
const duplicate = {
|
||||
...rest,
|
||||
agent_name: `${agent.agent_name} (cópia)`,
|
||||
executions: [],
|
||||
status: 'active',
|
||||
};
|
||||
const created = manager.createAgent(duplicate);
|
||||
invalidateAgentMapCache();
|
||||
res.status(201).json(created);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/tasks', (req, res) => {
|
||||
try {
|
||||
res.json(tasksStore.getAll());
|
||||
@@ -384,22 +409,34 @@ router.post('/webhooks', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/webhooks/:id', (req, res) => {
|
||||
router.put('/webhooks/:id', async (req, res) => {
|
||||
try {
|
||||
const existing = webhooksStore.getById(req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
|
||||
const updateData = {};
|
||||
if (req.body.name !== undefined) updateData.name = req.body.name;
|
||||
if (req.body.active !== undefined) updateData.active = !!req.body.active;
|
||||
|
||||
const updated = webhooksStore.update(req.params.id, updateData);
|
||||
res.json(updated);
|
||||
const webhooks = webhooksStore.getAll();
|
||||
const idx = webhooks.findIndex(w => w.id === req.params.id);
|
||||
if (idx === -1) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
const allowed = ['name', 'targetType', 'targetId', 'active'];
|
||||
for (const key of allowed) {
|
||||
if (req.body[key] !== undefined) webhooks[idx][key] = req.body[key];
|
||||
}
|
||||
webhooks[idx].updated_at = new Date().toISOString();
|
||||
webhooksStore.save(webhooks);
|
||||
res.json(webhooks[idx]);
|
||||
} catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/webhooks/:id/test', async (req, res) => {
|
||||
try {
|
||||
const webhooks = webhooksStore.getAll();
|
||||
const wh = webhooks.find(w => w.id === req.params.id);
|
||||
if (!wh) return res.status(404).json({ error: 'Webhook não encontrado' });
|
||||
res.json({ success: true, message: 'Webhook testado com sucesso', webhook: { id: wh.id, name: wh.name, targetType: wh.targetType } });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/webhooks/:id', (req, res) => {
|
||||
try {
|
||||
const deleted = webhooksStore.delete(req.params.id);
|
||||
@@ -652,4 +689,169 @@ router.get('/executions/recent', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/executions/:id/retry', async (req, res) => {
|
||||
try {
|
||||
const execution = executionsStore.getById(req.params.id);
|
||||
if (!execution) return res.status(404).json({ error: 'Execução não encontrada' });
|
||||
if (!['error', 'canceled'].includes(execution.status)) {
|
||||
return res.status(400).json({ error: 'Apenas execuções com erro ou canceladas podem ser reexecutadas' });
|
||||
}
|
||||
const clientId = req.headers['x-client-id'] || null;
|
||||
if (execution.type === 'pipeline') {
|
||||
pipeline.executePipeline(execution.pipelineId, execution.input, (msg) => wsCallback(msg, clientId)).catch(() => {});
|
||||
return res.json({ success: true, message: 'Pipeline reexecutado' });
|
||||
}
|
||||
manager.executeTask(execution.agentId, execution.task, null, (msg) => wsCallback(msg, clientId));
|
||||
res.json({ success: true, message: 'Execução reiniciada' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/executions/export', async (req, res) => {
|
||||
try {
|
||||
const executions = executionsStore.getAll();
|
||||
const headers = ['ID', 'Tipo', 'Nome', 'Status', 'Início', 'Fim', 'Duração (ms)', 'Custo (USD)', 'Turnos'];
|
||||
const rows = executions.map(e => [
|
||||
e.id,
|
||||
e.type || 'agent',
|
||||
e.agentName || e.pipelineName || '',
|
||||
e.status,
|
||||
e.startedAt || '',
|
||||
e.endedAt || '',
|
||||
e.durationMs || '',
|
||||
e.costUsd || e.totalCostUsd || '',
|
||||
e.numTurns || '',
|
||||
]);
|
||||
const csv = [headers.join(','), ...rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(','))].join('\n');
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=executions_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
res.send('\uFEFF' + csv);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stats/charts', async (req, res) => {
|
||||
try {
|
||||
const days = parseInt(req.query.days) || 7;
|
||||
const executions = executionsStore.getAll();
|
||||
const now = new Date();
|
||||
const labels = [];
|
||||
const executionCounts = [];
|
||||
const costData = [];
|
||||
const successCounts = [];
|
||||
const errorCounts = [];
|
||||
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
labels.push(dateStr);
|
||||
const dayExecs = executions.filter(e => e.startedAt && e.startedAt.startsWith(dateStr));
|
||||
executionCounts.push(dayExecs.length);
|
||||
costData.push(+(dayExecs.reduce((sum, e) => sum + (e.costUsd || e.totalCostUsd || 0), 0)).toFixed(4));
|
||||
successCounts.push(dayExecs.filter(e => e.status === 'completed').length);
|
||||
errorCounts.push(dayExecs.filter(e => e.status === 'error').length);
|
||||
}
|
||||
|
||||
const agentCounts = {};
|
||||
executions.forEach(e => {
|
||||
if (e.agentName) agentCounts[e.agentName] = (agentCounts[e.agentName] || 0) + 1;
|
||||
});
|
||||
const topAgents = Object.entries(agentCounts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([name, count]) => ({ name, count }));
|
||||
|
||||
const statusDist = {};
|
||||
executions.forEach(e => { statusDist[e.status] = (statusDist[e.status] || 0) + 1; });
|
||||
|
||||
res.json({ labels, executionCounts, costData, successCounts, errorCounts, topAgents, statusDistribution: statusDist });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/notifications', async (req, res) => {
|
||||
try {
|
||||
const notifications = notificationsStore.getAll();
|
||||
const unreadCount = notifications.filter(n => !n.read).length;
|
||||
res.json({ notifications: notifications.slice(-50).reverse(), unreadCount });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/notifications/:id/read', async (req, res) => {
|
||||
try {
|
||||
const notifications = notificationsStore.getAll();
|
||||
const n = notifications.find(n => n.id === req.params.id);
|
||||
if (!n) return res.status(404).json({ error: 'Notificação não encontrada' });
|
||||
n.read = true;
|
||||
notificationsStore.save(notifications);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/notifications/read-all', async (req, res) => {
|
||||
try {
|
||||
const notifications = notificationsStore.getAll();
|
||||
notifications.forEach(n => n.read = true);
|
||||
notificationsStore.save(notifications);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/notifications', async (req, res) => {
|
||||
try {
|
||||
notificationsStore.save([]);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/reports', (req, res) => {
|
||||
try {
|
||||
if (!existsSync(REPORTS_DIR)) return res.json([]);
|
||||
const files = readdirSync(REPORTS_DIR)
|
||||
.filter(f => f.endsWith('.md'))
|
||||
.sort((a, b) => b.localeCompare(a))
|
||||
.slice(0, 100);
|
||||
res.json(files);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/reports/:filename', (req, res) => {
|
||||
try {
|
||||
const filename = req.params.filename.replace(/[^a-zA-Z0-9À-ÿ_.\-]/g, '');
|
||||
if (!filename.endsWith('.md')) return res.status(400).json({ error: 'Formato inválido' });
|
||||
const filepath = join(REPORTS_DIR, filename);
|
||||
if (!existsSync(filepath)) return res.status(404).json({ error: 'Relatório não encontrado' });
|
||||
const content = readFileSync(filepath, 'utf-8');
|
||||
res.json({ filename, content });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/reports/:filename', (req, res) => {
|
||||
try {
|
||||
const filename = req.params.filename.replace(/[^a-zA-Z0-9À-ÿ_.\-]/g, '');
|
||||
const filepath = join(REPORTS_DIR, filename);
|
||||
if (!existsSync(filepath)) return res.status(404).json({ error: 'Relatório não encontrado' });
|
||||
unlinkSync(filepath);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -30,7 +30,9 @@ function readJson(path, fallback) {
|
||||
|
||||
function writeJson(path, data) {
|
||||
ensureDir();
|
||||
writeFileSync(path, JSON.stringify(data, null, 2), 'utf8');
|
||||
const tmpPath = path + '.tmp';
|
||||
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');
|
||||
renameSync(tmpPath, path);
|
||||
}
|
||||
|
||||
function clone(v) {
|
||||
@@ -198,4 +200,5 @@ export const webhooksStore = createStore(`${DATA_DIR}/webhooks.json`);
|
||||
export const settingsStore = createSettingsStore(`${DATA_DIR}/settings.json`);
|
||||
export const secretsStore = createStore(`${DATA_DIR}/secrets.json`);
|
||||
export const notificationsStore = createStore(`${DATA_DIR}/notifications.json`);
|
||||
notificationsStore.setMaxSize(200);
|
||||
export const agentVersionsStore = createStore(`${DATA_DIR}/agent_versions.json`);
|
||||
|
||||
Reference in New Issue
Block a user