# frozen_string_literal: true # tools/adn/subcomando_sync.rb — Subcomando para sincronizar archivos MD con API dtic-BITACORAs # ============================================================================================= # Este subcomando permite sincronizar bitácoras en formato Markdown con la base de datos # de dtic-BITACORAs mediante la API REST. require_relative 'clients/dtic_bitacoras_api' require 'optparse' require 'fileutils' module ADN class SubcomandoSync def initialize(args, logger) @args = args @logger = logger @opciones = { sobrescribir: false, validar_primero: true, batch: false, fecha: nil, ruta: nil } @resultados = { archivos_procesados: 0, archivos_exitosos: 0, archivos_fallidos: 0, entradas_sincronizadas: 0, errores: [] } end def ejecutar parser = OptionParser.new do |opts| opts.banner = "Uso: ruby tools/adn.rb sync [OPCIONES] [RUTA_O_FECHA]" opts.separator "" opts.separator "Sincroniza archivos Markdown con la API de dtic-BITACORAs" opts.separator "" opts.separator "Opciones:" opts.on("--sobrescribir", "Sobrescribir entradas existentes en la base de datos") do @opciones[:sobrescribir] = true end opts.on("--no-validar", "No validar archivos antes de sincronizar") do @opciones[:validar_primero] = false end opts.on("--batch", "Sincronizar todos los archivos en el directorio bitacoras/") do @opciones[:batch] = true end opts.on("--fecha FECHA", "Sincronizar bitácora específica (formato: YYYY-MM-DD)") do |f| @opciones[:fecha] = f end opts.on("-h", "--help", "Mostrar esta ayuda") do puts opts exit end end begin parser.parse!(@args) rescue OptionParser::InvalidOption => e @logger.error("Opción inválida: #{e.message}") puts parser exit 1 end # Determinar qué sincronizar if @args.any? && !@opciones[:batch] && !@opciones[:fecha] @opciones[:ruta] = @args[0] end ejecutar_sincronizacion end private def ejecutar_sincronizacion @logger.info("Iniciando sincronización con API dtic-BITACORAs", @opciones) # Inicializar cliente API begin @client = DticBitacorasApi::Client.new(logger: @logger) salud = @client.salud @logger.exito("API dtic-BITACORAs disponible: #{salud['status']}") rescue DticBitacorasApi::ApiError => e @logger.error("No se puede conectar a la API: #{e.message}") mostrar_conexion_fallback exit 1 rescue DticBitacorasApi::ConfigError => e @logger.error("Error de configuración: #{e.message}") puts "Configura la URL de la API en tools/adn/config.yml o con export DTIC_API_URL=http://localhost:3002" exit 1 end # Determinar archivos a sincronizar archivos = determinar_archivos_a_sincronizar if archivos.empty? @logger.advertencia("No se encontraron archivos para sincronizar") mostrar_estadisticas exit 0 end puts "\n#{Color::CYAN}🔄 Sincronizando #{archivos.size} archivo(s) con dtic-BITACORAs#{Color::RESET}" puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" # Procesar cada archivo archivos.each_with_index do |archivo, index| procesar_archivo(archivo, index + 1, archivos.size) end mostrar_estadisticas end def determinar_archivos_a_sincronizar archivos = [] if @opciones[:batch] # Sincronizar todos los archivos en bitacoras/ patron = File.join(ADN::BITACORAS_DIR, "*.md") archivos = Dir.glob(patron).sort elsif @opciones[:fecha] # Sincronizar archivo específico por fecha ruta = File.join(ADN::BITACORAS_DIR, "#{@opciones[:fecha]}.md") archivos = File.exist?(ruta) ? [ruta] : [] elsif @opciones[:ruta] # Sincronizar archivo específico por ruta archivos = [File.expand_path(@opciones[:ruta])] else # Sincronizar archivo del día actual fecha_hoy = Date.today.strftime('%Y-%m-%d') ruta_hoy = File.join(ADN::BITACORAS_DIR, "#{fecha_hoy}.md") archivos = File.exist?(ruta_hoy) ? [ruta_hoy] : [] end archivos end def procesar_archivo(ruta_archivo, indice, total) nombre = File.basename(ruta_archivo) puts "\n#{Color::CYAN}[#{indice}/#{total}] #{nombre}#{Color::RESET}" unless File.exist?(ruta_archivo) @resultados[:errores] << "#{nombre}: Archivo no encontrado" @resultados[:archivos_fallidos] += 1 puts " #{Color::RED}✗ Archivo no encontrado#{Color::RESET}" return end # Extraer fecha del nombre del archivo fecha_match = nombre.match(/(\d{4}-\d{2}-\d{2})\.md/) unless fecha_match @resultados[:errores] << "#{nombre}: Formato de nombre inválido (debe ser YYYY-MM-DD.md)" @resultados[:archivos_fallidos] += 1 puts " #{Color::RED}✗ Formato de nombre inválido#{Color::RESET}" return end fecha = fecha_match[1] # Validar formato del archivo si está habilitado if @opciones[:validar_primero] unless validar_archivo(ruta_archivo) @resultados[:errores] << "#{nombre}: Falló validación previa" @resultados[:archivos_fallidos] += 1 return end end # Sincronizar con la API begin resultado = @client.sincronizar_md(ruta_archivo, sobrescribir: @opciones[:sobrescribir]) @resultados[:archivos_procesados] += 1 @resultados[:archivos_exitosos] += 1 @resultados[:entradas_sincronizadas] += resultado[:entradas_procesadas] puts " #{Color::GREEN}✓ Sincronizado exitosamente#{Color::RESET}" puts " #{Color::DIM}Bitácora ID: #{resultado[:bitacora_id]}#{Color::RESET}" puts " #{Color::DIM}Entradas: #{resultado[:entradas_procesadas]}#{Color::RESET}" @logger.info("Archivo sincronizado: #{nombre}", resultado) rescue DticBitacorasApi::ApiError => e @resultados[:archivos_procesados] += 1 @resultados[:archivos_fallidos] += 1 @resultados[:errores] << "#{nombre}: #{e.message}" puts " #{Color::RED}✗ Error de API: #{e.message}#{Color::RESET}" @logger.error("Error sincronizando #{nombre}: #{e.message}") rescue => e @resultados[:archivos_procesados] += 1 @resultados[:archivos_fallidos] += 1 @resultados[:errores] << "#{nombre}: Error inesperado - #{e.message}" puts " #{Color::RED}✗ Error inesperado: #{e.message}#{Color::RESET}" @logger.error("Error inesperado en #{nombre}: #{e.message}", backtrace: e.backtrace) end end def validar_archivo(ruta_archivo) puts " #{Color::CYAN}🔍 Validando formato...#{Color::RESET}" contenido = File.read(ruta_archivo, encoding: 'UTF-8') errores_validacion = [] # Validar formato básico unless contenido.include?('## 📝 Actividades Detalladas') errores_validacion << "Falta sección '## 📝 Actividades Detalladas'" end # Validar que tenga al menos una tabla de entradas unless contenido.match(/\|\s*[I]\s*\|\s*[F]\s*\|\s*Descripción\s*\|\s*[E]\s*\|/) errores_validacion << "No se encontró tabla de entradas I-F-D-E" end # Validar formato de tiempo if contenido.match(/\d{1,2}:\d{2}\s*hs/) errores_validacion << "Formato de tiempo incorrecto (contiene 'hs')" end if errores_validacion.empty? puts " #{Color::GREEN}✓ Validación exitosa#{Color::RESET}" true else puts " #{Color::YELLOW}⚠ Advertencias de validación:#{Color::RESET}" errores_validacion.each { |e| puts " - #{e}" } puts " #{Color::DIM}Continuando con sincronización...#{Color::RESET}" true # Continuar a pesar de advertencias end end def mostrar_estadisticas puts "\n#{Color::CYAN}📊 Resultados de sincronización#{Color::RESET}" puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" puts " #{Color::GREEN}Archivos procesados:#{Color::RESET} #{@resultados[:archivos_procesados]}" puts " #{Color::GREEN}Archivos exitosos:#{Color::RESET} #{@resultados[:archivos_exitosos]}" if @resultados[:archivos_fallidos] > 0 puts " #{Color::RED}Archivos fallidos:#{Color::RESET} #{@resultados[:archivos_fallidos]}" else puts " #{Color::GREEN}Archivos fallidos:#{Color::RESET} 0" end puts " #{Color::GREEN}Entradas sincronizadas:#{Color::RESET} #{@resultados[:entradas_sincronizadas]}" # Mostrar estadísticas de la API mostrar_estadisticas_api # Mostrar errores si los hay unless @resultados[:errores].empty? puts "\n#{Color::YELLOW}⚠ Errores encontrados:#{Color::RESET}" @resultados[:errores].each do |error| puts " #{Color::RED}• #{error}#{Color::RESET}" end end # Resumen final if @resultados[:archivos_fallidos] == 0 @logger.exito("Sincronización completada exitosamente") puts "\n#{Color::GREEN}✅ Sincronización completada exitosamente#{Color::RESET}" else @logger.advertencia("Sincronización completada con errores") puts "\n#{Color::YELLOW}⚠ Sincronización completada con #{@resultados[:archivos_fallidos]} error(es)#{Color::RESET}" end @logger.info("Resumen de sincronización", @resultados) end def mostrar_estadisticas_api begin stats = @client.estadisticas puts "\n#{Color::CYAN}📈 Estadísticas de dtic-BITACORAs#{Color::RESET}" puts "#{Color::DIM}#{'-' * 40}#{Color::RESET}" puts " #{Color::GREEN}Nodos en sistema:#{Color::RESET} #{stats[:nodos]}" puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET}