From 427b000aacbc45183ce399c14986a24add0e63f1 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Tue, 10 Mar 2026 20:02:28 -0300 Subject: [PATCH] =?UTF-8?q?Refactor(ADN):=20Retiro=20de=20directorios=20y?= =?UTF-8?q?=20m=C3=B3dulos=20obsoletos=20seg=C3=BAn=20principio=20Menos=20?= =?UTF-8?q?es=20Mas=20(spec/,=20clientes/,=20sync.rb)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/adn/README.md | 3 - tools/adn/clientes/dtic_bitacoras_api.rb | 517 ----------------------- tools/adn/comando/ayuda.rb | 1 - tools/adn/comando/sync.rb | 282 ------------- tools/adn/run | 4 - tools/adn/spec/bitacora_db_test.rb | 104 ----- 6 files changed, 911 deletions(-) delete mode 100644 tools/adn/clientes/dtic_bitacoras_api.rb delete mode 100644 tools/adn/comando/sync.rb delete mode 100644 tools/adn/spec/bitacora_db_test.rb diff --git a/tools/adn/README.md b/tools/adn/README.md index 0974f28a..b9d3380b 100644 --- a/tools/adn/README.md +++ b/tools/adn/README.md @@ -40,9 +40,6 @@ El proyecto sigue un patrón **Modular y Dirigido por Eventos**: # Ver salud del sistema ./tools/adn/run salud --json - -# Sincronizar bitácoras con la API -./tools/adn/run sync ``` --- diff --git a/tools/adn/clientes/dtic_bitacoras_api.rb b/tools/adn/clientes/dtic_bitacoras_api.rb deleted file mode 100644 index 9997d644..00000000 --- a/tools/adn/clientes/dtic_bitacoras_api.rb +++ /dev/null @@ -1,517 +0,0 @@ -# frozen_string_literal: true - -# tools/adn/clients/dtic_bitacoras_api.rb — Cliente Ruby para API de dtic-BITACORAs -# =============================================================================== -# Este cliente permite interactuar con la API REST de dtic-BITACORAs desde Ruby. -# Proporciona métodos para operaciones CRUD en nodos, bitácoras, entradas y proyectos. -# -# Configuración: -# export DTIC_API_URL=http://localhost:3002 -# o configurar en tools/adn/config.yml -# -# Uso básico: -# require_relative 'clients/dtic_bitacoras_api' -# client = DticBitacorasApi::Client.new -# bitacoras = client.listar_bitacoras -# client.crear_entrada(...) -# -# Sincronización: -# client.sincronizar_md('bitacoras/2026-03-05.md') - -require 'net/http' -require 'json' -require 'uri' -require 'date' -require 'yaml' - -module DticBitacorasApi - # Excepciones personalizadas - class ApiError < StandardError - attr_reader :status_code, :response_body - - def initialize(message, status_code = nil, response_body = nil) - super(message) - @status_code = status_code - @response_body = response_body - end - - def to_s - msg = super - msg += " (Status: #{@status_code})" if @status_code - msg += "\nResponse: #{@response_body}" if @response_body - msg - end - end - - class ConfigError < StandardError; end - - # Cliente principal - class Client - # Inicializa el cliente con configuración - # - # @param options [Hash] Opciones de configuración - # @option options [String] :base_url URL base de la API (ej: http://localhost:3002) - # @option options [String] :config_path Ruta al archivo de configuración YAML - # @option options [Logger] :logger Instancia de logger (opcional) - # @option options [Integer] :timeout_seconds Timeout para requests HTTP (default: 30) - def initialize(options = {}) - @config_path = options[:config_path] || File.join(__dir__, '..', 'config.yml') - @base_url = options[:base_url] || extraer_url_desde_config - @logger = options[:logger] || ADN.logger rescue nil - @timeout_seconds = options[:timeout_seconds] || 30 - - # Validar URL base - @base_url = @base_url.chomp('/') - unless @base_url.start_with?('http://', 'https://') - raise ConfigError, "URL base debe comenzar con http:// o https://: #{@base_url}" - end - - @logger&.info("Cliente dtic-BITACORAs inicializado para #{@base_url}") - end - - # --- MÉTODOS PARA NODOS --- - - # Lista todos los nodos activos - # - # @return [Array] Lista de nodos - def listar_nodos - get('/api/nodos') - end - - # Obtiene un nodo por nombre - # - # @param nombre [String] Nombre del nodo - # @return [Hash] Datos del nodo - def obtener_nodo_por_nombre(nombre) - nodos = listar_nodos - nodos.find { |n| n['nombre'] == nombre } - end - - # Crea un nuevo nodo - # - # @param nombre [String] Nombre del nodo - # @param ip [String] Dirección IP - # @param tipo [String] Tipo de nodo (servidor, vm, pc, hipervisor) - # @param descripcion [String] Descripción opcional - # @return [Hash] Nodo creado - def crear_nodo(nombre, ip: nil, tipo: 'servidor', descripcion: '') - post('/api/nodos', { - nombre: nombre, - ip: ip, - tipo: tipo, - descripcion: descripcion - }) - end - - # --- MÉTODOS PARA BITÁCORAS --- - - # Lista las últimas bitácoras (por defecto 30 días) - # - # @param limit [Integer] Límite de resultados - # @return [Array] Lista de bitácoras - def listar_bitacoras(limit: 30) - get("/api/bitacoras?limit=#{limit}") - end - - # Obtiene o crea una bitácora para una fecha específica - # - # @param fecha [String, Date] Fecha en formato YYYY-MM-DD - # @return [Hash] Bitácora - def obtener_bitacora(fecha) - fecha_str = fecha.respond_to?(:strftime) ? fecha.strftime('%Y-%m-%d') : fecha.to_s - get("/api/bitacoras?fecha=#{fecha_str}") - end - - # Obtiene bitácora completa con entradas agrupadas por nodo - # - # @param fecha [String, Date] Fecha en formato YYYY-MM-DD - # @return [Hash] Bitácora completa con nodos y entradas - def obtener_bitacora_completa(fecha) - fecha_str = fecha.respond_to?(:strftime) ? fecha.strftime('%Y-%m-%d') : fecha.to_s - get("/api/bitacoras/#{fecha_str}/completa") - end - - # Exporta bitácora a formato Markdown - # - # @param fecha [String, Date] Fecha en formato YYYY-MM-DD - # @return [String] Contenido Markdown - def exportar_bitacora_md(fecha) - fecha_str = fecha.respond_to?(:strftime) ? fecha.strftime('%Y-%m-%d') : fecha.to_s - get_raw("/api/bitacoras/#{fecha_str}/export") - end - - # --- MÉTODOS PARA ENTRADAS --- - - # Lista entradas con filtros opcionales - # - # @param bitacora_id [Integer] ID de bitácora para filtrar - # @param nodo_id [Integer] ID de nodo para filtrar - # @return [Array] Lista de entradas - def listar_entradas(bitacora_id: nil, nodo_id: nil) - params = [] - params << "bitacora_id=#{bitacora_id}" if bitacora_id - params << "nodo_id=#{nodo_id}" if nodo_id - query = params.empty? ? '' : "?#{params.join('&')}" - get("/api/entradas#{query}") - end - - # Crea una nueva entrada - # - # @param inicio [String] Hora de inicio (HH:MM) - # @param fin [String, nil] Hora de fin (HH:MM) o nil si en curso - # @param descripcion [String] Descripción de la actividad - # @param bitacora_id [Integer] ID de la bitácora - # @param nodo_id [Integer] ID del nodo - # @param estado [String] Estado (✅, ⏳, ⚠️, ❌) - # @param modo [String] Modo (P: Presencial, R: Remoto) - # @param es_ia [Boolean] Si la acción fue realizada por IA - # @return [Hash] Entrada creada - def crear_entrada(inicio:, fin: nil, descripcion:, bitacora_id:, nodo_id:, - estado: '⏳', modo: 'P', es_ia: false) - post('/api/entradas', { - inicio: inicio, - fin: fin, - descripcion: descripcion, - estado: estado, - modo: modo, - es_ia: es_ia, - bitacora_id: bitacora_id, - nodo_id: nodo_id - }) - end - - # Actualiza una entrada existente - # - # @param id [Integer] ID de la entrada - # @param attributes [Hash] Atributos a actualizar - # @return [Hash] Entrada actualizada - def actualizar_entrada(id, attributes) - put("/api/entradas/#{id}", attributes) - end - - # --- MÉTODOS PARA PROYECTOS --- - - # Lista todos los proyectos - # - # @return [Array] Lista de proyectos - def listar_proyectos - get('/api/proyectos') - end - - # Obtiene un proyecto por código con detalles completos - # - # @param codigo [String] Código del proyecto (ej: P2601) - # @return [Hash] Proyecto con fases, hitos y métricas - def obtener_proyecto(codigo) - get("/api/proyectos/#{codigo}") - end - - # Crea un hito en un proyecto - # - # @param codigo_proyecto [String] Código del proyecto - # @param fase_numero [Integer] Número de fase - # @param id_hito [String] ID del hito (ej: SINC01) - # @param titulo [String] Título del hito - # @param options [Hash] Opciones adicionales - # @option options [String] :descripcion Descripción del hito - # @option options [String] :estado Estado del hito (✅, ⏳, 📍) - # @option options [String] :fecha Fecha del hito (YYYY-MM-DD) - # @option options [Integer] :horas_presencial Horas presenciales - # @option options [Integer] :horas_remoto Horas remotas - # @option options [String] :nodo_nombre Nombre del nodo asociado - # @return [Hash] Hito creado - def crear_hito(codigo_proyecto, fase_numero, id_hito, titulo, options = {}) - post("/api/proyectos/#{codigo_proyecto}/hitos", { - fase_numero: fase_numero, - id_hito: id_hito, - titulo: titulo, - descripcion: options[:descripcion] || '', - estado: options[:estado] || '⏳', - fecha: options[:fecha], - horas_presencial: options[:horas_presencial] || 0, - horas_remoto: options[:horas_remoto] || 0, - nodo_nombre: options[:nodo_nombre] - }) - end - - # --- SINCRONIZACIÓN DE ARCHIVOS MD --- - - # Sincroniza un archivo Markdown con la base de datos - # - # @param ruta_md [String] Ruta al archivo .md - # @param options [Hash] Opciones de sincronización - # @option options [Boolean] :sobrescribir Sobrescribir entradas existentes - # @return [Hash] Resultado de la sincronización - def sincronizar_md(ruta_md, options = {}) - unless File.exist?(ruta_md) - raise ApiError, "Archivo no encontrado: #{ruta_md}" - end - - contenido = File.read(ruta_md, encoding: 'UTF-8') - nombre_archivo = File.basename(ruta_md) - - # Extraer fecha del nombre del archivo - match = nombre_archivo.match(/(\d{4}-\d{2}-\d{2})\.md/) - unless match - raise ApiError, "Nombre de archivo no sigue formato YYYY-MM-DD.md: #{nombre_archivo}" - end - - fecha = match[1] - @logger&.info("Sincronizando bitácora: #{fecha} desde #{ruta_md}") - - # Obtener o crear bitácora - bitacora = obtener_bitacora(fecha) - bitacora_id = bitacora['id'] - - # Obtener mapeo de nodos por nombre - nodos = listar_nodos - nodo_map = {} - nodos.each { |n| nodo_map[n['nombre']] = n['id'] } - - # Si sobrescribir, eliminar entradas existentes - if options[:sobrescribir] - entradas = listar_entradas(bitacora_id: bitacora_id) - entradas.each do |entrada| - # Podríamos implementar delete si la API lo soporta - @logger&.debug("Entrada existente que sería sobrescrita: #{entrada['id']}") - end - end - - # Parsear contenido MD - entradas_encontradas = [] - nodo_actual_id = nil - en_seccion_detalladas = false - - contenido.each_line do |linea| - linea = linea.chomp - - # Detectar inicio de sección de actividades detalladas - if linea.include?('Actividades Detalladas') - en_seccion_detalladas = true - next - end - - next unless en_seccion_detalladas - - # Detectar cambio de nodo - if linea.start_with?('### ') - nombre_nodo = linea.sub('### ', '').strip.split(' ')[0] - nodo_actual_id = nodo_map[nombre_nodo] - @logger&.debug("Cambiando a nodo: #{nombre_nodo} (ID: #{nodo_actual_id})") if nodo_actual_id - next - end - - next unless nodo_actual_id - - # Parsear entrada de tabla I-F-D-E - entrada = parsear_linea_tabla(linea) - if entrada - entrada[:bitacora_id] = bitacora_id - entrada[:nodo_id] = nodo_actual_id - - begin - entrada_creada = crear_entrada(entrada) - entradas_encontradas << entrada_creada - @logger&.debug("Entrada creada: #{entrada[:inicio]} - #{entrada_creada['id']}") - rescue ApiError => e - @logger&.error("Error creando entrada: #{e.message}") - end - end - end - - { - fecha: fecha, - bitacora_id: bitacora_id, - entradas_procesadas: entradas_encontradas.size, - entradas: entradas_encontradas - } - end - - # --- MÉTODOS DE UTILIDAD --- - - # Verifica salud de la API - # - # @return [Hash] Estado de la API - def salud - get('/health') - end - - # Obtiene estadísticas del sistema - # - # @return [Hash] Estadísticas - def estadisticas - { - nodos: listar_nodos.size, - bitacoras: listar_bitacoras.size, - proyectos: listar_proyectos.size - } - end - - private - - # Extrae URL base desde archivo de configuración - def extraer_url_desde_config - return 'http://localhost:3002' unless File.exist?(@config_path) - - begin - config = YAML.load_file(@config_path) - - # Intentar obtener URL de dtic-BITACORAs - if config['dtic_bitacoras'] && config['dtic_bitacoras']['api_url'] - return config['dtic_bitacoras']['api_url'] - end - - # Fallback a puertos predeterminados - 'http://localhost:3002' - rescue => e - @logger&.error("Error cargando configuración: #{e.message}") - 'http://localhost:3002' - end - end - - # Parsear línea de tabla I-F-D-E - def parsear_linea_tabla(linea) - # Formato: | HH:MM | HH:MM | Descripción | [P/R] Estado | - regex = /\|\s*(\d{1,2}:\d{2})\s*\|\s*(\d{1,2}:\d{2}|\-)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|/ - match = linea.match(regex) - return nil unless match - - inicio = match[1] - fin = match[2] == '-' ? nil : match[2] - descripcion_raw = match[3] - estado_raw = match[4] - - # Detectar si es acción de IA - es_ia = descripcion_raw.include?('(IA)') - descripcion = descripcion_raw - .gsub('(IA)', '') - .gsub(/\*\*/, '') - .gsub(/__/, '') - .strip - - # Parsear modo y estado - modo = 'P' # Default presencial - estado = estado_raw - - if estado_raw.match(/\[([PR])\]\s*(.+)/) - modo = $1 - estado = $2 - end - - { - inicio: inicio, - fin: fin, - descripcion: descripcion, - estado: estado, - modo: modo, - es_ia: es_ia - } - end - - # Métodos HTTP genéricos - - def get(path) - request(:get, path) - end - - def get_raw(path) - request_raw(:get, path) - end - - def post(path, data) - request(:post, path, data) - end - - def put(path, data) - request(:put, path, data) - end - - def request(method, path, data = nil) - response = request_raw(method, path, data) - JSON.parse(response) - rescue JSON::ParserError => e - raise ApiError.new("Error parseando respuesta JSON: #{e.message}", nil, response) - end - - def request_raw(method, path, data = nil) - uri = URI.join(@base_url, path) - - http = Net::HTTP.new(uri.host, uri.port) - http.read_timeout = @timeout_seconds - http.open_timeout = @timeout_seconds - - request_class = case method - when :get then Net::HTTP::Get - when :post then Net::HTTP::Post - when :put then Net::HTTP::Put - when :delete then Net::HTTP::Delete - else raise "Método HTTP no soportado: #{method}" - end - - request = request_class.new(uri.request_uri) - request['Content-Type'] = 'application/json' - request['User-Agent'] = "dtic-bitacoras-ruby-client/1.0" - - request.body = data.to_json if data && !data.empty? - - @logger&.debug("#{method.upcase} #{uri} #{data ? "con datos: #{data.inspect}" : ''}") - - begin - response = http.request(request) - rescue Net::ReadTimeout => e - raise ApiError.new("Timeout después de #{@timeout_seconds} segundos: #{e.message}") - rescue SocketError, Errno::ECONNREFUSED => e - raise ApiError.new("No se puede conectar a #{@base_url}: #{e.message}") - end - - @logger&.debug("Respuesta: #{response.code} #{response.message}") - - unless response.is_a?(Net::HTTPSuccess) - error_message = "Error #{response.code}: #{response.message}" - begin - error_body = JSON.parse(response.body) - error_message += " - #{error_body['error']}" if error_body['error'] - rescue JSON::ParserError - error_message += " - #{response.body[0..100]}" - end - - raise ApiError.new(error_message, response.code, response.body) - end - - response.body - end - end - - # Helper para uso rápido - def self.client(options = {}) - @client ||= Client.new(options) - end -end - -# Si se ejecuta directamente como script -if __FILE__ == $0 - begin - client = DticBitacorasApi::Client.new - puts "🧪 Probando conexión a #{client.instance_variable_get(:@base_url)}" - - # Prueba de salud - salud = client.salud - puts "✅ API saludable: #{salud['status']}" - - # Listar nodos - nodos = client.listar_nodos - puts "📊 Nodos encontrados: #{nodos.size}" - - # Listar bitácoras - bitacoras = client.listar_bitacoras(limit: 5) - puts "📅 Últimas bitácoras: #{bitacoras.map { |b| b['fecha'] }.join(', ')}" - - rescue DticBitacorasApi::ApiError => e - puts "❌ Error de API: #{e.message}" - exit 1 - rescue DticBitacorasApi::ConfigError => e - puts "❌ Error de configuración: #{e.message}" - puts " Configura con: export DTIC_API_URL=http://localhost:3002" - exit 1 - end -end diff --git a/tools/adn/comando/ayuda.rb b/tools/adn/comando/ayuda.rb index f7588ab7..ef881fba 100644 --- a/tools/adn/comando/ayuda.rb +++ b/tools/adn/comando/ayuda.rb @@ -33,7 +33,6 @@ class SubcomandoAyuda generar nodo Generar ficha de nodo #{Color::YELLOW}generar proyecto #{Color::RESET} Generar manifiesto de proyecto #{Color::YELLOW}backup [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo - #{Color::YELLOW}sync [ruta|fecha]#{Color::RESET} Sincronizar con API dtic-BITACORAs #{Color::YELLOW}inicio de jornada [presencial|remoto]#{Color::RESET} Inicio de jornada laboral #{Color::YELLOW}cierre de jornada [presencial|remoto]#{Color::RESET} Cierre de jornada laboral #{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers diff --git a/tools/adn/comando/sync.rb b/tools/adn/comando/sync.rb deleted file mode 100644 index bfc58c45..00000000 --- a/tools/adn/comando/sync.rb +++ /dev/null @@ -1,282 +0,0 @@ - -# 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 '../clientes/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: ./tools/adn/run 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] || 0}" - puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET} #{stats[:bitacoras] || 0}" - rescue => e - puts " #{Color::RED}No se pudieron obtener estadísticas: #{e.message}#{Color::RESET}" - end - end - end -end diff --git a/tools/adn/run b/tools/adn/run index 4f45ebcf..86146cf1 100755 --- a/tools/adn/run +++ b/tools/adn/run @@ -46,11 +46,9 @@ require_relative 'core/logger' require_relative 'core/eventos' require_relative 'core/triggers' require_relative 'core/validador' -require_relative 'clientes/dtic_bitacoras_api' require_relative 'comando/db' require_relative 'comando/plan' require_relative 'comando/backup' -require_relative 'comando/sync' require_relative 'comando/triggers' require_relative 'comando/conocimiento' require_relative 'comando/cierre' @@ -103,8 +101,6 @@ class ADNCLI ADN::SubcomandoDB.new(args, @logger).ejecutar when 'backup', 'backups' ADN::SubcomandoBackup.new(args, @logger).ejecutar - when 'sync' - ADN::SubcomandoSync.new(args, @logger).ejecutar when 'triggers' ADN::SubcomandoTriggers.new(args, @logger).ejecutar when 'cierre' diff --git a/tools/adn/spec/bitacora_db_test.rb b/tools/adn/spec/bitacora_db_test.rb deleted file mode 100644 index a035855a..00000000 --- a/tools/adn/spec/bitacora_db_test.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'minitest/autorun' -require_relative '../../bitacoras-db/core/bitacora_db' -require 'date' - -module ADN - class BitacoraDBTest < Minitest::Test - def setup - # Usar el entorno development encapsulado en transacción - # Cualquier cambio persistente será anulado tras cada bloque de prueba. - @db = BitacorasDB::BitacoraDB.new(env: :development) - @db.connect - @db.execute('BEGIN') - end - - def teardown - # Abortar la transacción para dejar la db limpia, luego desconectar - @db.execute('ROLLBACK') rescue nil - @db.disconnect - end - - def test_connection_health - assert @db.connection_test? - salud = @db.health_check - assert_equal 'healthy', salud[:status] - assert salud[:bitacoras_count] >= 0 - end - - def test_nodo_crud - # Crear nodo - nombre_nodo = "test_nodo_#{Time.now.to_i}" - nodo = @db.create_nodo(nombre: nombre_nodo, tipo: 'servidor', descripcion: 'Nodo de pruebas UT') - assert nodo['id'] - assert_equal nombre_nodo, nodo['nombre'] - - # Listar nodos (debería encontrar al menos al nuevo nodo) - nodos = @db.list_nodos - encontrado = nodos.find { |n| n['nombre'] == nombre_nodo } - refute_nil encontrado - - # Encontrar por nombre - buscado = @db.find_nodo_by_nombre(nombre_nodo) - assert_equal nodo['id'], buscado['id'] - end - - def test_bitacora_crud - # Usar fecha en el futuro lejano para no colisionar con reales si falla el db - fecha_futura = Date.new(2050, 1, 1) - - # Crear Bitácora - resumen_test = 'Tests unitarios automatizados' - bitacora = @db.create_bitacora(fecha: fecha_futura, resumen: resumen_test) - assert bitacora['id'] - assert_equal fecha_futura.to_s, bitacora['fecha'] - - # Encontrar por fecha - buscada = @db.find_bitacora_by_fecha(fecha_futura) - assert_equal bitacora['id'], buscada['id'] - - # Listar filtrando fechas - rango = @db.list_bitacoras(desde: fecha_futura.to_s, hasta: fecha_futura.to_s) - assert_equal 1, rango.length - assert_equal bitacora['id'], rango.first['id'] - end - - def test_entrada_crud_con_busqueda - # 1. Setup Base - Nodo y Bitacora - fecha_act = Date.new(2050, 2, 2) - bitacora = @db.create_bitacora(fecha: fecha_act) - nodo = @db.create_nodo(nombre: "test_nodo_ev_#{Time.now.to_i}", tipo: 'vm') - - # 2. Crear entrada - desc_unica = "Validacion especial MINITEST_#{Time.now.to_i}" - entrada = @db.create_entrada( - inicio: '10:00:00', - descripcion: desc_unica, - fin: '11:00:00', - estado: '✅', - modo: 'R', - es_ia: true, - bitacora_id: bitacora['id'], - nodo_id: nodo['id'] - ) - - assert entrada['id'] - assert_equal '10:00:00', entrada['inicio'] - assert_equal 'R', entrada['modo'] - - # 3. Consultar y verificar búsqueda/filtrado implementada en Tarea 2.2 - entradas = @db.list_entradas( - buscar: 'MINITEST_', - desde: '2050-02-01', - hasta: '2050-02-03' - ) - - assert entradas.length >= 1 - hallazgo = entradas.find { |e| e['id'] == entrada['id'] } - refute_nil hallazgo - assert_equal desc_unica, hallazgo['descripcion'] - assert_equal bitacora['fecha'], hallazgo['bitacora_fecha'] - end - end -end