From 2e76496ff44662a25d43f8a8bcb111798fd4f2ae Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Thu, 2 Apr 2026 16:47:28 -0300 Subject: [PATCH] =?UTF-8?q?feat(kernel):=20integra=C3=A7=C3=A3o=20Engine?= =?UTF-8?q?=20Room=20com=20Arcadia=20Kernel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MIGRAÇÃO COMPLETA: Sistema antigo → Kernel Nativo (porta 5001) 📁 Arquivos alterados: - server/engine-room/kernel-adapter.ts (NOVO) - server/engine-room/routes.ts - server/kernel/config/services.json 🔧 O que mudou no Kernel (services.json): + Adicionado: python-fisco (porta 8002) + Corrigido: node-communication (8006, era 9001) + Adicionado: node-core-api (9002) + Adicionado: node-worker (9003) - Removido: java-metaset (gerenciado externamente) Total: 6 → 7 serviços gerenciados pelo Kernel 🔄 Engine Room - Novo comportamento: - Todas as rotas /api/engine-room/* agora usam Kernel Adapter - Sistema antigo (managedServices) mantido em comentário para fallback - Verificação automática se Kernel está disponível - Retorna erro 503 se Kernel não responde na porta 5001 📊 Mapeamento de serviços: Engine Room → Kernel ID ──────────────────────────────────────── contabil → python-contabil bi-engine → python-bi automation-engine → python-automation fisco → python-fisco communication → node-communication 🚫 Serviços externos (não gerenciados pelo Kernel): - plus (PHP/Laravel, porta 8080) - metaset (Java, porta 8088) ✨ Benefícios: - Auto-restart configurável por serviço - Health checks automáticos - Logs centralizados com timestamps - WebSocket para atualizações em tempo real - Dashboard na porta 5001 ⚠️ Pré-requisito: KERNEL_ENABLED=true npm run dev Refs: Semana 4 - Integração Kernel com Engine Room Breaking Change: Engine Room requer Kernel rodando --- server/engine-room/kernel-adapter.ts | 268 +++++++++++++++++++++++++++ server/engine-room/routes.ts | 134 ++++++++++---- server/kernel/config/services.json | 28 ++- 3 files changed, 388 insertions(+), 42 deletions(-) create mode 100644 server/engine-room/kernel-adapter.ts diff --git a/server/engine-room/kernel-adapter.ts b/server/engine-room/kernel-adapter.ts new file mode 100644 index 0000000..df46070 --- /dev/null +++ b/server/engine-room/kernel-adapter.ts @@ -0,0 +1,268 @@ +/** + * Kernel Adapter - Ponte entre Engine Room e Arcadia Kernel + * Traduz chamadas do formato antigo para o novo Kernel (porta 5001) + */ + +const KERNEL_BASE_URL = 'http://localhost:5001/api/kernel'; + +// Mapeamento: Nome do Engine Room -> ID do Kernel +// NOTA: metaset NÃO está aqui - é gerenciado externamente (Java) +const ENGINE_TO_KERNEL_ID: Record = { + "contabil": "python-contabil", + "bi-engine": "python-bi", + "automation-engine": "python-automation", + "fisco": "python-fisco", + "communication": "node-communication", +}; + +// Mapeamento inverso +const KERNEL_TO_ENGINE_NAME: Record = { + "python-contabil": "contabil", + "python-bi": "bi-engine", + "python-automation": "automation-engine", + "python-fisco": "fisco", + "node-communication": "communication", +}; + +// Mapeamento de categorias +const CATEGORY_MAP: Record = { + "python-contabil": "fiscal", + "python-bi": "data", + "python-automation": "automation", + "python-fisco": "fiscal", + "node-communication": "intelligence", +}; + +// Mapeamento de display names +const DISPLAY_NAME_MAP: Record = { + "python-contabil": "Motor Contabil", + "python-bi": "Motor BI", + "python-automation": "Motor Automacao", + "python-fisco": "Motor Fiscal", + "node-communication": "Motor Comunicacao", +}; + +interface KernelServiceState { + config: { + id: string; + name: string; + type: string; + port: number; + autoStart: boolean; + maxRestarts: number; + }; + status: 'running' | 'stopped' | 'error' | 'restarting'; + pid?: number; + startTime?: string; + restartCount: number; + health?: 'healthy' | 'unhealthy' | 'unknown'; +} + +interface EngineStatus { + name: string; + displayName: string; + type: string; + port: number; + category: string; + description: string; + status: "online" | "offline" | "error"; + responseTime?: number; + details?: any; + error?: string; +} + +// Helper para fazer requests ao Kernel +async function kernelRequest(endpoint: string, method = 'GET', body?: any): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); + + const options: RequestInit = { + method, + headers: { + 'Content-Type': 'application/json', + }, + signal: controller.signal, + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(`${KERNEL_BASE_URL}${endpoint}`, options); + clearTimeout(timeout); + + if (!response.ok) { + throw new Error(`Kernel responded with ${response.status}`); + } + + return await response.json(); + } catch (error: any) { + if (error.name === 'AbortError') { + throw new Error('Kernel request timeout'); + } + throw error; + } +} + +// Converter status do Kernel para formato do Engine Room +function mapStatus(kernelStatus: string, health?: string): "online" | "offline" | "error" { + if (kernelStatus === 'running') { + return health === 'unhealthy' ? 'error' : 'online'; + } + if (kernelStatus === 'error') return 'error'; + return 'offline'; +} + +// Obter todos os serviços do Kernel formatados para Engine Room +export async function getKernelEngines(): Promise { + try { + const response = await kernelRequest('/services'); + const services: KernelServiceState[] = response.data || []; + + return services.map((service): EngineStatus => { + const engineName = KERNEL_TO_ENGINE_NAME[service.config.id] || service.config.id; + + return { + name: engineName, + displayName: DISPLAY_NAME_MAP[service.config.id] || service.config.name, + type: service.config.type === 'python' ? 'python' : + service.config.type === 'java' ? 'java' : 'node', + port: service.config.port, + category: CATEGORY_MAP[service.config.id] || 'data', + description: service.config.name, + status: mapStatus(service.status, service.health), + details: { + pid: service.pid, + restartCount: service.restartCount, + startTime: service.startTime, + health: service.health, + }, + }; + }); + } catch (error) { + console.error('[KernelAdapter] Error fetching engines:', error); + return []; + } +} + +// Obter info de um serviço específico (formato compatível com antigo) +export async function getKernelServiceInfo(engineName: string): Promise { + const kernelId = ENGINE_TO_KERNEL_ID[engineName]; + if (!kernelId) return null; + + try { + const response = await kernelRequest(`/services/${kernelId}`); + const service: KernelServiceState = response.data; + + if (!service) return null; + + return { + name: engineName, + port: service.config.port, + status: service.status, + startedAt: service.startTime || null, + restartCount: service.restartCount, + uptime: service.startTime + ? Math.round((Date.now() - new Date(service.startTime).getTime()) / 1000) + : 0, + logLines: 0, // Kernel não expõe isso diretamente + health: service.health, + }; + } catch (error) { + console.error(`[KernelAdapter] Error getting info for ${engineName}:`, error); + return null; + } +} + +// Obter logs de um serviço (formato compatível) +export async function getKernelServiceLogs(engineName: string, lines = 50): Promise { + const kernelId = ENGINE_TO_KERNEL_ID[engineName]; + if (!kernelId) return []; + + try { + const response = await kernelRequest(`/services/${kernelId}/logs?lines=${lines}`); + const logs = response.data || []; + + // Converter formato do Kernel (objetos) para formato antigo (strings) + return logs.map((log: any) => { + const time = new Date(log.timestamp).toISOString().split('T')[1].slice(0, 8); + return `[${time}] [${log.level.toUpperCase()}] ${log.message}`; + }); + } catch (error) { + console.error(`[KernelAdapter] Error getting logs for ${engineName}:`, error); + return []; + } +} + +// Iniciar serviço +export async function startKernelService(engineName: string): Promise { + const kernelId = ENGINE_TO_KERNEL_ID[engineName]; + if (!kernelId) return false; + + try { + await kernelRequest(`/services/${kernelId}/start`, 'POST'); + return true; + } catch (error) { + console.error(`[KernelAdapter] Error starting ${engineName}:`, error); + return false; + } +} + +// Parar serviço +export async function stopKernelService(engineName: string): Promise { + const kernelId = ENGINE_TO_KERNEL_ID[engineName]; + if (!kernelId) return false; + + try { + await kernelRequest(`/services/${kernelId}/stop`, 'POST'); + return true; + } catch (error) { + console.error(`[KernelAdapter] Error stopping ${engineName}:`, error); + return false; + } +} + +// Reiniciar serviço +export async function restartKernelService(engineName: string): Promise { + const kernelId = ENGINE_TO_KERNEL_ID[engineName]; + if (!kernelId) return false; + + try { + await kernelRequest(`/services/${kernelId}/restart`, 'POST'); + return true; + } catch (error) { + console.error(`[KernelAdapter] Error restarting ${engineName}:`, error); + return false; + } +} + +// Verificar health geral do Kernel +export async function getKernelHealth(): Promise { + try { + const response = await kernelRequest('/health'); + return response.data; + } catch (error) { + console.error('[KernelAdapter] Error getting health:', error); + return null; + } +} + +// Verificar se Kernel está disponível +export async function isKernelAvailable(): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + + const response = await fetch('http://localhost:5001/health', { + signal: controller.signal, + }); + + clearTimeout(timeout); + return response.ok; + } catch { + return false; + } +} + +console.log('[KernelAdapter] Adapter loaded - proxying to localhost:5001'); diff --git a/server/engine-room/routes.ts b/server/engine-room/routes.ts index 54b45dc..2abf751 100644 --- a/server/engine-room/routes.ts +++ b/server/engine-room/routes.ts @@ -1,5 +1,17 @@ import type { Express, Request, Response } from "express"; -import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index"; +import { + getKernelEngines, + getKernelServiceInfo, + getKernelServiceLogs, + startKernelService, + stopKernelService, + restartKernelService, + getKernelHealth, + isKernelAvailable +} from "./kernel-adapter"; + +// Fallback para sistema antigo (import comentado - remover após validação) +// import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index"; interface EngineConfig { name: string; @@ -120,7 +132,45 @@ export function registerEngineRoomRoutes(app: Express): void { return res.status(401).json({ error: "Not authenticated" }); } - const results = await Promise.all(ENGINES.map(checkEngineHealth)); + // Verificar se Kernel está disponível + const kernelAvailable = await isKernelAvailable(); + if (!kernelAvailable) { + return res.status(503).json({ + error: "Kernel não disponível", + message: "O Arcadia Kernel (porta 5001) não está respondendo. Verifique se está rodando com KERNEL_ENABLED=true" + }); + } + + // Obter engines do Kernel + const kernelEngines = await getKernelEngines(); + + // Adicionar Plus (gerenciado externamente - PHP/Laravel) + const plusEngine = ENGINES.find(e => e.name === "plus"); + if (plusEngine) { + const plusHealth = await checkEngineHealth(plusEngine); + kernelEngines.push({ + ...plusHealth, + name: "plus", + displayName: "Arcadia Plus (ERP)", + type: "php", + category: "erp", + description: "ERP completo Laravel", + } as any); + } + + // Adicionar MetaSet (gerenciado externamente - Java) + const metasetEngine = ENGINES.find(e => e.name === "metaset"); + if (metasetEngine) { + const metasetHealth = await checkEngineHealth(metasetEngine); + kernelEngines.push({ + ...metasetHealth, + name: "metaset", + displayName: "MetaSet (Motor BI)", + type: "java", + category: "data", + description: "Motor de BI - Consultas, Dashboards, Gráficos", + } as any); + } let agentsStatus: any[] = []; try { @@ -130,11 +180,11 @@ export function registerEngineRoomRoutes(app: Express): void { agentsStatus = []; } - const online = results.filter((r) => r.status === "online").length; - const total = results.length; + const online = kernelEngines.filter((r) => r.status === "online").length; + const total = kernelEngines.length; res.json({ - engines: results, + engines: kernelEngines, agents: agentsStatus, summary: { total_engines: total, @@ -239,13 +289,6 @@ export function registerEngineRoomRoutes(app: Express): void { } }); - const ENGINE_NAME_MAP: Record = { - "contabil": "contabil", - "fisco": "fisco", - "bi-engine": "bi", - "automation-engine": "automation", - "metaset": "metaset", - }; app.post("/api/engine-room/engine/:name/restart", async (req: Request, res: Response) => { try { @@ -254,17 +297,21 @@ export function registerEngineRoomRoutes(app: Express): void { } const engineName = req.params.name; - const serviceName = ENGINE_NAME_MAP[engineName]; if (engineName === "plus") { return res.status(400).json({ error: "Plus (Laravel) nao pode ser reiniciado por aqui" }); } - - if (!serviceName) { - return res.status(404).json({ error: "Motor nao encontrado" }); + if (engineName === "metaset") { + return res.status(400).json({ error: "MetaSet (Java) nao pode ser reiniciado por aqui" }); } - const restarted = restartManagedService(serviceName); + // Verificar se Kernel está disponível + const kernelAvailable = await isKernelAvailable(); + if (!kernelAvailable) { + return res.status(503).json({ error: "Kernel não disponível" }); + } + + const restarted = await restartKernelService(engineName); if (restarted) { res.json({ success: true, message: `Motor ${engineName} reiniciando...` }); } else { @@ -282,17 +329,21 @@ export function registerEngineRoomRoutes(app: Express): void { } const engineName = req.params.name; - const serviceName = ENGINE_NAME_MAP[engineName]; if (engineName === "plus") { return res.status(400).json({ error: "Plus (Laravel) nao pode ser parado por aqui" }); } - - if (!serviceName) { - return res.status(404).json({ error: "Motor nao encontrado" }); + if (engineName === "metaset") { + return res.status(400).json({ error: "MetaSet (Java) nao pode ser parado por aqui" }); } - const stopped = stopManagedService(serviceName); + // Verificar se Kernel está disponível + const kernelAvailable = await isKernelAvailable(); + if (!kernelAvailable) { + return res.status(503).json({ error: "Kernel não disponível" }); + } + + const stopped = await stopKernelService(engineName); if (stopped) { res.json({ success: true, message: `Motor ${engineName} parado` }); } else { @@ -310,18 +361,22 @@ export function registerEngineRoomRoutes(app: Express): void { } const engineName = req.params.name; - const serviceName = ENGINE_NAME_MAP[engineName]; if (engineName === "plus") { return res.status(400).json({ error: "Plus (Laravel) nao pode ser iniciado por aqui" }); } - - if (!serviceName) { - return res.status(404).json({ error: "Motor nao encontrado" }); + if (engineName === "metaset") { + return res.status(400).json({ error: "MetaSet (Java) nao pode ser iniciado por aqui" }); } - const restarted = restartManagedService(serviceName); - if (restarted) { + // Verificar se Kernel está disponível + const kernelAvailable = await isKernelAvailable(); + if (!kernelAvailable) { + return res.status(503).json({ error: "Kernel não disponível" }); + } + + const started = await startKernelService(engineName); + if (started) { res.json({ success: true, message: `Motor ${engineName} iniciando...` }); } else { res.status(500).json({ error: `Falha ao iniciar motor ${engineName}` }); @@ -338,16 +393,15 @@ export function registerEngineRoomRoutes(app: Express): void { } const engineName = req.params.name; - const serviceName = ENGINE_NAME_MAP[engineName]; - if (!serviceName) { - if (engineName === "plus") { - return res.json({ name: "plus", port: 8080, status: "managed-externally", message: "Plus e gerenciado separadamente" }); - } - return res.status(404).json({ error: "Motor nao encontrado" }); + if (engineName === "plus") { + return res.json({ name: "plus", port: 8080, status: "managed-externally", message: "Plus e gerenciado separadamente" }); + } + if (engineName === "metaset") { + return res.json({ name: "metaset", port: 8088, status: "managed-externally", message: "MetaSet e gerenciado separadamente (Java)" }); } - const info = getManagedServiceInfo(serviceName); + const info = await getKernelServiceInfo(engineName); if (!info) { return res.status(404).json({ error: "Servico nao encontrado no gerenciador" }); } @@ -365,14 +419,14 @@ export function registerEngineRoomRoutes(app: Express): void { } const engineName = req.params.name; - const serviceName = ENGINE_NAME_MAP[engineName]; - if (!serviceName) { - return res.status(404).json({ error: "Motor nao encontrado" }); + // Serviços externos não têm logs via Kernel + if (engineName === "plus" || engineName === "metaset") { + return res.json({ engine: engineName, lines: 0, logs: [], message: "Logs nao disponiveis para servicos gerenciados externamente" }); } const lines = parseInt(req.query.lines as string) || 50; - const logs = getManagedServiceLogs(serviceName, lines); + const logs = await getKernelServiceLogs(engineName, lines); res.json({ engine: engineName, lines: logs.length, logs }); } catch (error: any) { @@ -380,5 +434,5 @@ export function registerEngineRoomRoutes(app: Express): void { } }); - console.log("[Engine Room] Rotas registradas em /api/engine-room/*"); + console.log("[Engine Room] Rotas registradas em /api/engine-room/* (usando Kernel Adapter)"); } diff --git a/server/kernel/config/services.json b/server/kernel/config/services.json index 2261bba..0885ebd 100644 --- a/server/kernel/config/services.json +++ b/server/kernel/config/services.json @@ -69,6 +69,29 @@ "maxRestarts": 5, "autoStart": true }, + { + "id": "python-fisco", + "name": "Motor Fiscal", + "description": "NF-e/NFC-e - NCMs, CFOPs, CESTs, SEFAZ", + "type": "python", + "command": "python3", + "args": ["-m", "uvicorn", "fisco_service:app", "--host", "0.0.0.0", "--port", "8002"], + "cwd": "./server/python", + "port": 8002, + "healthCheck": { + "path": "/health", + "interval": 30000, + "timeout": 5000, + "retries": 3 + }, + "env": { + "PYTHONPATH": ".", + "SERVICE_NAME": "fisco", + "LOG_LEVEL": "info" + }, + "maxRestarts": 5, + "autoStart": false + }, { "id": "node-communication", "name": "Communication Service", @@ -76,7 +99,7 @@ "type": "node", "command": "node", "args": ["dist/services/communication/index.js"], - "port": 9001, + "port": 8006, "healthCheck": { "path": "/health", "interval": 30000, @@ -86,11 +109,12 @@ "env": { "NODE_ENV": "production", "SERVICE_NAME": "communication", - "PORT": "9001" + "PORT": "8006" }, "maxRestarts": 5, "autoStart": false }, + { "id": "node-core-api", "name": "Core API Service",