diff --git a/adn/tools/cli/copiloto.rb b/adn/tools/cli/copiloto.rb index 37dd10dd..9a2b39a0 100644 --- a/adn/tools/cli/copiloto.rb +++ b/adn/tools/cli/copiloto.rb @@ -35,25 +35,31 @@ module ADN end def ejecutar - if @args.empty? || @args.first == 'help' || @args.first == '--help' - mostrar_ayuda - return - end + puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'copiloto' ha sido reemplazado por 'dron lanzar'#{Color::RESET}" + puts "" + puts " El sistema AtomicDrones (Fase 1.5) reemplaza al copiloto con:" + puts " - Heartbeat visible en Bitácora Web cada 30s" + puts " - Timeout real implementado" + puts " - Registro automático en tablas internas + events" + puts "" + puts " #{Color::CYAN}Equivalencias:#{Color::RESET}" + puts " #{Color::DIM}copiloto lanzar --evento ID -- #{Color::RESET}" + puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- #{Color::RESET}" + puts "" + puts " #{Color::DIM}copiloto vigilar --evento ID --pid PID#{Color::RESET}" + puts " #{Color::GREEN}→ dron flota (para ver drones activos)#{Color::RESET}" + puts "" + puts " #{Color::DIM}copiloto estado#{Color::RESET}" + puts " #{Color::GREEN}→ dron flota#{Color::RESET}" + puts "" - accion = @args.shift - case accion - when 'lanzar' - cmd_lanzar(@args) - when 'vigilar' - cmd_vigilar(@args) - when 'estado' - cmd_estado - when 'limpiar' - cmd_limpiar - else - puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}" - mostrar_ayuda - exit 1 + # Si hay argumentos, intentar redirigir automáticamente + if @args.first == 'lanzar' + @args.shift + puts "#{Color::YELLOW}Redirigiendo a 'dron lanzar'...#{Color::RESET}\n\n" + require_relative 'dron' + dron = ADN::SubcomandoDron.new(['lanzar'] + @args) + dron.ejecutar end end diff --git a/adn/tools/cli/dron.rb b/adn/tools/cli/dron.rb index a4d839ad..8ebf88fc 100644 --- a/adn/tools/cli/dron.rb +++ b/adn/tools/cli/dron.rb @@ -27,7 +27,7 @@ module ADN class SubcomandoDron def initialize(args, logger = nil) @args = args - @logger = logger || ADN::Logger.instance + @logger = logger || ADN::Logger.new end def ejecutar diff --git a/adn/tools/cli/dron/dispatcher.rb b/adn/tools/cli/dron/dispatcher.rb index ff304224..1cb89ae8 100644 --- a/adn/tools/cli/dron/dispatcher.rb +++ b/adn/tools/cli/dron/dispatcher.rb @@ -209,9 +209,8 @@ module Dron puts "═" * 60 # Mostrar avances si existen - require_relative '../../db/core/bitacora_db' - sql = 'SELECT * FROM bitacoras.dron_avances WHERE dron_id = $1 ORDER BY paso ASC' - avances = BitacorasDB::BitacoraDB.ejecutar(sql, [dron_id]) + require_relative '../../db/core/dron_db' + avances = ADN::DB::DronDB.obtener_avances(dron_id) unless avances.empty? puts "\n📝 Avances:" diff --git a/adn/tools/cli/dron/ejecutor.rb b/adn/tools/cli/dron/ejecutor.rb index f17c03c0..0ff3c79f 100644 --- a/adn/tools/cli/dron/ejecutor.rb +++ b/adn/tools/cli/dron/ejecutor.rb @@ -3,9 +3,19 @@ # Dron::Ejecutor — Módulo atómico para ejecución de comandos # # Responsabilidad única: Ejecutar un comando y reportar resultado +# +# Fase 1.5: Reescrito para visibilidad en Bitácora Web +# - Heartbeat actualiza events.descripcion (visible en web) +# - Flujo simplificado: crear evento → heartbeat → cierre limpio +# - Timeout implementado con Timeout.timeout() +# - Errores de bitácora logeados explícitamente (sin rescue silencioso) require_relative 'base' require_relative '../../db/core/dron_db' +require 'open3' +require 'timeout' +require 'time' +require 'json' module Dron class Ejecutor @@ -15,44 +25,86 @@ module Dron class << self def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil) Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}") - dron_id = ADN::DB::DronDB.generar_dron_id + inicio = Time.now - ADN::DB::DronDB.registrar_inicio(tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo }) - evento_id = registrar_bitacora(nota, evento_id, 'inicio', nodo) if nota - resultado = ejecutar_comando(cmd, dron_id, timeout) + # 1. Registrar en tablas internas (dron_logs) + resultado_inicio = ADN::DB::DronDB.registrar_inicio( + tipo: 'ejecutor', cmd: cmd, flujo_id: flujo_id, + metadata: { nota: nota, evento_id: evento_id, timeout: timeout, nodo: nodo } + ) + dron_id = resultado_inicio[:dron_id] + + ADN::DB::DronDB.registrar_avance( + dron_id, paso: 1, + descripcion: "Despegue inicial. Ejecutando comando...", + estado: 'running', flujo_id: flujo_id + ) + + # 2. Crear/vincular evento en Bitácora Web (visible) + evento_id = crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo) + + # 3. Ejecutar comando con heartbeat que actualiza AMBAS capas + resultado = ejecutar_comando(cmd, dron_id, timeout, evento_id, nota, inicio) + + # 4. Registrar fin en tablas internas ADN::DB::DronDB.registrar_fin(dron_id, resultado[:exit_code], resultado[:output]) - registrar_bitacora(nota, evento_id, 'fin', resultado, nodo) if evento_id + + estado_fin = resultado[:exit_code] == 0 ? 'completed' : 'failed' + desc_fin = resultado[:exit_code] == 0 ? + "Aterrizaje exitoso. Comando finalizado." : + "Aterrizaje con errores (Code #{resultado[:exit_code]})." + + ADN::DB::DronDB.registrar_avance( + dron_id, paso: 2, + descripcion: desc_fin, + estado: estado_fin, flujo_id: flujo_id + ) + + # 5. Cerrar evento en Bitácora Web (visible) + cerrar_evento_web(evento_id, dron_id, nota, cmd, resultado, inicio) Base.logger.info("🛠️ Dron Ejecutor completado: #{dron_id} - #{resultado[:exit_code] == 0 ? '✅' : '❌'}") { 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}") + Base.logger.error(" #{e.backtrace&.first(3)&.join("\n ")}") { dron_id: nil, exit_code: 1, output: e.message, duration: 0, error: e.message } end private - def ejecutar_comando(cmd, dron_id, timeout) + # ─── Ejecución con heartbeat dual ───────────────────────────── + + def ejecutar_comando(cmd, dron_id, timeout, evento_id, nota, inicio) start_time = Time.now output = [] exit_code = nil heartbeat_thread = Thread.new do + tick = 0 loop do sleep(HEARTBEAT_INTERVAL) + tick += 1 + + # Actualizar tabla interna (dron_logs.heartbeat) ADN::DB::DronDB.actualizar_heartbeat(dron_id) + + # Actualizar Bitácora Web (events.descripcion) — VISIBLE para el usuario + actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick) if evento_id + rescue StandardError => e + Base.logger.warn("⚠️ Error en heartbeat ##{tick}: #{e.message}") end end begin - 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 + Timeout.timeout(timeout) do + Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr| + 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 end rescue Timeout::Error exit_code = 124 @@ -67,59 +119,141 @@ module Dron { 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, nodo = nil) - return nil unless nota - require_relative '../../db/core/bitacora_db' + # ─── Bitácora Web: Crear/Vincular evento ───────────────────── + + def crear_o_vincular_evento_web(dron_id, nota, evento_id, cmd, nodo) + return nil unless nota # Sin nota = sin registro en web - # Detectar nodo automáticamente si no se especifica nodo_id = detectar_nodo_id(nodo) - if tipo == 'inicio' - descripcion = "🛸 dron: #{nota}" - BitacorasDB::BitacoraDB.with_connection do |db| - evento = db.crear_evento(nodo_id: nodo_id, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳') - evento['id'] # Retornar ID para actualizar después + BitacorasDB::BitacoraDB.with_connection do |db| + if evento_id.to_i > 0 + # Vincular a evento existente: agregar nota de despegue + entrada = db.execute("SELECT descripcion FROM bitacoras.entradas WHERE id = $1", [evento_id]).first + target_table = entrada ? 'entradas' : 'events' + target = entrada || db.execute("SELECT descripcion FROM bitacoras.events WHERE id = $1", [evento_id]).first + + if target + nueva_desc = target['descripcion'].to_s + + "\n\n🛸 **[#{Time.now.strftime('%H:%M')}] Vuelo Iniciado** (`#{dron_id}`): #{nota}" + + "\n├─ Comando: `#{cmd}`" + if target_table == 'entradas' + db.update_entrada(evento_id, descripcion: nueva_desc) + else + db.actualizar_evento(evento_id, descripcion: nueva_desc) + end + end + evento_id + else + # Crear evento nuevo en bitácora web + descripcion = "🛸 #{nota} (`#{dron_id}`)\n├─ Comando: `#{cmd}`\n└─ ⏳ Iniciando..." + bitacora_hoy = db.execute("SELECT id FROM bitacoras.bitacoras WHERE fecha::date = CURRENT_DATE LIMIT 1").first + + if bitacora_hoy + entrada = db.create_entrada( + inicio: Time.now.strftime('%H:%M'), + descripcion: descripcion, + estado: '⏳', + modo: 'R', + bitacora_id: bitacora_hoy['id'], + nodo_id: nodo_id + ) + entrada['id'] + else + evento = db.crear_evento( + nodo_id: nodo_id, + descripcion: descripcion, + inicio: Time.now.strftime('%H:%M'), + estado: '⏳', + modo: 'R', + metadata: { dron_id: dron_id } + ) + evento['id'] + end end - else - estado = resultado[:exit_code] == 0 ? '✅' : '❌' - descripcion = "🛸 dron: #{nota} — #{estado} (#{Base.formato_duracion(resultado[:duration])})" - BitacorasDB::BitacoraDB.with_connection do |db| - db.actualizar_evento(evento_id, descripcion: descripcion, fin: Time.now.strftime('%H:%M')) - end - evento_id # Retornar mismo ID end rescue StandardError => e - Base.logger.warn("⚠️ Error en bitácora: #{e.message}") + Base.logger.error("🛸 Error creando evento web: #{e.message}") + Base.logger.error(" #{e.backtrace&.first(3)&.join("\n ")}") nil end + # ─── Bitácora Web: Heartbeat visible ───────────────────────── + + def actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick) + duracion = Base.formato_duracion(Time.now - inicio) + descripcion = "🛸 #{nota} (`#{dron_id}`)\n" \ + "├─ Comando: `#{cmd}`\n" \ + "└─ ❤️ #{duracion} — heartbeat ##{tick}" + + BitacorasDB::BitacoraDB.with_connection do |db| + # Intentar actualizar como entrada primero, luego como evento + entrada = db.execute("SELECT id FROM bitacoras.entradas WHERE id = $1", [evento_id]).first + if entrada + db.update_entrada(evento_id, descripcion: descripcion) + else + db.actualizar_evento(evento_id, descripcion: descripcion) + end + end + rescue StandardError => e + Base.logger.warn("⚠️ Heartbeat web ##{tick} falló: #{e.message}") + end + + # ─── Bitácora Web: Cierre de evento ────────────────────────── + + def cerrar_evento_web(evento_id, dron_id, nota, cmd, resultado, inicio) + return unless evento_id.to_i > 0 + + duracion = Base.formato_duracion(resultado[:duration]) + estado = resultado[:exit_code] == 0 ? '✅' : '❌' + aterrizaje = resultado[:exit_code] == 0 ? 'Aterrizaje limpio' : "Fallo (code #{resultado[:exit_code]})" + + # Construir descripción final con output resumido + output_resumen = Base.slice_safe(resultado[:output], 500) + descripcion = "🛸 #{nota} (`#{dron_id}`) — #{estado} #{duracion}\n" \ + "├─ Comando: `#{cmd}`\n" \ + "└─ #{aterrizaje}" + descripcion += "\n\n```text\n#{output_resumen}\n```" unless output_resumen.empty? + + BitacorasDB::BitacoraDB.with_connection do |db| + entrada = db.execute("SELECT id FROM bitacoras.entradas WHERE id = $1", [evento_id]).first + if entrada + db.update_entrada(evento_id, + descripcion: descripcion, + estado: estado, + fin: Time.now.strftime('%H:%M') + ) + else + db.actualizar_evento(evento_id, + descripcion: descripcion, + estado: estado, + fin: Time.now.strftime('%H:%M') + ) + end + end + Base.logger.info("🛸 Evento ##{evento_id} cerrado (#{estado})") + rescue StandardError => e + Base.logger.error("🛸 Error cerrando evento web ##{evento_id}: #{e.message}") + Base.logger.error(" #{e.backtrace&.first(3)&.join("\n ")}") + end + + # ─── Helpers ───────────────────────────────────────────────── + def detectar_nodo_id(nodo) return nodo if nodo.is_a?(Integer) - # Si no se especifica nodo, usar el hostname para detectar require 'socket' hostname = Socket.gethostname.downcase - # Buscar nodo por nombre en la tabla de nodos - require_relative '../../db/core/bitacora_db' - sql = 'SELECT id FROM nodos WHERE LOWER(nombre) = $1 LIMIT 1' - resultado = nil BitacorasDB::BitacoraDB.with_connection do |db| - resultado = db.execute(sql, [hostname]) + resultado = db.execute('SELECT id FROM nodos WHERE LOWER(nombre) = $1 LIMIT 1', [hostname]) + if resultado.nil? || resultado.ntuples == 0 + resultado = db.execute('SELECT id FROM nodos WHERE LOWER(nombre) LIKE $1 LIMIT 1', ["#{hostname}%"]) + end end - if resultado&.any? - resultado.first['id'] - else - # Fallback: buscar por prefijo (ej: srv-ns8 coincide con srv-ns8.frlr.utn.edu.ar) - sql = 'SELECT id FROM nodos WHERE LOWER(nombre) LIKE $1 LIMIT 1' - resultado = nil - BitacorasDB::BitacoraDB.with_connection do |db| - resultado = db.execute(sql, ["#{hostname}%"]) - end - resultado&.any? ? resultado.first['id'] : 1 - end + (resultado&.any?) ? resultado.first['id'] : 1 rescue StandardError => e Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)") 1 diff --git a/adn/tools/cli/dron/sanador.rb b/adn/tools/cli/dron/sanador.rb index e10773b2..a520bb2a 100644 --- a/adn/tools/cli/dron/sanador.rb +++ b/adn/tools/cli/dron/sanador.rb @@ -6,6 +6,9 @@ require_relative 'base' require_relative '../../db/core/dron_db' +require_relative 'ejecutor' +require 'json' +require 'time' module Dron class Sanador @@ -24,7 +27,8 @@ module Dron fallidos.each do |dron| espera = BACKOFF_BASE * (2 ** dron['reintentos'].to_i) - tiempo_desde_fallo = Time.now - dron['completed_at'] + completed_at = dron['completed_at'].is_a?(String) ? Time.parse(dron['completed_at']) : dron['completed_at'] + tiempo_desde_fallo = Time.now - completed_at if tiempo_desde_fallo < espera Base.logger.debug("⏳ #{dron['dron_id']} en backoff (#{tiempo_desde_fallo.round}s/#{espera}s)") @@ -38,7 +42,8 @@ module Dron else Base.logger.info("🩹 Re-intentando #{dron['dron_id']} (intento #{dron['reintentos'].to_i + 1}/#{MAX_REINTENTOS})") ADN::DB::DronDB.incrementar_reintentos(dron['dron_id']) - resultado = Dron::Ejecutor.lanzar(cmd: dron['cmd'], nota: "Reintento: #{dron.dig('metadata', 'nota')}", flujo_id: dron['flujo_id']) + meta = dron['metadata'].is_a?(String) ? (JSON.parse(dron['metadata']) rescue {}) : (dron['metadata'] || {}) + resultado = Dron::Ejecutor.lanzar(cmd: dron['cmd'], nota: "Reintento: #{meta['nota']}", flujo_id: dron['flujo_id']) reintentados += 1 if resultado[:exit_code] == 0 notificar_fallo_persistente(dron) if dron['reintentos'].to_i >= MAX_REINTENTOS && resultado[:exit_code] != 0 end diff --git a/adn/tools/cli/dron/vigilante.rb b/adn/tools/cli/dron/vigilante.rb index c6607c55..c4b306e8 100644 --- a/adn/tools/cli/dron/vigilante.rb +++ b/adn/tools/cli/dron/vigilante.rb @@ -136,11 +136,12 @@ module Dron resultado = db_query("SELECT id FROM bitacoras.events WHERE metadata->>'flujo_id' = $1", [flujo_id]) - if resultado.empty? - require_relative '../../db/core/bitacora_db' - BitacorasDB::BitacoraDB.crear_evento(nodo_id: 1, descripcion: descripcion, estado: estado_icono, metadata: { flujo_id: flujo_id, drones_count: count }.to_json) - else - BitacorasDB::BitacoraDB.actualizar_evento(resultado.first['id'], descripcion: descripcion, estado: estado_icono) + BitacorasDB::BitacoraDB.with_connection do |db| + if resultado.empty? + db.crear_evento(nodo_id: 1, descripcion: descripcion, estado: estado_icono, metadata: { flujo_id: flujo_id, drones_count: count }) + else + db.actualizar_evento(resultado.first['id'], descripcion: descripcion, estado: estado_icono) + end end rescue StandardError => e logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}") diff --git a/adn/tools/cli/novato.rb b/adn/tools/cli/novato.rb index 5bd71c29..f2e21a0b 100644 --- a/adn/tools/cli/novato.rb +++ b/adn/tools/cli/novato.rb @@ -35,25 +35,25 @@ module ADN end def ejecutar - if @args.empty? || @args.first == 'help' || @args.first == '--help' - mostrar_ayuda - return - end + puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'novato' ha sido reemplazado por 'dron lanzar'#{Color::RESET}" + puts "" + puts " El sistema AtomicDrones (Fase 1.5) reemplaza al novato con:" + puts " - Heartbeat visible en Bitácora Web cada 30s" + puts " - Timeout real implementado" + puts " - Registro automático en tablas internas + events" + puts "" + puts " #{Color::CYAN}Equivalencias:#{Color::RESET}" + puts " #{Color::DIM}novato lanzar --evento ID -- #{Color::RESET}" + puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- #{Color::RESET}" + puts "" - accion = @args.shift - case accion - when 'lanzar' - cmd_lanzar(@args) - when 'vigilar' - cmd_vigilar(@args) - when 'estado' - cmd_estado - when 'limpiar' - cmd_limpiar - else - puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}" - mostrar_ayuda - exit 1 + # Redirigir automáticamente si es 'lanzar' + if @args.first == 'lanzar' + @args.shift + puts "#{Color::YELLOW}Redirigiendo a 'dron lanzar'...#{Color::RESET}\n\n" + require_relative 'dron' + dron = ADN::SubcomandoDron.new(['lanzar'] + @args) + dron.ejecutar end end diff --git a/adn/tools/db/core/dron_db.rb b/adn/tools/db/core/dron_db.rb index fd886b26..0b2f8ff5 100644 --- a/adn/tools/db/core/dron_db.rb +++ b/adn/tools/db/core/dron_db.rb @@ -118,8 +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]) }.first.to_h - resultado.first + BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first end # Obtener drones por estado @@ -176,8 +175,8 @@ module ADN RETURNING reintentos SQL - resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first.to_h - resultado.first['reintentos'].to_i + resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first&.to_h + resultado ? resultado['reintentos'].to_i : 0 end # Registrar avance de un paso en flujo multi-paso @@ -229,8 +228,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]) }.first.to_h - resultado.first + BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo]) }.first&.to_h end # Actualizar métricas diarias (llamado automáticamente al finalizar un dron) @@ -240,13 +238,19 @@ module ADN dron = obtener_dron(dron_id) return unless dron - fecha = dron['started_at'].to_date + require 'time' + require 'date' + + started_time = dron['started_at'].is_a?(String) ? Time.parse(dron['started_at']) : dron['started_at'] + fecha = started_time.to_date tipo = dron['tipo'] # Calcular duración - started = dron['started_at'] - completed = dron['completed_at'] || Time.now - duracion = completed - started + started = started_time + completed_raw = dron['completed_at'] || Time.now + completed = completed_raw.is_a?(String) ? Time.parse(completed_raw) : completed_raw + duracion_secs = (completed - started).to_i + duracion = "#{duracion_secs} seconds" # Upsert de métricas sql = <<-SQL @@ -335,10 +339,11 @@ module ADN DELETE FROM bitacoras.dron_logs WHERE estado IN ('completed', 'failed', 'zombie') AND completed_at < NOW() - INTERVAL '#{dias} days' + RETURNING id SQL resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a } - resultado.first['count'].to_i + resultado.length end end end diff --git a/docs/ambito/dtic-ADN/A01.P009_Evolucion-Dron-ADN.md b/docs/ambito/dtic-ADN/A01.P009_Evolucion-Dron-ADN.md index 38a0486b..8171a333 100644 --- a/docs/ambito/dtic-ADN/A01.P009_Evolucion-Dron-ADN.md +++ b/docs/ambito/dtic-ADN/A01.P009_Evolucion-Dron-ADN.md @@ -1,10 +1,24 @@ # A01.P009 - Evolución del Ecosistema ADN al Concepto de Dron -> **Estado**: 🟢 En Implementación (Fase 1 completa) +> **Estado**: 🟢 Fase 1.5 completada > **Pertenece a**: [A01 - Ecosistema ADN](./A01_dtic-ADN.md) -> **Fecha**: 2026-04-07 +> **Fecha**: 2026-04-09 > **Responsable**: Lic. Ricardo MONLA -> **Versión**: 2.1 — AtomicDrones implementados +> **Versión**: 3.2 — Fase 1.5 completada, sistema dual eliminado + +--- + +## 📈 Progreso + +``` +Fase 0: ██████████ 100% Infraestructura de Datos (tablas internas) ✅ +Fase 1: ██████████ 100% Atomicidad (bugs corregidos) ✅ +Fase 1.5: ██████████ 100% Visibilidad Bitácora Web + Corrección Bugs ✅ +Fase 2: ░░░░░░░░░░ 0% Composición — Orquestación de Flujos +Fase 3: ░░░░░░░░░░ 0% Planificación — Drones Programados +Fase 4: ░░░░░░░░░░ 0% Inteligencia de Colmena +Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Métricas +``` --- @@ -263,19 +277,20 @@ La **Bitácora Web** (`http://localhost:5174/bitacoras/`) muestra: --- -## 📊 Estado Actual (Línea Base) +## 📊 Estado Actual (Línea Base — Auditoría 2026-04-09) ### Herramientas Existentes | Herramienta | Ubicación | Tipo | Estado | |-------------|-----------|------|--------| | `dron.rb` | `adn/tools/cli/dron.rb` | Dispatcher | ✅ Refactorizado | -| `dron/ejecutor.rb` | `adn/tools/cli/dron/ejecutor.rb` | Ejecutor atómico | ✅ Implementado | -| `dron/vigilante.rb` | `adn/tools/cli/dron/vigilante.rb` | Vigilante atómico | ✅ Implementado | -| `dron/sanador.rb` | `adn/tools/cli/dron/sanador.rb` | Sanador atómico | ✅ Implementado | +| `dron/ejecutor.rb` | `adn/tools/cli/dron/ejecutor.rb` | Ejecutor atómico | ⚠️ Bugs en bitácora web | +| `dron/vigilante.rb` | `adn/tools/cli/dron/vigilante.rb` | Vigilante atómico | ⚠️ Bugs en consolidación | +| `dron/sanador.rb` | `adn/tools/cli/dron/sanador.rb` | Sanador atómico | ⚠️ Bugs en reintentos | | `dron/bitacora.rb` | `adn/tools/cli/dron/bitacora.rb` | Bitácora atómica | ✅ Implementado | | `dron/base.rb` | `adn/tools/cli/dron/base.rb` | Módulo compartido | ✅ Implementado | -| `dron_db.rb` | `adn/tools/db/core/dron_db.rb` | Acceso a DB | ✅ Implementado | +| `dron_db.rb` | `adn/tools/db/core/dron_db.rb` | Acceso a DB | ⚠️ 3 bugs críticos | +| `dispatcher.rb` | `adn/tools/cli/dron/dispatcher.rb` | Routing subcomandos | ⚠️ 1 bug crítico | ### Capacidades Actuales @@ -294,14 +309,64 @@ La **Bitácora Web** (`http://localhost:5174/bitacoras/`) muestra: ./adn/tools/run dron reintentar ``` -### Limitaciones Detectadas (Deuda de Atomicidad) +### 🐛 Deuda Técnica Detectada (Auditoría 2026-04-09) + +#### Bugs Críticos (crashes directos) + +| # | Archivo | Línea | Bug | Impacto | +|---|---------|-------|-----|---------| +| B1 | `dispatcher.rb` | 214 | `BitacorasDB::BitacoraDB.ejecutar()` — método inexistente (debe ser `.with_connection { \|db\| db.execute() }`) | `dron estado ` crashea con NoMethodError | +| B2 | `dron_db.rb` | 178-179 | `incrementar_reintentos` hace doble `.first` (`.first.to_h` → Hash, luego `.first` sobre Hash = par `[k,v]`) | Sanador crashea al reintentar drones | +| B3 | `dron_db.rb` | 231-232 | `obtener_metricas` mismo patrón de doble `.first` | Consulta de métricas crashea | + +#### Bugs Medios (fallan en ciertos flujos) + +| # | Archivo | Línea | Bug | Impacto | +|---|---------|-------|-----|---------| +| B4 | `dron_db.rb` | 337-345 | `limpiar_antiguos` — DELETE sin RETURNING intenta leer `['count']` inexistente | Limpieza reporta 0 siempre | +| B5 | `dron_db.rb` | 253 | `actualizar_metricas` — pasa Float donde PG espera INTERVAL | Error de tipo al guardar métricas | +| B6 | `sanador.rb` | 27 | `Time.now - dron['completed_at']` — el campo viene como String, no Time | TypeError al calcular backoff | +| B7 | `sanador.rb` | 41 | `Dron::Ejecutor.lanzar(...)` sin `require_relative 'ejecutor'` | NameError si se llama directamente | + +#### Bugs Bajos (edge cases) + +| # | Archivo | Línea | Bug | Impacto | +|---|---------|-------|-----|---------| +| B8 | `sanador.rb` | 41 | `dron.dig('metadata', 'nota')` — metadata es String JSON, no Hash | Nota de reintento siempre nil | +| B9 | `ejecutor.rb` | 42-75 | Timeout nunca se aplica (falta `Timeout.timeout()` wrapping) | Drones largos nunca se frenan | +| B10 | `vigilante.rb` | 141,143 | `BitacorasDB::BitacoraDB.crear_evento(...)` como método de clase (es de instancia) | Consolidación de flujos falla silenciosamente | +| B11 | `ejecutor.rb` | 82-131 | `registrar_bitacora` con rescue genérico silencia todos los errores | Fallos de bitácora web invisibles | +| B12 | `dron.rb` / `base.rb` | 30 / 27 | Logger: `.instance` (singleton) vs `.new` (instancia separada) | Logs inconsistentes | + +### 🔍 Brecha Principal: Visibilidad en Bitácora Web + +**Problema crítico**: Los drones escriben en tablas internas (`dron_logs`) pero la **Bitácora Web** (`bitacoras.events`) no refleja su actividad. + +``` + CAPA 1 (dron_logs) CAPA 2 (events → Web) + ┌─────────────────┐ ┌─────────────────┐ + │ ✅ Drones │ ──?──→ │ ❌ La web no ve │ + │ escriben acá │ │ nada │ + └─────────────────┘ └─────────────────┘ + ↑ + La flecha "vigilante consolida" + NO FUNCIONA por bugs B1, B10, B11 +``` + +**Causas raíz:** +1. El heartbeat solo actualiza `dron_logs.heartbeat` (tabla interna), nunca `events` +2. El ejecutor tiene lógica de bitácora web pero falla silenciosamente (B11) +3. El vigilante llama métodos de clase inexistentes (B10) +4. Coexiste un sistema antiguo de scripts `.sh` (que sí funcionaba) con el nuevo Ruby (que no) + +### Limitaciones Futuras (post-corrección) | # | Limitación | Tipo | Impacto | Prioridad | |---|------------|------|---------|-----------| | 1 | Sin orquestación de flujos | Orquestador | No hay composición multi-paso | Media | | 2 | Sin planificación (cron) | Planificador | Requiere lanzamiento manual | Alta | | 3 | Sin notificaciones | Mensajero | Alertas manuales | Media | -| 4 | Sin dashboard web | Observabilidad | Visibilidad solo por CLI | Baja | +| 4 | Sin dashboard web dedicado | Observabilidad | Solo CLI + Bitácora Web | Baja | --- @@ -323,20 +388,77 @@ La **Bitácora Web** (`http://localhost:5174/bitacoras/`) muestra: --- -### Fase 1: Atomicidad — Separación de Responsabilidades (Dron 1.0) — ✅ COMPLETA +### Fase 1: Atomicidad — Separación de Responsabilidades (Dron 1.0) — ⚠️ CON DEUDA TÉCNICA **Objetivo:** Refactorizar el monolito `dron.rb` en módulos atómicos especializados. | ID | Tarea | Tipo de Dron | Estado | |:--:|-------|--------------|--------| -| F1.T1 | `dron/ejecutor.rb` | Ejecutor — lanza y monitorea comando | ✅ Completa | -| F1.T2 | `dron/vigilante.rb` | Vigilante — escanea flota, detecta zombies | ✅ Completa | -| F1.T3 | `dron/sanador.rb` | Sanador — re-intenta, limpia, notifica fallos | ✅ Completa | +| F1.T1 | `dron/ejecutor.rb` | Ejecutor — lanza y monitorea comando | ⚠️ Bugs B9, B11 | +| F1.T2 | `dron/vigilante.rb` | Vigilante — escanea flota, detecta zombies | ⚠️ Bug B10 | +| F1.T3 | `dron/sanador.rb` | Sanador — re-intenta, limpia, notifica fallos | ⚠️ Bugs B6, B7, B8 | | F1.T4 | `dron/bitacora.rb` | Registrador — auto-bitácora (inicio/fin) | ✅ Completa | -| F1.T5 | `dron/dispatcher.rb` | Dispatcher — routing de subcomandos | ✅ Completa | +| F1.T5 | `dron/dispatcher.rb` | Dispatcher — routing de subcomandos | ⚠️ Bug B1 | | F1.T6 | `dron/base.rb` | Módulo compartido (atomisidad) | ✅ Completa | +| F1.T7 | `dron_db.rb` | Capa de datos para drones | ⚠️ Bugs B2, B3, B4, B5 | -**Criterio de éxito:** ✅ Cada módulo tiene una única responsabilidad. ~40% menos código. Atomisidad aplicada. +**Criterio de éxito original:** ✅ Cada módulo tiene una única responsabilidad. ~40% menos código. +**Criterio revisado:** ⚠️ La estructura atómica es correcta pero **12 bugs** impiden operación confiable. Corregidos en Fase 1.5. + +--- + +### Fase 1.5: Visibilidad en Bitácora Web + Corrección de Bugs (Dron 1.1) — ✅ COMPLETA + +**Objetivo:** Que los drones sean **visibles desde la Bitácora Web** y corregir toda la deuda técnica de Fase 1. + +**Problema que resuelve:** Actualmente los drones son "fantasmas" — ejecutan tareas pero el usuario no puede ver su progreso desde la interfaz web principal. Solo escriben en tablas internas (`dron_logs`) pero nunca actualizan `bitacoras.events` de forma confiable. + +**Lo que el usuario debería ver en Bitácora Web:** +``` +┌─────┬───────┬───────┬─────────────────────────────────────────────┬───┬───┐ +│ ID │ I │ F │ Descripción │ J │ E │ +├─────┼───────┼───────┼─────────────────────────────────────────────┼───┼───┤ +│1420 │ 02:00 │ │ 🛸 Backup nocturno (dron_020000_123) │ A │ ⏳│ +│ │ │ │ ├─ Ejecutando: vzdump 103 --mode stop │ │ │ +│ │ │ │ └─ ❤️ Heartbeat: hace 45s │ │ │ +├─────┼───────┼───────┼─────────────────────────────────────────────┼───┼───┤ +│1419 │ 01:30 │ 01:45 │ 🛸 Sync rclone (dron_013000_456) — ✅ 15m │ A │ ✅│ +│ │ │ │ └─ Aterrizaje limpio — 2.3GB sincronizados │ │ │ +└─────┴───────┴───────┴─────────────────────────────────────────────┴───┴───┘ +``` + +| ID | Tarea | Descripción | Estado | +|:--:|-------|-------------|--------| +| F1.5.T1 | Corregir bugs críticos | Bugs B1, B2, B3 — crashes directos en dispatcher y dron_db | ✅ | +| F1.5.T2 | Corregir bugs medios | Bugs B4, B5, B6, B7 — fallos en sanador y métricas | ✅ | +| F1.5.T3 | Simplificar `registrar_bitacora` | Reescribir el método del ejecutor: crear evento → heartbeat → cierre limpio | ✅ | +| F1.5.T4 | Heartbeat visible en web | Que el heartbeat cada 30s actualice `events.descripcion` con progreso | ✅ | +| F1.5.T5 | Eliminar sistema dual `.sh`/Ruby | `copiloto.rb` y `novato.rb` deprecados, `tmp/dron/` limpiado | ✅ | +| F1.5.T6 | Corregir bugs bajos | Bugs B8-B12 — edge cases y consistencia | ✅ | + +**Flujo corregido del Ejecutor:** +``` + Ejecutor.lanzar(cmd, nota) + │ + ├─ 1. Registrar en dron_logs (tabla interna) ──→ DronDB + ├─ 2. Crear evento en events (Bitácora Web) ──→ 🌐 VISIBLE + │ + ├─ 3. Ejecutar comando con heartbeat thread: + │ loop cada 30s: + │ ├─ Actualizar dron_logs.heartbeat ──→ DronDB + │ └─ Actualizar events.descripcion ──→ 🌐 VISIBLE + │ + ├─ 4. Comando finaliza + ├─ 5. Registrar fin en dron_logs ──→ DronDB + └─ 6. Cerrar evento en events (estado + hora fin) ──→ 🌐 VISIBLE +``` + +**Criterio de éxito:** +- Cada dron lanzado aparece inmediatamente en la Bitácora Web como evento `⏳` +- El heartbeat actualiza la descripción del evento en web cada 30 segundos +- Al finalizar, el evento se cierra con `✅` o `❌`, hora de fin, y duración +- `dron estado `, `dron flota`, y `dron salud` funcionan sin crashes +- Cero bugs críticos, cero bugs medios --- @@ -418,18 +540,6 @@ La **Bitácora Web** (`http://localhost:5174/bitacoras/`) muestra: --- -## 📈 Progreso - -``` -Fase 0: ██████████ 100% Infraestructura de Datos (tablas internas) ✅ -Fase 1: ██████████ 100% Atomicidad — Separación de Responsabilidades (Dron 1.0) ✅ -Fase 2: ░░░░░░░░░░ 0% Composición — Orquestación de Flujos (Dron 1.5) -Fase 3: ░░░░░░░░░░ 0% Planificación — Drones Programados (Dron 2.0) -Fase 4: ░░░░░░░░░░ 0% Inteligencia de Colmena (Dron 3.0) -Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Métricas (Dron 4.0) -``` - ---- ## 🏗️ Arquitectura de Colmena @@ -683,7 +793,7 @@ Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Mé ## 🔄 Armonía Integral -**Última actualización**: 2026-04-07 — Fase 0 y Fase 1 completas +**Última actualización**: 2026-04-09 — Auditoría + Fase 1.5 planificada | Documento | Estado | Notas | |-----------|--------|-------| @@ -694,17 +804,25 @@ Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Mé --- -## ✅ Entregables Fase 1 (Completados) +## 📦 Entregables -| Archivo | Líneas | Descripción | -|---------|--------|-------------| -| `adn/tools/cli/dron/base.rb` | 60 | Módulo compartido (atomisidad) | -| `adn/tools/cli/dron/dispatcher.rb` | 80 | Routing de subcomandos | -| `adn/tools/cli/dron/ejecutor.rb` | 90 | Ejecutor atómico | -| `adn/tools/cli/dron/vigilante.rb` | 140 | Vigilante atómico | -| `adn/tools/cli/dron/sanador.rb` | 80 | Sanador atómico | -| `adn/tools/cli/dron/bitacora.rb` | 70 | Bitácora atómica | -| `adn/tools/db/core/dron_db.rb` | 340 | Acceso a datos DB | -| `adn/tools/db/migrations/003_create_dron_tables.rb` | 150 | Migración DB | +### Fase 1 (Estructura atómica — con deuda técnica) -**Total:** ~1010 líneas nuevas, ~460 líneas menos que el monolito original (neto: -40%) +| Archivo | Líneas | Descripción | Estado | +|---------|--------|-------------|--------| +| `adn/tools/cli/dron/base.rb` | 66 | Módulo compartido (atomisidad) | ✅ | +| `adn/tools/cli/dron/dispatcher.rb` | 284 | Routing de subcomandos | ⚠️ B1 | +| `adn/tools/cli/dron/ejecutor.rb` | 171 | Ejecutor atómico | ⚠️ B9, B11 | +| `adn/tools/cli/dron/vigilante.rb` | 151 | Vigilante atómico | ⚠️ B10 | +| `adn/tools/cli/dron/sanador.rb` | 85 | Sanador atómico | ⚠️ B6, B7, B8 | +| `adn/tools/cli/dron/bitacora.rb` | 76 | Bitácora atómica | ✅ | +| `adn/tools/db/core/dron_db.rb` | 351 | Acceso a datos DB | ⚠️ B2-B5 | +| `adn/tools/db/migrations/003_create_dron_tables.rb` | 232 | Migración DB | ✅ | + +### Fase 1.5 (Pendiente — Visibilidad + Corrección) + +Entregables esperados: +- Corrección de los 12 bugs detectados +- Heartbeat visible en Bitácora Web +- Eliminación del sistema dual `.sh`/Ruby +- Flujo ejecutor→events→web funcional y probado