/** * 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 { 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(url: string, headers: Record): Promise { 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 } 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 { // 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() 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 }