# frozen_string_literal: true # tools/adn/subcomando_db.rb — Subcomando para operaciones con base de datos dtic-BITACORAs # ============================================================================== # Este subcomando integra las operaciones de base de datos PostgreSQL con la CLI ADN. # Proporciona comandos para gestión de bitácoras, nodos, temas y entradas directamente # en la base de datos, permitiendo la migración gradual desde archivos .md. # # Comandos disponibles: # • adn db salud:bd — Verificar conexión y estado de la BD # • adn db bitacora:crear [FECHA] — Crear nueva bitácora diaria # • adn db bitacora:listar [--limit N] — Listar bitácoras existentes # • adn db evento:crear [OPCIONES] — Crear nueva entrada cronológica # • adn db evento:listar [OPCIONES] — Listar entradas con filtros # • adn db nodo:crear [OPCIONES] — Crear nuevo nodo (servidor/VM/PC) # • adn db nodo:listar [OPCIONES] — Listar nodos del sistema # • adn db migrar:md [FECHA] — Migrar bitácora .md a base de datos # • adn db estadisticas — Mostrar estadísticas del sistema # # Uso: # ./tools/adn/run db [SUBCOMANDO] [OPCIONES] require 'optparse' require 'date' require 'time' # Cargar módulo BitacorasDB begin require_relative '../../bitacoras-db/core/bitacora_db' require_relative '../../bitacoras-db/importers/md_importer' rescue LoadError => e # Fallback para desarrollo puts "⚠ Advertencia: No se pudo cargar BitacoraDB o MDImporter: #{e.message}" puts " Asegúrate de que los archivos en tools/bitacoras-db/ existen" end module ADN class SubcomandoDB # Inicializa el subcomando con argumentos y logger # # @param args [Array] Argumentos de línea de comandos # @param logger [ADNLogger] Instancia de logger para registro def initialize(args, logger) @args = args @logger = logger @config = cargar_configuracion end # Ejecuta el subcomando según los argumentos proporcionados def ejecutar if @args.empty? || @args[0] == 'help' mostrar_ayuda return end subcomando = @args[0] sub_args = @args[1..-1] || [] case subcomando when 'salud:bd' comando_salud_bd when 'bitacora:crear' comando_bitacora_crear(sub_args) when 'bitacora:listar' comando_bitacora_listar(sub_args) when 'evento:crear' comando_evento_crear(sub_args) when 'evento:listar' comando_evento_listar(sub_args) when 'nodo:crear' comando_nodo_crear(sub_args) when 'nodo:listar' comando_nodo_listar(sub_args) when 'migrar:md' comando_migrar_md(sub_args) when 'estadisticas' comando_estadisticas when 'backup' comando_backup(sub_args) else @logger.error("Subcomando desconocido: #{subcomando}") puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}" puts "" mostrar_ayuda exit 1 end rescue Interrupt puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}" @logger.info("Operación de base de datos interrumpida por usuario") exit 130 rescue => e @logger.error("Error en operación de base de datos: #{e.message}", { backtrace: e.backtrace }) puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] exit 1 end private # ─── COMANDOS PRINCIPALES ────────────────────────────────────────────── # Comando: salud:bd - Verificar conexión y estado de la base de datos def comando_salud_bd puts "#{Color::CYAN}🔍 Verificando salud de la base de datos dtic-BITACORAs...#{Color::RESET}" puts "" begin BitacorasDB::BitacoraDB.with_connection(env: :development) do |db| salud = db.health_check case salud[:status] when 'healthy' puts "#{Color::GREEN}✅ Conexión a PostgreSQL establecida#{Color::RESET}" puts "" puts " #{Color::BOLD}Versión PostgreSQL:#{Color::RESET} #{salud[:postgres_version].split(',')[0]}" puts " #{Color::BOLD}Bitácoras en BD:#{Color::RESET} #{salud[:bitacoras_count]}" puts " #{Color::BOLD}Servidor:#{Color::RESET} #{salud[:connection][:host]}:#{salud[:connection][:port]}" puts " #{Color::BOLD}Base de datos:#{Color::RESET} #{salud[:connection][:database]}" puts " #{Color::BOLD}Timestamp:#{Color::RESET} #{salud[:timestamp]}" when 'disconnected' puts "#{Color::RED}❌ No hay conexión activa a la base de datos#{Color::RESET}" puts " #{Color::DIM}#{salud[:message]}#{Color::RESET}" when 'unhealthy' puts "#{Color::YELLOW}⚠ Problema de salud en la base de datos#{Color::RESET}" puts " #{Color::DIM}#{salud[:message]}#{Color::RESET}" end end # Verificar contenedor Docker puts "" puts "#{Color::CYAN}🔍 Verificando contenedor Docker...#{Color::RESET}" if system("docker ps --filter name=postgres --format '{{.Names}}' | grep -q postgres") puts "#{Color::GREEN}✅ Contenedor PostgreSQL en ejecución#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Contenedor PostgreSQL no encontrado#{Color::RESET}" puts " Ejecuta: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}" end rescue PG::ConnectionBad => e puts "#{Color::RED}❌ Error de conexión a PostgreSQL#{Color::RESET}" puts " #{e.message}" puts "" puts "#{Color::YELLOW}Solución:#{Color::RESET}" puts " 1. Verifica que el contenedor esté en ejecución:" puts " #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}" puts " 2. Espera 10-20 segundos para que PostgreSQL inicie" puts " 3. Verifica credenciales en tools/config/database.yml" rescue => e puts "#{Color::RED}❌ Error inesperado#{Color::RESET}" puts " #{e.message}" end end # Comando: bitacora:crear [FECHA] - Crear nueva bitácora diaria def comando_bitacora_crear(args) opts = { fecha: Date.today, resumen: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db bitacora:crear [FECHA] [OPCIONES]" opt.on("--resumen TEXTO", "Resumen inicial de la bitácora") do |r| opts[:resumen] = r end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) # Si queda un argumento sin procesar, asumimos que es la fecha opts[:fecha] = Date.parse(args[0]) if args[0] rescue OptionParser::InvalidOption, Date::Error => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📝 Creando bitácora para #{opts[:fecha]}...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| begin bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen]) puts "#{Color::GREEN}✅ Bitácora creada exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}ID:#{Color::RESET} #{bitacora['id']}" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{bitacora['fecha']}" puts " #{Color::BOLD}Resumen:#{Color::RESET} #{bitacora['resumen'] || '(sin resumen)'}" puts " #{Color::BOLD}Creada:#{Color::RESET} #{bitacora['created_at']}" rescue PG::UniqueViolation puts "#{Color::YELLOW}⚠ Ya existe una bitácora para la fecha #{opts[:fecha]}#{Color::RESET}" puts " Usa #{Color::DIM}adn db bitacora:listar#{Color::RESET} para ver bitácoras existentes" end end end # Comando: bitacora:listar [OPCIONES] - Listar bitácoras existentes def comando_bitacora_listar(args) opts = { limit: 10, offset: 0, desde: nil, hasta: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db bitacora:listar [OPCIONES]" opt.on("--limit N", Integer, "Número máximo de resultados (default: 10)") do |n| opts[:limit] = n end opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n| opts[:offset] = n end opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d| opts[:desde] = d end opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h| opts[:hasta] = h end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📋 Listando bitácoras...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| bitacoras = db.list_bitacoras( limit: opts[:limit], offset: opts[:offset], desde: opts[:desde], hasta: opts[:hasta] ) if bitacoras.empty? puts "#{Color::YELLOW}⚠ No se encontraron bitácoras#{Color::RESET}" return end puts "" puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Resumen".ljust(40)} #{"Creada".ljust(20)}#{Color::RESET}" puts "#{"─" * 5} #{"─" * 12} #{"─" * 40} #{"─" * 20}" bitacoras.each do |b| fecha = b['fecha'] resumen = b['resumen'] || '' resumen = resumen[0..37] + '...' if resumen.length > 40 creada = Time.parse(b['created_at']).strftime('%Y-%m-%d %H:%M') puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} #{resumen.ljust(40)} #{creada.ljust(20)}" end puts "" puts "Total: #{bitacoras.length} bitácoras" end end # Comando: evento:crear [OPCIONES] - Crear nueva entrada cronológica def comando_evento_crear(args) opts = { inicio: nil, descripcion: nil, fin: nil, estado: '⏳', modo: 'P', es_ia: false, nodo: nil, tema: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db evento:crear [OPCIONES]" opt.on("--inicio HH:MM", "Hora de inicio (requerido)") do |i| opts[:inicio] = i end opt.on("--descripcion TEXTO", "Descripción de la actividad (requerido)") do |d| opts[:descripcion] = d end opt.on("--fin HH:MM", "Hora de fin (opcional)") do |f| opts[:fin] = f end opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| opts[:estado] = e end opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") do |m| opts[:modo] = m end opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n| opts[:nodo] = n end opt.on("--ia", "Marcar como generado por IA") do opts[:es_ia] = true end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end # Validaciones básicas unless opts[:inicio] && opts[:descripcion] puts "#{Color::RED}✗ Error: --inicio y --descripcion son requeridos#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📝 Creando nueva entrada...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| # Buscar nodo por nombre si se especificó nodo_id = nil if opts[:nodo] nodo = db.find_nodo_by_nombre(opts[:nodo]) if nodo nodo_id = nodo['id'] puts " #{Color::DIM}Nodo encontrado: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}" puts " Creando entrada sin nodo asociado" end end # Buscar bitácora para hoy (o crearla) fecha_hoy = Date.today.to_s bitacora = db.find_bitacora_by_fecha(fecha_hoy) if bitacora.nil? puts " #{Color::DIM}Creando bitácora para hoy...#{Color::RESET}" bitacora = db.create_bitacora(fecha: fecha_hoy) end bitacora_id = bitacora['id'] # Crear la entrada entrada = db.create_entrada( inicio: opts[:inicio], descripcion: opts[:descripcion], fin: opts[:fin], estado: opts[:estado], modo: opts[:modo], es_ia: opts[:es_ia], bitacora_id: bitacora_id, nodo_id: nodo_id, tema_id: nil # Por ahora sin tema ) puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}" puts " #{Color::BOLD}Inicio:#{Color::RESET} #{entrada['inicio']}" puts " #{Color::BOLD}Fin:#{Color::RESET} #{entrada['fin'] || '(sin fin)'}" puts " #{Color::BOLD}Descripción:#{Color::RESET} #{entrada['descripcion']}" puts " #{Color::BOLD}Estado:#{Color::RESET} #{entrada['estado']}" puts " #{Color::BOLD}Modo:#{Color::RESET} #{entrada['modo'] == 'P' ? 'Presencial' : 'Remoto'}" puts " #{Color::BOLD}IA:#{Color::RESET} #{entrada['es_ia'] ? 'Sí' : 'No'}" puts " #{Color::BOLD}Bitácora:#{Color::RESET} #{fecha_hoy} (ID: #{bitacora_id})" end end # Comando: evento:listar [OPCIONES] - Listar entradas con filtros def comando_evento_listar(args) opts = { limit: 20, offset: 0, nodo: nil, estado: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db evento:listar [OPCIONES]" opt.on("--limit N", Integer, "Número máximo de resultados (default: 20)") do |n| opts[:limit] = n end opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n| opts[:offset] = n end opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n| opts[:nodo] = n end opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e| opts[:estado] = e end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}📋 Listando entradas...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| # Obtener ID de nodo si se especificó nombre nodo_id = nil if opts[:nodo] nodo = db.find_nodo_by_nombre(opts[:nodo]) if nodo nodo_id = nodo['id'] puts " #{Color::DIM}Filtrando por nodo: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}" else puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}" end end entradas = db.list_entradas( nodo_id: nodo_id, estado: opts[:estado], limit: opts[:limit], offset: opts[:offset] ) if entradas.empty? puts "#{Color::YELLOW}⚠ No se encontraron entradas#{Color::RESET}" return end puts "" puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Fin".ljust(8)} #{"Nodo".ljust(15)} #{"Estado".ljust(3)} #{"Descripción".ljust(40)}#{Color::RESET}" puts "#{"─" * 5} #{"─" * 8} #{"─" * 8} #{"─" * 15} #{"─" * 3} #{"─" * 40}" entradas.each do |e| id = e['id'].to_s inicio = e['inicio'][0..7] # HH:MM:SS -> HH:MM fin = e['fin'] ? e['fin'][0..7] : '--:--' nodo = e['nodo_nombre'] || '(sin nodo)' nodo = nodo[0..13] + '...' if nodo.length > 15 estado = e['estado'] desc = e['descripcion'] desc = desc[0..37] + '...' if desc.length > 40 puts "#{id.ljust(5)} #{inicio.ljust(8)} #{fin.ljust(8)} #{nodo.ljust(15)} #{estado.ljust(3)} #{desc.ljust(40)}" end puts "" puts "Total: #{entradas.length} entradas" end end # Comando: nodo:crear [OPCIONES] - Crear nuevo nodo def comando_nodo_crear(args) opts = { nombre: nil, ip: nil, tipo: 'servidor', descripcion: nil, activo: true } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db nodo:crear [OPCIONES]" opt.on("--nombre NOMBRE", "Nombre único del nodo (requerido)") do |n| opts[:nombre] = n end opt.on("--ip DIRECCION", "Dirección IP (opcional)") do |i| opts[:ip] = i end opt.on("--tipo TIPO", "Tipo (servidor, hipervisor, vm, pc)") do |t| opts[:tipo] = t end opt.on("--descripcion TEXTO", "Descripción del nodo") do |d| opts[:descripcion] = d end opt.on("--inactivo", "Crear nodo como inactivo") do opts[:activo] = false end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end unless opts[:nombre] puts "#{Color::RED}✗ Error: --nombre es requerido#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}🖥️ Creando nodo '#{opts[:nombre]}'...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| begin nodo = db.create_nodo( nombre: opts[:nombre], ip: opts[:ip], tipo: opts[:tipo], descripcion: opts[:descripcion], activo: opts[:activo] ) puts "#{Color::GREEN}✅ Nodo creado exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}" puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}" puts " #{Color::BOLD}IP:#{Color::RESET} #{nodo['ip'] || '(sin IP)'}" puts " #{Color::BOLD}Tipo:#{Color::RESET} #{nodo['tipo']}" puts " #{Color::BOLD}Activo:#{Color::RESET} #{nodo['activo'] ? 'Sí' : 'No'}" puts " #{Color::BOLD}Descripción:#{Color::RESET} #{nodo['descripcion'] || '(sin descripción)'}" puts " #{Color::BOLD}Creado:#{Color::RESET} #{nodo['created_at']}" rescue PG::UniqueViolation puts "#{Color::YELLOW}⚠ Ya existe un nodo con el nombre '#{opts[:nombre]}'#{Color::RESET}" puts " Usa #{Color::DIM}adn db nodo:listar#{Color::RESET} para ver nodos existentes" rescue => e puts "#{Color::RED}✗ Error creando nodo: #{e.message}#{Color::RESET}" end end end # Comando: nodo:listar [OPCIONES] - Listar nodos del sistema def comando_nodo_listar(args) opts = { activo: nil, tipo: nil } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db nodo:listar [OPCIONES]" opt.on("--activo", "Mostrar solo nodos activos") do opts[:activo] = true end opt.on("--inactivo", "Mostrar solo nodos inactivos") do opts[:activo] = false end opt.on("--tipo TIPO", "Filtrar por tipo (servidor, hipervisor, vm, pc)") do |t| opts[:tipo] = t end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}🖥️ Listando nodos...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], limit: 100) if nodos.empty? puts "#{Color::YELLOW}⚠ No se encontraron nodos#{Color::RESET}" return end puts "" puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}" puts "#{"─" * 5} #{"─" * 20} #{"─" * 15} #{"─" * 12} #{"─" * 6} #{"─" * 30}" nodos.each do |n| id = n['id'].to_s nombre = n['nombre'] ip = n['ip'] || '(sin IP)' tipo = n['tipo'] activo = n['activo'] ? '✅' : '❌' desc = n['descripcion'] || '' desc = desc[0..27] + '...' if desc.length > 30 puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}" end puts "" puts "Total: #{nodos.length} nodos" # Estadísticas por tipo tipos = nodos.group_by { |n| n['tipo'] } puts "#{Color::DIM}Tipos: #{tipos.map { |t, ns| "#{t}: #{ns.length}" }.join(', ')}#{Color::RESET}" end end # Comando: migrar:md [FECHA] - Migrar bitácora .md a base de datos def comando_migrar_md(args) opts = { sobrescribir: false, crear_nodos: true, todo: false } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db migrar:md [FECHA] [OPCIONES]" opt.on("--sobrescribir", "Sobrescribir bitácora si ya existe") do opts[:sobrescribir] = true end opt.on("--no-crear-nodos", "No crear nodos automáticamente") do opts[:crear_nodos] = false end opt.on("--todo", "Importar todas las bitácoras del directorio") do opts[:todo] = true end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end # Si se especificó --todo, importar todas las bitácoras if opts[:todo] importar_todas_bitacoras(opts) return end # Si no quedan argumentos, mostrar ayuda if args.empty? puts "#{Color::YELLOW}⚠ Se requiere fecha o --todo#{Color::RESET}" puts parser exit 1 end fecha_arg = args[0] begin fecha = Date.parse(fecha_arg) rescue Date::Error puts "#{Color::RED}✗ Formato de fecha inválido: #{fecha_arg}#{Color::RESET}" puts " Usa formato YYYY-MM-DD (ej: 2026-03-07)" exit 1 end archivo_md = File.join(ADN::BITACORAS_DIR, "#{fecha}.md") unless File.exist?(archivo_md) puts "#{Color::RED}✗ Archivo de bitácora no encontrado: #{archivo_md}#{Color::RESET}" puts " Verifica que la bitácora .md existe en #{ADN::BITACORAS_DIR}/" exit 1 end puts "#{Color::CYAN}🔄 Migrando bitácora #{fecha} de .md a base de datos...#{Color::RESET}" puts " #{Color::DIM}Opciones: sobrescribir=#{opts[:sobrescribir]}, crear_nodos=#{opts[:crear_nodos]}#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| begin # Crear instancia del importador importer = BitacorasDB::Importers::MDImporter.new(db, @logger) # Importar bitácora resultado = importer.importar_bitacora( archivo_md, sobrescribir: opts[:sobrescribir], crear_nodos: opts[:crear_nodos] ) mostrar_resultado_migracion(resultado, fecha) rescue => e puts "#{Color::RED}✗ Error durante migración: #{e.message}#{Color::RESET}" puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] end end end # Comando: estadisticas - Mostrar estadísticas del sistema def comando_estadisticas puts "#{Color::CYAN}📊 Estadísticas del sistema dtic-BITACORAs#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| stats = db.statistics puts "" puts "#{Color::BOLD}📈 Conteos de tablas:#{Color::RESET}" puts " #{Color::DIM}Nodos:#{Color::RESET} #{stats[:nodos] || 0}" puts " #{Color::DIM}Bitácoras:#{Color::RESET} #{stats[:bitacoras] || 0}" puts " #{Color::DIM}Temas:#{Color::RESET} #{stats[:temas] || 0}" puts " #{Color::DIM}Entradas:#{Color::RESET} #{stats[:entradas] || 0}" if stats[:nodos_con_entradas] puts "" puts "#{Color::BOLD}📊 Estadísticas de actividad:#{Color::RESET}" puts " #{Color::DIM}Nodos con entradas:#{Color::RESET} #{stats[:nodos_con_entradas]}" puts " #{Color::DIM}Días con actividad:#{Color::RESET} #{stats[:dias_con_actividad]}" puts " #{Color::DIM}Entradas generadas por IA:#{Color::RESET} #{stats[:entradas_ia]}" puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{stats[:entradas_completadas]}" end # Calcular porcentajes total_entradas = stats[:entradas] || 0 if total_entradas > 0 porcentaje_ia = (stats[:entradas_ia] || 0) * 100 / total_entradas porcentaje_completadas = (stats[:entradas_completadas] || 0) * 100 / total_entradas puts "" puts "#{Color::BOLD}📈 Porcentajes:#{Color::RESET}" puts " #{Color::DIM}Entradas IA:#{Color::RESET} #{porcentaje_ia}%" puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{porcentaje_completadas}%" end end def comando_backup(args) opts = { output: nil, compress: true, timestamp: true } parser = OptionParser.new do |opt| opt.banner = "Uso: adn db backup [OPCIONES]" opt.on("--output FILE", "Archivo de salida (default: backup_YYYYMMDD_HHMMSS.sql)") do |file| opts[:output] = file end opt.on("--no-compress", "No comprimir el archivo de backup") do opts[:compress] = false end opt.on("--no-timestamp", "No agregar timestamp al nombre del archivo") do opts[:timestamp] = false end opt.on("-h", "--help", "Mostrar ayuda") do puts opt exit end end begin parser.parse!(args) rescue OptionParser::InvalidOption => e puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}💾 Creando backup de la base de datos dtic-BITACORAs...#{Color::RESET}" puts " #{Color::DIM}Compresión: #{opts[:compress]}, Timestamp: #{opts[:timestamp]}#{Color::RESET}" # Cargar configuración de base de datos config_path = File.join(__dir__, '..', '..', 'config', 'database.yml') unless File.exist?(config_path) puts "#{Color::RED}✗ Archivo de configuración no encontrado: #{config_path}#{Color::RESET}" exit 1 end begin config = YAML.load_file(config_path) db_config = config['development'] || config.first[1] # Tomar primer entorno si no hay development host = db_config['host'] || 'localhost' port = db_config['port'] || 5433 database = db_config['database'] username = db_config['username'] password = db_config['password'] unless database && username puts "#{Color::RED}✗ Configuración de base de datos incompleta#{Color::RESET}" exit 1 end # Generar nombre de archivo timestamp = Time.now.strftime("%Y%m%d_%H%M%S") base_name = "dtic_bitacoras_backup_#{timestamp}" sql_file = opts[:output] || "#{base_name}.sql" final_file = opts[:compress] ? "#{sql_file}.gz" : sql_file puts " #{Color::DIM}Archivo de salida: #{final_file}#{Color::RESET}" # Construir comando pg_dump dump_cmd = "PGPASSWORD=\"#{password}\" pg_dump" dump_cmd += " -h #{host} -p #{port} -U #{username} -d #{database}" dump_cmd += " --schema=bitacoras" # Solo esquema bitacoras dump_cmd += " --clean --if-exists" # Incluir DROP statements dump_cmd += " --no-owner --no-privileges" # No incluir permisos dump_cmd += " --inserts" # Usar INSERT en lugar de COPY if opts[:compress] dump_cmd += " | gzip -c > \"#{final_file}\"" else dump_cmd += " > \"#{final_file}\"" end # Ejecutar backup puts " #{Color::DIM}Ejecutando pg_dump...#{Color::RESET}" system(dump_cmd) if $?.success? file_size = File.size(final_file) rescue 0 size_str = file_size > 0 ? " (#{file_size / 1024} KB)" : "" puts "#{Color::GREEN}✅ Backup creado exitosamente: #{final_file}#{size_str}#{Color::RESET}" puts " #{Color::DIM}Para restaurar:#{Color::RESET} gunzip -c #{final_file} | psql -h #{host} -p #{port} -U #{username} -d #{database}" else puts "#{Color::RED}✗ Error creando backup#{Color::RESET}" # Limpiar archivo parcial si existe File.delete(final_file) if File.exist?(final_file) exit 1 end rescue => e puts "#{Color::RED}✗ Error durante backup: #{e.message}#{Color::RESET}" puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] exit 1 end end end # Importa todas las bitácoras del directorio bitacoras/ def importar_todas_bitacoras(opciones) puts "#{Color::CYAN}🔄 Migrando todas las bitácoras .md a base de datos...#{Color::RESET}" puts " #{Color::DIM}Opciones: sobrescribir=#{opciones[:sobrescribir]}, crear_nodos=#{opciones[:crear_nodos]}#{Color::RESET}" puts "" BitacorasDB::BitacoraDB.with_connection do |db| # Crear instancia del importador importer = BitacorasDB::Importers::MDImporter.new(db, @logger) # Importar directorio completo resultado = importer.importar_directorio( ADN::BITACORAS_DIR, sobrescribir: opciones[:sobrescribir], crear_nodos: opciones[:crear_nodos] ) # Mostrar resultados consolidados totales = resultado[:totales] estadisticas = resultado[:estadisticas] puts "" puts "#{Color::BOLD}📊 Resultado de migración masiva:#{Color::RESET}" puts " #{Color::DIM}Archivos procesados:#{Color::RESET} #{totales[:archivos]}" puts " #{Color::DIM}Migraciones exitosas:#{Color::RESET} #{totales[:exitosos]}" puts " #{Color::DIM}Omitidos (ya existían):#{Color::RESET} #{totales[:omitidos]}" puts " #{Color::DIM}Errores:#{Color::RESET} #{totales[:errores]}" if estadisticas[:errores].any? puts "" puts "#{Color::YELLOW}⚠ Errores encontrados:#{Color::RESET}" estadisticas[:errores].each_with_index do |error, idx| puts " #{idx + 1}. #{error}" end end puts "" puts "#{Color::BOLD}📈 Estadísticas acumuladas:#{Color::RESET}" puts " #{Color::DIM}Bitácoras creadas:#{Color::RESET} #{estadisticas[:bitacoras_creadas]}" puts " #{Color::DIM}Nodos creados:#{Color::RESET} #{estadisticas[:nodos_creados]}" puts " #{Color::DIM}Temas creados:#{Color::RESET} #{estadisticas[:temas_creados]}" puts " #{Color::DIM}Entradas creadas:#{Color::RESET} #{estadisticas[:entradas_creadas]}" if totales[:errores] > 0 puts "" puts "#{Color::YELLOW}⚠ Algunas bitácoras no se migraron correctamente#{Color::RESET}" puts " Revisa los errores arriba y corrige los problemas" elsif totales[:exitosos] > 0 puts "" puts "#{Color::GREEN}✅ Migración masiva completada exitosamente#{Color::RESET}" end end rescue => e puts "#{Color::RED}✗ Error durante migración masiva: #{e.message}#{Color::RESET}" puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG'] end # Muestra el resultado de una migración individual def mostrar_resultado_migracion(resultado, fecha) if resultado[:exitoso] puts "#{Color::GREEN}✅ Bitácora migrada exitosamente#{Color::RESET}" puts "" puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" puts " #{Color::BOLD}Bitácora ID:#{Color::RESET} #{resultado[:bitacora_id]}" if resultado[:bitacora_id] puts " #{Color::BOLD}Estadísticas:#{Color::RESET}" puts " • Nodos creados: #{resultado.dig(:estadisticas, :nodos_creados) || 0}" puts " • Temas creados: #{resultado.dig(:estadisticas, :temas_creados) || 0}" puts " • Entradas creadas: #{resultado.dig(:estadisticas, :entradas_creadas) || 0}" elsif resultado[:skip] puts "#{Color::YELLOW}⚠ Bitácora ya existe, omitiendo#{Color::RESET}" puts " Usa --sobrescribir para forzar migración" puts " Bitácora ID: #{resultado[:bitacora_id]}" if resultado[:bitacora_id] else puts "#{Color::RED}✗ Error migrando bitácora#{Color::RESET}" if resultado[:error] puts " #{resultado[:error]}" end if resultado[:errores]&.any? resultado[:errores].each do |error| puts " • #{error}" end end end end # ─── MÉTODOS AUXILIARES ────────────────────────────────────────────── # Carga la configuración desde config.yml def cargar_configuracion config_path = File.join(__dir__, 'config.yml') return {} unless File.exist?(config_path) begin YAML.load_file(config_path) rescue => e @logger.error("Error cargando configuración: #{e.message}") {} end end # Muestra la ayuda general del subcomando def mostrar_ayuda puts "#{Color::BOLD}Uso: ./tools/adn/run db [SUBCOMANDO] [OPCIONES]#{Color::RESET}" puts "" puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}" puts "" puts " #{Color::CYAN}salud:bd#{Color::RESET} Verificar conexión y estado de la BD" puts " #{Color::CYAN}backup [OPCIONES]#{Color::RESET} Crear backup de la base de datos" puts " #{Color::CYAN}bitacora:crear [FECHA]#{Color::RESET} Crear nueva bitácora diaria" puts " #{Color::CYAN}bitacora:listar [OPCIONES]#{Color::RESET} Listar bitácoras existentes" puts " #{Color::CYAN}evento:crear [OPCIONES]#{Color::RESET} Crear nueva entrada cronológica" puts " #{Color::CYAN}evento:listar [OPCIONES]#{Color::RESET} Listar entradas con filtros" puts " #{Color::CYAN}nodo:crear [OPCIONES]#{Color::RESET} Crear nuevo nodo (servidor/VM/PC)" puts " #{Color::CYAN}nodo:listar [OPCIONES]#{Color::RESET} Listar nodos del sistema" puts " #{Color::CYAN}migrar:md [FECHA]#{Color::RESET} Migrar bitácora .md a base de datos" puts " #{Color::CYAN}estadisticas#{Color::RESET} Mostrar estadísticas del sistema" puts "" puts "#{Color::BOLD}Ejemplos:#{Color::RESET}" puts " ./tools/adn/run db salud:bd" puts " ./tools/adn/run db bitacora:crear --resumen \"Inicio de proyecto\"" puts " ./tools/adn/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8" puts " ./tools/adn/run db nodo:listar --tipo servidor" puts "" puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)" puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}" end end end