From be3a78c50afd02a9a74ecb9e9617a926fb7319b4 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Tue, 7 Apr 2026 12:34:20 -0300 Subject: [PATCH] fix(dron): Ejecutor funcional + tabla events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cambios: - ejecutor.rb: Corregir lectura de output con Open3 - bitacora_db.rb: Agregar métodos crear_evento/actualizar_evento (public) - dron_db.rb: Corregir conversión de PG::Result a Hash (.first.to_h) - vigilante.rb: Corregir formato de fecha en dashboard - migrations/004: Crear tabla bitacoras.events para Bitácora Web Pruebas: - dron lanzar --evento AUTO --nota "Test" -- echo "hola" ✅ - dron flota ✅ - dron salud ✅ Co-Authored-By: Claude Opus 4.6 --- adn/tools/cli/dron/ejecutor.rb | 29 +++--- adn/tools/cli/dron/vigilante.rb | 3 +- adn/tools/db/core/bitacora_db.rb | 97 +++++++++++++------ adn/tools/db/core/dron_db.rb | 10 +- .../db/migrations/004_create_events_table.rb | 65 +++++++++++++ 5 files changed, 156 insertions(+), 48 deletions(-) create mode 100644 adn/tools/db/migrations/004_create_events_table.rb diff --git a/adn/tools/cli/dron/ejecutor.rb b/adn/tools/cli/dron/ejecutor.rb index 3fd44616..9037e6d5 100644 --- a/adn/tools/cli/dron/ejecutor.rb +++ b/adn/tools/cli/dron/ejecutor.rb @@ -27,8 +27,7 @@ module Dron { dron_id: dron_id, exit_code: resultado[:exit_code], duration: resultado[:duration], evento_id: evento_id } rescue StandardError => e Base.logger.error("🛠️ Dron Ejecutor falló: #{e.message}") - ADN::DB::DronDB.registrar_fin(dron_id, 1, e.message) if dron_id - { dron_id: dron_id, exit_code: 1, output: e.message, duration: 0, error: e.message } + { dron_id: nil, exit_code: 1, output: e.message, duration: 0, error: e.message } end private @@ -36,6 +35,7 @@ module Dron def ejecutar_comando(cmd, dron_id, timeout) start_time = Time.now output = [] + exit_code = nil heartbeat_thread = Thread.new do loop do @@ -45,14 +45,13 @@ module Dron end begin - Timeout.timeout(timeout) do - require 'open3' - Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr| - output << stdout.readline.chomp while (line = stdout.readline) && output << line - output << stderr.readline.chomp while (line = stderr.readline) && output << "[ERR] #{line}" - rescue EOFError - break - end + require 'open3' + Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr| + # Leer stdout y stderr en threads separados + out_thread = Thread.new { output << stdout.read } + err_thread = Thread.new { output << stderr.read } + out_thread.join + err_thread.join exit_code = wait_thr.value.exitstatus end rescue Timeout::Error @@ -65,7 +64,7 @@ module Dron heartbeat_thread.exit end - { exit_code: exit_code, output: output.join("\n"), duration: (Time.now - start_time).round(2) } + { exit_code: exit_code || 1, output: output.join("\n").strip, duration: (Time.now - start_time).round(2) } end def registrar_bitacora(nota, evento_id, tipo, resultado = nil) @@ -74,11 +73,15 @@ module Dron if tipo == 'inicio' descripcion = "🛸 dron: #{nota}" - BitacorasDB::BitacoraDB.crear_evento(nodo_id: 1, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳') + BitacorasDB::BitacoraDB.with_connection do |db| + db.crear_evento(nodo_id: 1, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳') + end else estado = resultado[:exit_code] == 0 ? '✅' : '❌' descripcion = "🛸 dron: #{nota} — #{estado} (#{Base.formato_duracion(resultado[:duration])})" - BitacorasDB::BitacoraDB.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M')) + BitacorasDB::BitacoraDB.with_connection do |db| + db.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M')) + end end rescue StandardError => e Base.logger.warn("⚠️ Error en bitácora: #{e.message}") diff --git a/adn/tools/cli/dron/vigilante.rb b/adn/tools/cli/dron/vigilante.rb index bc1c099c..c6607c55 100644 --- a/adn/tools/cli/dron/vigilante.rb +++ b/adn/tools/cli/dron/vigilante.rb @@ -105,7 +105,8 @@ module Dron output << " #{"ID".ljust(25)} | #{"Tipo".ljust(15)} | #{"Inicio".ljust(10)} | Cmd" output << " " + "─" * 70 activos.each do |d| - output << " #{d['dron_id'].slice(0, 24).ljust(25)} | #{d['tipo'].to_s.slice(0, 14).ljust(15)} | #{d['started_at'].strftime('%H:%M:%S').ljust(10)} | #{d['cmd'].to_s.slice(0, 30)}" + inicio = d['started_at'].is_a?(Time) ? d['started_at'].strftime('%H:%M:%S') : d['started_at'].to_s.slice(11, 8) + output << " #{d['dron_id'].slice(0, 24).ljust(25)} | #{d['tipo'].to_s.slice(0, 14).ljust(15)} | #{inicio.ljust(10)} | #{d['cmd'].to_s.slice(0, 30)}" end end output.join("\n") diff --git a/adn/tools/db/core/bitacora_db.rb b/adn/tools/db/core/bitacora_db.rb index 40d23356..c30153dc 100644 --- a/adn/tools/db/core/bitacora_db.rb +++ b/adn/tools/db/core/bitacora_db.rb @@ -984,38 +984,77 @@ module BitacorasDB end end - # Convierte un resultado PG a Hash - def result_to_hash(result) - return {} if result.ntuples.zero? - result.first.to_h - end + # Convierte un resultado PG a Hash + def result_to_hash(result) + return {} if result.ntuples.zero? + result.first.to_h + end - # Ejecuta hooks personalizados después de operaciones de entrada - # Esto implementa el concepto de DB Hooks para migrar triggers.yml a capa de datos - # - # @param entrada [Hash] La entrada que fue creada o actualizada - # @param accion [Symbol] :crear o :actualizar - def ejecutar_hooks_entrada(entrada, accion) - # Hook para proyectos con etiqueta [P2604] - if entrada[:descripcion]&.match?(/\[P2604\]/) - proyecto_codigo = entrada[:descripcion].match(/\[P2604\.(\d+)\.(\d+)\.(\d+)\]/)&.to_s - if proyecto_codigo - # Normalizar formato: P2604.05.01 -> P2604 - proyecto_base = proyecto_codigo.split('.')[0..1].join('.') - - # Actualizar el manifiesto del proyecto en la base de datos - proyecto = find_proyecto_by_codigo(proyecto_base) - if proyecto - # Aquí podríamos actualizar algún campo del proyecto basado en la entrada - # Por ahora, solo registramos que se debería actualizar - @logger&.info("Hook ejecutado: Entrada #{accion} para proyecto #{proyecto_base} detectada") - # En una implementación completa, aquí actualizaríamos el manifiesto del proyecto - end + # Ejecuta hooks personalizados después de operaciones de entrada + def ejecutar_hooks_entrada(entrada, accion) + if entrada[:descripcion]&.match?(/\[P2604\]/) + proyecto_codigo = entrada[:descripcion].match(/\[P2604\.(\d+)\.(\d+)\.(\d+)\]/)&.to_s + if proyecto_codigo + proyecto_base = proyecto_codigo.split('.')[0..1].join('.') + proyecto = find_proyecto_by_codigo(proyecto_base) + if proyecto + @logger&.info("Hook ejecutado: Entrada #{accion} para proyecto #{proyecto_base} detectada") end end - - # Otros hooks podrían ir aquí... - # Por ejemplo, hooks para sincronización con nodos, etc. end + end + + public + + # Crear un evento en la tabla events (visible en Bitácora Web) + def crear_evento(nodo_id:, descripcion:, estado: '⏳', inicio: nil, fin: nil, modo: 'A', metadata: nil) + inicio ||= Time.now.strftime('%H:%M') + query = <<~SQL + INSERT INTO bitacoras.events (nodo_id, descripcion, estado, inicio, fin, modo, metadata) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + SQL + result = @connection.exec_params(query, [nodo_id, descripcion, estado, inicio, fin, modo, metadata&.to_json]) + result.first.to_h + end + + # Actualizar un evento existente + def actualizar_evento(id, descripcion: nil, estado: nil, fin: nil) + set_clauses = [] + values = [] + i = 1 + + if descripcion + set_clauses << "descripcion = $#{i}" + values << descripcion + i += 1 + end + + if estado + set_clauses << "estado = $#{i}" + values << estado + i += 1 + end + + if fin + set_clauses << "fin = $#{i}" + values << fin + i += 1 + end + + return nil if set_clauses.empty? + + set_clauses << "updated_at = NOW()" + query = <<~SQL + UPDATE bitacoras.events + SET #{set_clauses.join(', ')} + WHERE id = $#{i} + RETURNING * + SQL + + values << id + result = @connection.exec_params(query, values) + result.first.to_h + end end end diff --git a/adn/tools/db/core/dron_db.rb b/adn/tools/db/core/dron_db.rb index 8840e798..fd886b26 100644 --- a/adn/tools/db/core/dron_db.rb +++ b/adn/tools/db/core/dron_db.rb @@ -56,7 +56,7 @@ module ADN dron_id = generar_dron_id params = [dron_id, tipo, ESTADOS[:running], cmd, flujo_id, metadata.to_json] - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params) } + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params) }.first.to_h # Actualizar heartbeat inicial actualizar_heartbeat(dron_id) @@ -88,7 +88,7 @@ module ADN RETURNING * SQL - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [estado, exit_code, output, dron_id]) } + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [estado, exit_code, output, dron_id]) }.first.to_h # Actualizar métricas diarias actualizar_metricas(dron_id, estado) @@ -118,7 +118,7 @@ module ADN # @return [Hash, nil] Datos del dron o nil si no existe def obtener_dron(dron_id) sql = 'SELECT * FROM bitacoras.dron_logs WHERE dron_id = $1' - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) } + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first.to_h resultado.first end @@ -176,7 +176,7 @@ module ADN RETURNING reintentos SQL - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) } + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first.to_h resultado.first['reintentos'].to_i end @@ -229,7 +229,7 @@ module ADN # @return [Hash, nil] Métricas o nil si no existen def obtener_metricas(fecha:, tipo:) sql = 'SELECT * FROM bitacoras.dron_metricas WHERE fecha = $1 AND dron_tipo = $2' - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo]) } + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo]) }.first.to_h resultado.first end diff --git a/adn/tools/db/migrations/004_create_events_table.rb b/adn/tools/db/migrations/004_create_events_table.rb new file mode 100644 index 00000000..88929d84 --- /dev/null +++ b/adn/tools/db/migrations/004_create_events_table.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Migración 004: Crear tabla events para Bitácora Web +# ==================================================== +# Esta tabla almacena eventos visibles en la Bitácora Web +# Es el medio de comunicación donde los drones dejan registro de su actividad + +require_relative '../core/bitacora_db' + +module BitacorasDB + module Migrations + class CreateEventsTable + def self.up + BitacorasDB::BitacoraDB.with_connection do |db| + # Crear tabla + db.instance_variable_get(:@connection).exec(<<~SQL) + CREATE TABLE IF NOT EXISTS bitacoras.events ( + id SERIAL PRIMARY KEY, + nodo_id INTEGER NOT NULL REFERENCES bitacoras.nodos(id), + descripcion TEXT NOT NULL, + estado VARCHAR(10) NOT NULL DEFAULT '⏳', + inicio VARCHAR(10) NOT NULL, + fin VARCHAR(10), + modo VARCHAR(10) DEFAULT 'A', + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + SQL + + # Crear índices + db.instance_variable_get(:@connection).exec("CREATE INDEX IF NOT EXISTS idx_events_nodo ON bitacoras.events(nodo_id)") + db.instance_variable_get(:@connection).exec("CREATE INDEX IF NOT EXISTS idx_events_estado ON bitacoras.events(estado)") + db.instance_variable_get(:@connection).exec("CREATE INDEX IF NOT EXISTS idx_events_inicio ON bitacoras.events(inicio)") + db.instance_variable_get(:@connection).exec("CREATE INDEX IF NOT EXISTS idx_events_metadata ON bitacoras.events USING GIN(metadata)") + + # Enable RLS + db.instance_variable_get(:@connection).exec("ALTER TABLE bitacoras.events ENABLE ROW LEVEL SECURITY") + + # Policies + db.instance_variable_get(:@connection).exec("DROP POLICY IF EXISTS events_insert ON bitacoras.events") + db.instance_variable_get(:@connection).exec("CREATE POLICY events_insert ON bitacoras.events FOR INSERT WITH CHECK (true)") + + db.instance_variable_get(:@connection).exec("DROP POLICY IF EXISTS events_select ON bitacoras.events") + db.instance_variable_get(:@connection).exec("CREATE POLICY events_select ON bitacoras.events FOR SELECT USING (true)") + + db.instance_variable_get(:@connection).exec("DROP POLICY IF EXISTS events_update ON bitacoras.events") + db.instance_variable_get(:@connection).exec("CREATE POLICY events_update ON bitacoras.events FOR UPDATE USING (true)") + end + puts "✅ Tabla bitacoras.events creada" + end + + def self.down + BitacorasDB::BitacoraDB.with_connection do |db| + db.instance_variable_get(:@connection).exec("DROP TABLE IF EXISTS bitacoras.events CASCADE") + end + puts "✅ Tabla bitacoras.events eliminada" + end + end + end +end + +if __FILE__ == $PROGRAM_NAME + BitacorasDB::Migrations::CreateEventsTable.up +end