module ADN module SubcomandoDBMisc # 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 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 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 def comando_migrar_sql(args) opts = {} parser = OptionParser.new do |opt| opt.banner = "Uso: adn db migrar:sql ARCHIVO_SQL" 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 sql_file = args.first if sql_file.nil? || !File.exist?(sql_file) puts "#{Color::RED}✗ Error: Archivo SQL no especificado o no encontrado.#{Color::RESET}" puts parser exit 1 end puts "#{Color::CYAN}🔄 Ejecutando script SQL: #{sql_file}...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| begin sql = File.read(sql_file) db.send(:execute, sql) puts "#{Color::GREEN}✅ Script SQL ejecutado exitosamente.#{Color::RESET}" rescue => e puts "#{Color::RED}✗ Error ejecutando SQL: #{e.message}#{Color::RESET}" exit 1 end end end # Comando: archivar:md - Mover bitácoras y proyectos MD importados/obsoletos a carpetas _hist def comando_archivar_md require 'fileutils' puts "#{Color::CYAN}🧹 Iniciando limpieza y archivado de archivos Markdown...#{Color::RESET}" BitacorasDB::BitacoraDB.with_connection do |db| bitacoras_dir = defined?(ADN::BITACORAS_DIR) ? ADN::BITACORAS_DIR : File.expand_path('../../../docs/bitacoras', __dir__) proyectos_dir = File.expand_path('../../../docs/proyectos', __dir__) hist_bitacoras = File.join(bitacoras_dir, '_hist') hist_proyectos = File.join(proyectos_dir, '_hist') FileUtils.mkdir_p(hist_bitacoras) FileUtils.mkdir_p(hist_proyectos) # 1. Archivar Bitácoras Diarias puts "\\n#{Color::BOLD}>> Verificando Bitácoras Diarias...#{Color::RESET}" bitacoras_db = db.send(:execute, "SELECT fecha FROM bitacoras.bitacoras").map { |row| row['fecha'] } Dir.glob(File.join(bitacoras_dir, '*.md')).each do |md_path| next if File.basename(md_path) == 'README.md' fecha_md = File.basename(md_path, '.md') if bitacoras_db.include?(fecha_md) dest = File.join(hist_bitacoras, File.basename(md_path)) FileUtils.mv(md_path, dest) puts " #{Color::GREEN}✓ Archivada:#{Color::RESET} #{fecha_md}.md" else puts " #{Color::YELLOW}⚠ No en DB (se omite):#{Color::RESET} #{fecha_md}.md" end end # 2. Archivar Proyectos puts "\\n#{Color::BOLD}>> Verificando Proyectos MD...#{Color::RESET}" proyectos_db = db.send(:execute, "SELECT codigo FROM bitacoras.proyectos").map { |row| row['codigo'] } Dir.glob(File.join(proyectos_dir, '*.md')).each do |md_path| next if File.basename(md_path) == 'README.md' # Extraer el código PXXXX del principio del archivo, asumiendo formato PXXXX_Titulo.md o PXXXX.md codigo_match = File.basename(md_path).match(/^(P\d+)/) if codigo_match && proyectos_db.include?(codigo_match[1]) dest = File.join(hist_proyectos, File.basename(md_path)) FileUtils.mv(md_path, dest) puts " #{Color::GREEN}✓ Archivado Proyecto:#{Color::RESET} #{File.basename(md_path)}" else puts " #{Color::YELLOW}⚠ Proyecto no registrado en DB o formato no válido:#{Color::RESET} #{File.basename(md_path)}" end end puts "\\n#{Color::GREEN}✅ Limpieza Markdown finalizada. Entorno 100% DB-First fortalecido.#{Color::RESET}" end end end end