Files
DashDescomplicar/api/services/n8n.ts
T
ealmeida 12f688ff7c feat: adicionar 5 novos painéis ao dashboard (MCPs, n8n, Paperclip, IA, Operações)
Expansão do dashboard de 3 para 8 páginas com dados reais do stack:
- MCPs: monitorização de 33 MCPs no gateway com ping e estado online/offline
- n8n: 14 workflows com último run, duração e falhas 24h
- Paperclip: 16 agentes operacionais, routines e issues (PostgreSQL)
- IA/Claude: visão das 3 camadas (189 skills, 72 agents, 39 MCPs, CARL)
- Operações: tickets Desk CRM por departamento + cobertura PROCs

16 ficheiros novos (3042 linhas), 3 existentes editados.
Nova dependência: pg (PostgreSQL client para Paperclip).
Audit: 0 vulnerabilidades (npm audit fix aplicado).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:58:48 +01:00

183 lines
5.0 KiB
TypeScript

/**
* n8n Workflows Service
* Consulta a API REST do n8n e agrega dados de workflows e execuções.
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
// --- Tipos ---
interface N8nWorkflowRaw {
id: string
name: string
active: boolean
tags?: { id: string; name: string }[]
}
interface N8nExecutionRaw {
id: string
workflowId: string
status: 'success' | 'error' | 'running' | 'canceled' | 'waiting'
startedAt: string | null
stoppedAt: string | null
}
export interface N8nWorkflow {
id: string
name: string
active: boolean
last_execution: {
status: 'success' | 'error' | 'running' | null
started_at: string | null
finished_at: string | null
duration_ms: number | null
} | null
tags: string[]
}
export interface N8nDashboard {
total: number
active: number
inactive: number
failed_24h: number
workflows: N8nWorkflow[]
last_updated: string
}
// --- Cache em memória ---
interface CacheEntry {
data: N8nDashboard
timestamp: number
}
const CACHE_TTL_MS = 300 * 1000 // 300 segundos
let cache: CacheEntry | null = null
// --- Helpers ---
function buildHeaders(): Record<string, string> {
const apiKey = process.env.N8N_API_KEY
if (!apiKey) {
throw new Error(
'N8N_API_KEY não está configurada. Defina a variável de ambiente N8N_API_KEY com a chave de API do n8n.'
)
}
return {
'X-N8N-API-KEY': apiKey,
'Content-Type': 'application/json',
}
}
function getBaseUrl(): string {
return process.env.N8N_API_URL || 'https://automator.descomplicar.pt/api/v1'
}
async function fetchJson<T>(url: string, headers: Record<string, string>): Promise<T> {
const response = await fetch(url, { headers })
if (!response.ok) {
throw new Error(`n8n API respondeu com ${response.status} ${response.statusText} para ${url}`)
}
return response.json() as Promise<T>
}
function normaliseStatus(
raw: N8nExecutionRaw['status']
): 'success' | 'error' | 'running' | null {
if (raw === 'success') return 'success'
if (raw === 'error' || raw === 'canceled') return 'error'
if (raw === 'running' || raw === 'waiting') return 'running'
return null
}
function calcDurationMs(started: string | null, stopped: string | null): number | null {
if (!started || !stopped) return null
const diff = new Date(stopped).getTime() - new Date(started).getTime()
return isNaN(diff) ? null : diff
}
// --- Lógica principal ---
export async function getN8nDashboard(): Promise<N8nDashboard> {
// Servir do cache se ainda válido
if (cache && Date.now() - cache.timestamp < CACHE_TTL_MS) {
return cache.data
}
const baseUrl = getBaseUrl()
const headers = buildHeaders() // lança erro se sem API key
// Buscar workflows e execuções em paralelo
const [workflowsResp, executionsResp] = await Promise.all([
fetchJson<{ data: N8nWorkflowRaw[] } | N8nWorkflowRaw[]>(
`${baseUrl}/workflows`,
headers
),
fetchJson<{ data: N8nExecutionRaw[] } | N8nExecutionRaw[]>(
`${baseUrl}/executions?limit=50`,
headers
),
])
// n8n pode retornar array directo ou { data: [...] }
const workflowsRaw: N8nWorkflowRaw[] = Array.isArray(workflowsResp)
? workflowsResp
: (workflowsResp as { data: N8nWorkflowRaw[] }).data ?? []
const executionsRaw: N8nExecutionRaw[] = Array.isArray(executionsResp)
? executionsResp
: (executionsResp as { data: N8nExecutionRaw[] }).data ?? []
// Mapear última execução por workflowId (a mais recente)
const lastExecByWorkflow = new Map<string, N8nExecutionRaw>()
for (const exec of executionsRaw) {
const existing = lastExecByWorkflow.get(exec.workflowId)
if (!existing || new Date(exec.startedAt ?? 0) > new Date(existing.startedAt ?? 0)) {
lastExecByWorkflow.set(exec.workflowId, exec)
}
}
// Calcular falhas nas últimas 24h
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000
let failed_24h = 0
for (const exec of executionsRaw) {
if (
(exec.status === 'error' || exec.status === 'canceled') &&
exec.startedAt &&
new Date(exec.startedAt).getTime() >= cutoff24h
) {
failed_24h++
}
}
// Construir lista de workflows enriquecida
const workflows: N8nWorkflow[] = workflowsRaw.map((wf) => {
const lastExec = lastExecByWorkflow.get(wf.id) ?? null
return {
id: wf.id,
name: wf.name,
active: wf.active,
last_execution: lastExec
? {
status: normaliseStatus(lastExec.status),
started_at: lastExec.startedAt,
finished_at: lastExec.stoppedAt,
duration_ms: calcDurationMs(lastExec.startedAt, lastExec.stoppedAt),
}
: null,
tags: (wf.tags ?? []).map((t) => t.name),
}
})
const dashboard: N8nDashboard = {
total: workflows.length,
active: workflows.filter((w) => w.active).length,
inactive: workflows.filter((w) => !w.active).length,
failed_24h,
workflows,
last_updated: new Date().toISOString(),
}
cache = { data: dashboard, timestamp: Date.now() }
return dashboard
}