# frozen_string_literal: true # Dron::Vigilante — Módulo atómico para monitoreo de flota # # Responsabilidad única: Escanear flota y detectar anomalías require_relative 'base' require_relative '../../db/core/dron_db' module Dron class Vigilante ZOMBIE_TIMEOUT = 300 ANOMALY_THRESHOLD = 5 class << self def logger @logger ||= ADN::Logger.new end def db_query(sql, params = []) BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params).to_a } end def db_exec(sql, params = []) BitacorasDB::BitacoraDB.with_connection { |db| db.execute(sql, params) } end def escanear_flota logger.info("👁️ Dron Vigilante: Escaneando flota...") resumen = ADN::DB::DronDB.resumen_flota output = ["\n🛸 Flota de Drones", "═" * 60] output << " Total: #{resumen[:total]} | 🟢 #{resumen[:running] || 0} | ⏳ #{resumen[:pending] || 0} | ✅ #{resumen[:completed] || 0} | ❌ #{resumen[:failed] || 0} | 🧟 #{resumen[:zombie] || 0}" output << "═" * 60 puts output.join("\n") { exitoso: true, resumen: resumen, timestamp: Time.now } rescue StandardError => e logger.error("👁️ Error escaneando flota: #{e.message}") { exitoso: false, error: e.message, timestamp: Time.now } end def detectar_zombies zombies = ADN::DB::DronDB.detectar_zombies return [] if zombies.empty? logger.warn("👁️ Detectados #{zombies.count} drones zombies:") zombies.each do |z| minutos = z['minutos_sin_heartbeat'].to_f.round(1) logger.warn(" 🧟 #{z['dron_id']} - #{minutos}min sin heartbeat") ADN::DB::DronDB.marcar_zombie(z['dron_id']) registrar_avance(z['dron_id'], "Dron zombie (#{minutos}min sin heartbeat)") end zombies end def salud logger.info("👁️ Dron Vigilante: Health check...") diagnosticos = { zombies: [], fallos_recientes: [], anomalias: [], saludable: true } diagnosticos[:zombies] = detectar_zombies.map { |z| z['dron_id'] } diagnosticos[:fallos_recientes] = db_query(sql_fallos_recientes) if diagnosticos[:fallos_recientes].count >= ANOMALY_THRESHOLD diagnosticos[:anomalias] << "Demasiados fallos: #{diagnosticos[:fallos_recientes].count} en 1h" diagnosticos[:saludable] = false end running_antiguos = db_query(sql_running_antiguos) unless running_antiguos.empty? diagnosticos[:anomalias] << "Drones running antiguos: #{running_antiguos.count}" diagnosticos[:saludable] = false end puts "\n🏥 Salud de la Flota" puts "═" * 60 puts " Estado: #{diagnosticos[:saludable] ? '✅ Saludable' : '🚨 Problemas'}" puts " Zombies: #{diagnosticos[:zombies].count} | Fallos recientes: #{diagnosticos[:fallos_recientes].count} | Anomalías: #{diagnosticos[:anomalias].count}" diagnosticos[:anomalias].each { |a| puts " ⚠️ #{a}" } unless diagnosticos[:anomalias].empty? puts "═" * 60 diagnosticos end def consolidar_flujo(flujo_id) estado = ADN::DB::DronDB.consolidar_estado_flujo(flujo_id) return nil unless estado drones = ADN::DB::DronDB.obtener_por_flujo(flujo_id) actualizar_evento_resumen(flujo_id, estado, drones.count) logger.info("👁️ Flujo #{flujo_id} consolidado: #{estado} (#{drones.count} drones)") estado end def dashboard resumen = ADN::DB::DronDB.resumen_flota activos = db_query(sql_activos) output = ["\n🛸 Dashboard de Drones", "═" * 70] output << " Total: #{resumen[:total]} | 🟢 #{resumen[:running] || 0} | ⏳ #{resumen[:pending] || 0} | ✅ #{resumen[:completed] || 0} | ❌ #{resumen[:failed] || 0} | 🧟 #{resumen[:zombie] || 0}" output << "═" * 70 unless activos.empty? output << "\n Activos:" output << " #{"ID".ljust(25)} | #{"Tipo".ljust(15)} | #{"Inicio".ljust(10)} | Cmd" output << " " + "─" * 70 activos.each do |d| 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") end private def sql_fallos_recientes "SELECT * FROM bitacoras.dron_logs WHERE estado = 'failed' AND completed_at > NOW() - INTERVAL '60 minutes' ORDER BY completed_at DESC" end def sql_running_antiguos "SELECT * FROM bitacoras.dron_logs WHERE estado = 'running' AND started_at < NOW() - INTERVAL '1 hour' ORDER BY started_at ASC" end def sql_activos "SELECT * FROM bitacoras.dron_logs WHERE estado IN ('running', 'pending') ORDER BY started_at DESC" end def registrar_avance(dron_id, descripcion) db_exec("INSERT INTO bitacoras.dron_avances (dron_id, paso, descripcion, estado) VALUES ($1, -1, $2, 'failed')", [dron_id, descripcion]) end def actualizar_evento_resumen(flujo_id, estado, count) estado_icono = case estado when 'completed' then '✅' when 'failed' then '❌' else '⏳' end descripcion = "🛸 Flujo #{flujo_id}: #{count} drones - #{estado_icono}" 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) end rescue StandardError => e logger.warn("⚠️ No se pudo actualizar evento resumen: #{e.message}") end end end end