Reestructuración modular integral de servicios, reorganización de gestión en docs/ y limpieza profunda del repositorio
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
|
||||
# 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}
|
||||
Reference in New Issue
Block a user