Refactor(ADN): Retiro de directorios y módulos obsoletos según principio Menos es Mas (spec/, clientes/, sync.rb)

This commit is contained in:
Ricardo Monla
2026-03-10 20:02:28 -03:00
parent 5329f18cf0
commit 427b000aac
6 changed files with 0 additions and 911 deletions
-3
View File
@@ -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
```
---
-517
View File
@@ -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<Hash>] 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<Hash>] 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<Hash>] 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<Hash>] 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
-1
View File
@@ -33,7 +33,6 @@ class SubcomandoAyuda
generar nodo <nombre> Generar ficha de nodo
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}backup <nodo> [--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
-282
View File
@@ -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
-4
View File
@@ -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'
-104
View File
@@ -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