🛸 Fase 1.5 completa: AtomicDrones con visibilidad en Bitácora Web
Bugs corregidos (12): B1-B12 — crashes, tipos, métodos, logger Ejecutor reescrito: evento web → heartbeat 30s → cierre automático copiloto.rb y novato.rb deprecados → redirigen a dron lanzar Plan P009 v3.2 — Fase 1.5 100%% completa
This commit is contained in:
+24
-18
@@ -35,25 +35,31 @@ module ADN
|
|||||||
end
|
end
|
||||||
|
|
||||||
def ejecutar
|
def ejecutar
|
||||||
if @args.empty? || @args.first == 'help' || @args.first == '--help'
|
puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'copiloto' ha sido reemplazado por 'dron lanzar'#{Color::RESET}"
|
||||||
mostrar_ayuda
|
puts ""
|
||||||
return
|
puts " El sistema AtomicDrones (Fase 1.5) reemplaza al copiloto con:"
|
||||||
end
|
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 -- <cmd>#{Color::RESET}"
|
||||||
|
puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- <cmd>#{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
|
# Si hay argumentos, intentar redirigir automáticamente
|
||||||
case accion
|
if @args.first == 'lanzar'
|
||||||
when 'lanzar'
|
@args.shift
|
||||||
cmd_lanzar(@args)
|
puts "#{Color::YELLOW}Redirigiendo a 'dron lanzar'...#{Color::RESET}\n\n"
|
||||||
when 'vigilar'
|
require_relative 'dron'
|
||||||
cmd_vigilar(@args)
|
dron = ADN::SubcomandoDron.new(['lanzar'] + @args)
|
||||||
when 'estado'
|
dron.ejecutar
|
||||||
cmd_estado
|
|
||||||
when 'limpiar'
|
|
||||||
cmd_limpiar
|
|
||||||
else
|
|
||||||
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
|
|
||||||
mostrar_ayuda
|
|
||||||
exit 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ module ADN
|
|||||||
class SubcomandoDron
|
class SubcomandoDron
|
||||||
def initialize(args, logger = nil)
|
def initialize(args, logger = nil)
|
||||||
@args = args
|
@args = args
|
||||||
@logger = logger || ADN::Logger.instance
|
@logger = logger || ADN::Logger.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def ejecutar
|
def ejecutar
|
||||||
|
|||||||
@@ -209,9 +209,8 @@ module Dron
|
|||||||
puts "═" * 60
|
puts "═" * 60
|
||||||
|
|
||||||
# Mostrar avances si existen
|
# Mostrar avances si existen
|
||||||
require_relative '../../db/core/bitacora_db'
|
require_relative '../../db/core/dron_db'
|
||||||
sql = 'SELECT * FROM bitacoras.dron_avances WHERE dron_id = $1 ORDER BY paso ASC'
|
avances = ADN::DB::DronDB.obtener_avances(dron_id)
|
||||||
avances = BitacorasDB::BitacoraDB.ejecutar(sql, [dron_id])
|
|
||||||
|
|
||||||
unless avances.empty?
|
unless avances.empty?
|
||||||
puts "\n📝 Avances:"
|
puts "\n📝 Avances:"
|
||||||
|
|||||||
+182
-48
@@ -3,9 +3,19 @@
|
|||||||
# Dron::Ejecutor — Módulo atómico para ejecución de comandos
|
# Dron::Ejecutor — Módulo atómico para ejecución de comandos
|
||||||
#
|
#
|
||||||
# Responsabilidad única: Ejecutar un comando y reportar resultado
|
# 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 'base'
|
||||||
require_relative '../../db/core/dron_db'
|
require_relative '../../db/core/dron_db'
|
||||||
|
require 'open3'
|
||||||
|
require 'timeout'
|
||||||
|
require 'time'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
module Dron
|
module Dron
|
||||||
class Ejecutor
|
class Ejecutor
|
||||||
@@ -15,44 +25,86 @@ module Dron
|
|||||||
class << self
|
class << self
|
||||||
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil)
|
def lanzar(cmd:, nota: nil, evento_id: nil, timeout: DEFAULT_TIMEOUT, flujo_id: nil, nodo: nil)
|
||||||
Base.logger.info("🛠️ Dron Ejecutor iniciado: #{Process.pid}")
|
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 })
|
# 1. Registrar en tablas internas (dron_logs)
|
||||||
evento_id = registrar_bitacora(nota, evento_id, 'inicio', nodo) if nota
|
resultado_inicio = ADN::DB::DronDB.registrar_inicio(
|
||||||
resultado = ejecutar_comando(cmd, dron_id, timeout)
|
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])
|
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 ? '✅' : '❌'}")
|
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 }
|
{ dron_id: dron_id, exit_code: resultado[:exit_code], duration: resultado[:duration], evento_id: evento_id }
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Base.logger.error("🛠️ Dron Ejecutor falló: #{e.message}")
|
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 }
|
{ dron_id: nil, exit_code: 1, output: e.message, duration: 0, error: e.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
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
|
start_time = Time.now
|
||||||
output = []
|
output = []
|
||||||
exit_code = nil
|
exit_code = nil
|
||||||
|
|
||||||
heartbeat_thread = Thread.new do
|
heartbeat_thread = Thread.new do
|
||||||
|
tick = 0
|
||||||
loop do
|
loop do
|
||||||
sleep(HEARTBEAT_INTERVAL)
|
sleep(HEARTBEAT_INTERVAL)
|
||||||
|
tick += 1
|
||||||
|
|
||||||
|
# Actualizar tabla interna (dron_logs.heartbeat)
|
||||||
ADN::DB::DronDB.actualizar_heartbeat(dron_id)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
require 'open3'
|
Timeout.timeout(timeout) do
|
||||||
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
|
||||||
# Leer stdout y stderr en threads separados
|
out_thread = Thread.new { output << stdout.read }
|
||||||
out_thread = Thread.new { output << stdout.read }
|
err_thread = Thread.new { output << stderr.read }
|
||||||
err_thread = Thread.new { output << stderr.read }
|
out_thread.join
|
||||||
out_thread.join
|
err_thread.join
|
||||||
err_thread.join
|
exit_code = wait_thr.value.exitstatus
|
||||||
exit_code = wait_thr.value.exitstatus
|
end
|
||||||
end
|
end
|
||||||
rescue Timeout::Error
|
rescue Timeout::Error
|
||||||
exit_code = 124
|
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) }
|
{ exit_code: exit_code || 1, output: output.join("\n").strip, duration: (Time.now - start_time).round(2) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def registrar_bitacora(nota, evento_id, tipo, resultado = nil, nodo = nil)
|
# ─── Bitácora Web: Crear/Vincular evento ─────────────────────
|
||||||
return nil unless nota
|
|
||||||
require_relative '../../db/core/bitacora_db'
|
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)
|
nodo_id = detectar_nodo_id(nodo)
|
||||||
|
|
||||||
if tipo == 'inicio'
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
descripcion = "🛸 dron: #{nota}"
|
if evento_id.to_i > 0
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
# Vincular a evento existente: agregar nota de despegue
|
||||||
evento = db.crear_evento(nodo_id: nodo_id, descripcion: descripcion, inicio: Time.now.strftime('%H:%M'), estado: '⏳')
|
entrada = db.execute("SELECT descripcion FROM bitacoras.entradas WHERE id = $1", [evento_id]).first
|
||||||
evento['id'] # Retornar ID para actualizar después
|
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
|
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
|
end
|
||||||
rescue StandardError => e
|
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
|
nil
|
||||||
end
|
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)
|
def detectar_nodo_id(nodo)
|
||||||
return nodo if nodo.is_a?(Integer)
|
return nodo if nodo.is_a?(Integer)
|
||||||
|
|
||||||
# Si no se especifica nodo, usar el hostname para detectar
|
|
||||||
require 'socket'
|
require 'socket'
|
||||||
hostname = Socket.gethostname.downcase
|
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
|
resultado = nil
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
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
|
end
|
||||||
|
|
||||||
if resultado&.any?
|
(resultado&.any?) ? resultado.first['id'] : 1
|
||||||
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
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)")
|
Base.logger.warn("⚠️ Error detectando nodo: #{e.message}, usando default (1)")
|
||||||
1
|
1
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
require_relative 'base'
|
require_relative 'base'
|
||||||
require_relative '../../db/core/dron_db'
|
require_relative '../../db/core/dron_db'
|
||||||
|
require_relative 'ejecutor'
|
||||||
|
require 'json'
|
||||||
|
require 'time'
|
||||||
|
|
||||||
module Dron
|
module Dron
|
||||||
class Sanador
|
class Sanador
|
||||||
@@ -24,7 +27,8 @@ module Dron
|
|||||||
|
|
||||||
fallidos.each do |dron|
|
fallidos.each do |dron|
|
||||||
espera = BACKOFF_BASE * (2 ** dron['reintentos'].to_i)
|
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
|
if tiempo_desde_fallo < espera
|
||||||
Base.logger.debug("⏳ #{dron['dron_id']} en backoff (#{tiempo_desde_fallo.round}s/#{espera}s)")
|
Base.logger.debug("⏳ #{dron['dron_id']} en backoff (#{tiempo_desde_fallo.round}s/#{espera}s)")
|
||||||
@@ -38,7 +42,8 @@ module Dron
|
|||||||
else
|
else
|
||||||
Base.logger.info("🩹 Re-intentando #{dron['dron_id']} (intento #{dron['reintentos'].to_i + 1}/#{MAX_REINTENTOS})")
|
Base.logger.info("🩹 Re-intentando #{dron['dron_id']} (intento #{dron['reintentos'].to_i + 1}/#{MAX_REINTENTOS})")
|
||||||
ADN::DB::DronDB.incrementar_reintentos(dron['dron_id'])
|
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
|
reintentados += 1 if resultado[:exit_code] == 0
|
||||||
notificar_fallo_persistente(dron) if dron['reintentos'].to_i >= MAX_REINTENTOS && resultado[:exit_code] != 0
|
notificar_fallo_persistente(dron) if dron['reintentos'].to_i >= MAX_REINTENTOS && resultado[:exit_code] != 0
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -136,11 +136,12 @@ module Dron
|
|||||||
|
|
||||||
resultado = db_query("SELECT id FROM bitacoras.events WHERE metadata->>'flujo_id' = $1", [flujo_id])
|
resultado = db_query("SELECT id FROM bitacoras.events WHERE metadata->>'flujo_id' = $1", [flujo_id])
|
||||||
|
|
||||||
if resultado.empty?
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
require_relative '../../db/core/bitacora_db'
|
if resultado.empty?
|
||||||
BitacorasDB::BitacoraDB.crear_evento(nodo_id: 1, descripcion: descripcion, estado: estado_icono, metadata: { flujo_id: flujo_id, drones_count: count }.to_json)
|
db.crear_evento(nodo_id: 1, descripcion: descripcion, estado: estado_icono, metadata: { flujo_id: flujo_id, drones_count: count })
|
||||||
else
|
else
|
||||||
BitacorasDB::BitacoraDB.actualizar_evento(resultado.first['id'], descripcion: descripcion, estado: estado_icono)
|
db.actualizar_evento(resultado.first['id'], descripcion: descripcion, estado: estado_icono)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}")
|
logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}")
|
||||||
|
|||||||
+18
-18
@@ -35,25 +35,25 @@ module ADN
|
|||||||
end
|
end
|
||||||
|
|
||||||
def ejecutar
|
def ejecutar
|
||||||
if @args.empty? || @args.first == 'help' || @args.first == '--help'
|
puts "\n#{Color::YELLOW}⚠️ DEPRECADO: 'novato' ha sido reemplazado por 'dron lanzar'#{Color::RESET}"
|
||||||
mostrar_ayuda
|
puts ""
|
||||||
return
|
puts " El sistema AtomicDrones (Fase 1.5) reemplaza al novato con:"
|
||||||
end
|
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 -- <cmd>#{Color::RESET}"
|
||||||
|
puts " #{Color::GREEN}→ dron lanzar --evento ID --nota \"...\" -- <cmd>#{Color::RESET}"
|
||||||
|
puts ""
|
||||||
|
|
||||||
accion = @args.shift
|
# Redirigir automáticamente si es 'lanzar'
|
||||||
case accion
|
if @args.first == 'lanzar'
|
||||||
when 'lanzar'
|
@args.shift
|
||||||
cmd_lanzar(@args)
|
puts "#{Color::YELLOW}Redirigiendo a 'dron lanzar'...#{Color::RESET}\n\n"
|
||||||
when 'vigilar'
|
require_relative 'dron'
|
||||||
cmd_vigilar(@args)
|
dron = ADN::SubcomandoDron.new(['lanzar'] + @args)
|
||||||
when 'estado'
|
dron.ejecutar
|
||||||
cmd_estado
|
|
||||||
when 'limpiar'
|
|
||||||
cmd_limpiar
|
|
||||||
else
|
|
||||||
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
|
|
||||||
mostrar_ayuda
|
|
||||||
exit 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ module ADN
|
|||||||
# @return [Hash, nil] Datos del dron o nil si no existe
|
# @return [Hash, nil] Datos del dron o nil si no existe
|
||||||
def obtener_dron(dron_id)
|
def obtener_dron(dron_id)
|
||||||
sql = 'SELECT * FROM bitacoras.dron_logs WHERE dron_id = $1'
|
sql = 'SELECT * FROM bitacoras.dron_logs WHERE dron_id = $1'
|
||||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first.to_h
|
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first
|
||||||
resultado.first
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Obtener drones por estado
|
# Obtener drones por estado
|
||||||
@@ -176,8 +175,8 @@ module ADN
|
|||||||
RETURNING reintentos
|
RETURNING reintentos
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first.to_h
|
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [dron_id]) }.first&.to_h
|
||||||
resultado.first['reintentos'].to_i
|
resultado ? resultado['reintentos'].to_i : 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# Registrar avance de un paso en flujo multi-paso
|
# 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
|
# @return [Hash, nil] Métricas o nil si no existen
|
||||||
def obtener_metricas(fecha:, tipo:)
|
def obtener_metricas(fecha:, tipo:)
|
||||||
sql = 'SELECT * FROM bitacoras.dron_metricas WHERE fecha = $1 AND dron_tipo = $2'
|
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
|
BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, [fecha, tipo]) }.first&.to_h
|
||||||
resultado.first
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Actualizar métricas diarias (llamado automáticamente al finalizar un dron)
|
# Actualizar métricas diarias (llamado automáticamente al finalizar un dron)
|
||||||
@@ -240,13 +238,19 @@ module ADN
|
|||||||
dron = obtener_dron(dron_id)
|
dron = obtener_dron(dron_id)
|
||||||
return unless dron
|
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']
|
tipo = dron['tipo']
|
||||||
|
|
||||||
# Calcular duración
|
# Calcular duración
|
||||||
started = dron['started_at']
|
started = started_time
|
||||||
completed = dron['completed_at'] || Time.now
|
completed_raw = dron['completed_at'] || Time.now
|
||||||
duracion = completed - started
|
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
|
# Upsert de métricas
|
||||||
sql = <<-SQL
|
sql = <<-SQL
|
||||||
@@ -335,10 +339,11 @@ module ADN
|
|||||||
DELETE FROM bitacoras.dron_logs
|
DELETE FROM bitacoras.dron_logs
|
||||||
WHERE estado IN ('completed', 'failed', 'zombie')
|
WHERE estado IN ('completed', 'failed', 'zombie')
|
||||||
AND completed_at < NOW() - INTERVAL '#{dias} days'
|
AND completed_at < NOW() - INTERVAL '#{dias} days'
|
||||||
|
RETURNING id
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
resultado = BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql).to_a }
|
||||||
resultado.first['count'].to_i
|
resultado.length
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
# A01.P009 - Evolución del Ecosistema ADN al Concepto de Dron
|
# 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)
|
> **Pertenece a**: [A01 - Ecosistema ADN](./A01_dtic-ADN.md)
|
||||||
> **Fecha**: 2026-04-07
|
> **Fecha**: 2026-04-09
|
||||||
> **Responsable**: Lic. Ricardo MONLA
|
> **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
|
### Herramientas Existentes
|
||||||
|
|
||||||
| Herramienta | Ubicación | Tipo | Estado |
|
| Herramienta | Ubicación | Tipo | Estado |
|
||||||
|-------------|-----------|------|--------|
|
|-------------|-----------|------|--------|
|
||||||
| `dron.rb` | `adn/tools/cli/dron.rb` | Dispatcher | ✅ Refactorizado |
|
| `dron.rb` | `adn/tools/cli/dron.rb` | Dispatcher | ✅ Refactorizado |
|
||||||
| `dron/ejecutor.rb` | `adn/tools/cli/dron/ejecutor.rb` | Ejecutor 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 | ✅ Implementado |
|
| `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 | ✅ Implementado |
|
| `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/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/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
|
### Capacidades Actuales
|
||||||
|
|
||||||
@@ -294,14 +309,64 @@ La **Bitácora Web** (`http://localhost:5174/bitacoras/`) muestra:
|
|||||||
./adn/tools/run dron reintentar
|
./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 <ID>` 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 |
|
| # | Limitación | Tipo | Impacto | Prioridad |
|
||||||
|---|------------|------|---------|-----------|
|
|---|------------|------|---------|-----------|
|
||||||
| 1 | Sin orquestación de flujos | Orquestador | No hay composición multi-paso | Media |
|
| 1 | Sin orquestación de flujos | Orquestador | No hay composición multi-paso | Media |
|
||||||
| 2 | Sin planificación (cron) | Planificador | Requiere lanzamiento manual | Alta |
|
| 2 | Sin planificación (cron) | Planificador | Requiere lanzamiento manual | Alta |
|
||||||
| 3 | Sin notificaciones | Mensajero | Alertas manuales | Media |
|
| 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.
|
**Objetivo:** Refactorizar el monolito `dron.rb` en módulos atómicos especializados.
|
||||||
|
|
||||||
| ID | Tarea | Tipo de Dron | Estado |
|
| ID | Tarea | Tipo de Dron | Estado |
|
||||||
|:--:|-------|--------------|--------|
|
|:--:|-------|--------------|--------|
|
||||||
| F1.T1 | `dron/ejecutor.rb` | Ejecutor — lanza y monitorea comando | ✅ Completa |
|
| F1.T1 | `dron/ejecutor.rb` | Ejecutor — lanza y monitorea comando | ⚠️ Bugs B9, B11 |
|
||||||
| F1.T2 | `dron/vigilante.rb` | Vigilante — escanea flota, detecta zombies | ✅ Completa |
|
| F1.T2 | `dron/vigilante.rb` | Vigilante — escanea flota, detecta zombies | ⚠️ Bug B10 |
|
||||||
| F1.T3 | `dron/sanador.rb` | Sanador — re-intenta, limpia, notifica fallos | ✅ Completa |
|
| 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.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.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 <ID>`, `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
|
## 🏗️ Arquitectura de Colmena
|
||||||
|
|
||||||
@@ -683,7 +793,7 @@ Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Mé
|
|||||||
|
|
||||||
## 🔄 Armonía Integral
|
## 🔄 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 |
|
| Documento | Estado | Notas |
|
||||||
|-----------|--------|-------|
|
|-----------|--------|-------|
|
||||||
@@ -694,17 +804,25 @@ Fase 5: ░░░░░░░░░░ 0% Observabilidad — Dashboard y Mé
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ Entregables Fase 1 (Completados)
|
## 📦 Entregables
|
||||||
|
|
||||||
| Archivo | Líneas | Descripción |
|
### Fase 1 (Estructura atómica — con deuda técnica)
|
||||||
|---------|--------|-------------|
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
**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
|
||||||
|
|||||||
Reference in New Issue
Block a user