583 lines
19 KiB
Ruby
583 lines
19 KiB
Ruby
#!/usr/bin/env ruby
|
||
# frozen_string_literal: true
|
||
|
||
# tools/orquestador/lib/verificacion/manager.rb
|
||
# ============================================
|
||
# Clase para verificación centralizada del dashboard P2601
|
||
# Filosofía: "Menos es más" - código reutilizable y modular
|
||
|
||
require_relative '../dashboard_core'
|
||
|
||
module DashboardCore
|
||
class VerificacionManager < Base
|
||
# Reglas de verificación
|
||
REGLAS_VERIFICACION = {
|
||
estructura: {
|
||
nivel: 'crítico',
|
||
descripcion: 'Estructura HTML básica del dashboard',
|
||
verificar: ->(contenido) {
|
||
elementos = {
|
||
'DOCTYPE html' => contenido.include?('<!DOCTYPE html>'),
|
||
'head' => contenido.include?('<head>') && contenido.include?('</head>'),
|
||
'body' => contenido.include?('<body>') && contenido.include?('</body>'),
|
||
'title P2601' => contenido.include?('P2601'),
|
||
'container' => contenido.include?('class="container"'),
|
||
'header' => contenido.include?('class="header"'),
|
||
'footer' => contenido.include?('class="footer"'),
|
||
'timeline' => contenido.include?('class="timeline"')
|
||
}
|
||
elementos
|
||
}
|
||
},
|
||
hitos: {
|
||
nivel: 'alto',
|
||
descripcion: 'Integridad y formato de hitos',
|
||
verificar: ->(contenido) {
|
||
hitos = extraer_hitos(contenido)
|
||
{
|
||
total_hitos: hitos.size,
|
||
hitos_validos: hitos.count { |h| h[:id] && h[:titulo] },
|
||
ids_validos: hitos.count { |h| HitoIdGenerator.es_valido?(h[:id]) },
|
||
duplicados: buscar_hitos_duplicados(contenido).size
|
||
}
|
||
}
|
||
},
|
||
estados: {
|
||
nivel: 'medio',
|
||
descripcion: 'Estados válidos de hitos',
|
||
verificar: ->(contenido) {
|
||
hitos = extraer_hitos(contenido)
|
||
estados_validos = hitos.count { |h| Validator.validar_estado(h[:estado]) }
|
||
estados_invalidos = hitos.reject { |h| Validator.validar_estado(h[:estado]) }
|
||
|
||
{
|
||
estados_validos: estados_validos,
|
||
estados_invalidos: estados_invalidos.size,
|
||
estados_presentes: hitos.map { |h| h[:estado] }.uniq
|
||
}
|
||
}
|
||
},
|
||
timeline: {
|
||
nivel: 'alto',
|
||
descripcion: 'Estructura de la timeline',
|
||
verificar: ->(contenido) {
|
||
timeline_presente = contenido.include?('class="timeline"')
|
||
|
||
if timeline_presente
|
||
timeline_match = contenido.match(/<div class="timeline">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m)
|
||
timeline_content = timeline_match ? timeline_match[1] : ''
|
||
hitos_en_timeline = timeline_content.scan(/<div class="tl-item">/).size
|
||
else
|
||
hitos_en_timeline = 0
|
||
end
|
||
|
||
{
|
||
presente: timeline_presente,
|
||
hitos_en_timeline: hitos_en_timeline,
|
||
estructura_correcta: timeline_presente && hitos_en_timeline > 0
|
||
}
|
||
}
|
||
},
|
||
metricas: {
|
||
nivel: 'bajo',
|
||
descripcion: 'Métricas y estadísticas',
|
||
verificar: ->(contenido) {
|
||
metricas_encontradas = {
|
||
progreso: contenido.include?('Progreso General') || contenido.include?('progressPct'),
|
||
hitos_completados: contenido.include?('Hitos Completados'),
|
||
hitos_pendientes: contenido.include?('Hitos Pendientes'),
|
||
esfuerzo_total: contenido.include?('Esfuerzo Total')
|
||
}
|
||
|
||
valores = extraer_valores_metricas(contenido)
|
||
|
||
{
|
||
metricas_presentes: metricas_encontradas.values.count(true),
|
||
valores_encontrados: valores.size,
|
||
valores: valores
|
||
}
|
||
}
|
||
}
|
||
}.freeze
|
||
|
||
def initialize(dashboard_path = nil)
|
||
super(dashboard_path)
|
||
@resultados_cache = nil
|
||
end
|
||
|
||
# Métodos principales de verificación
|
||
|
||
def verificar_completo
|
||
puts "🔍 VERIFICACIÓN COMPLETA DEL DASHBOARD P2601"
|
||
puts "=" * 60
|
||
|
||
contenido = leer_dashboard
|
||
resultados = {}
|
||
|
||
REGLAS_VERIFICACION.each do |nombre, regla|
|
||
puts "📋 Verificando: #{regla[:descripcion]}"
|
||
resultados[nombre] = ejecutar_verificacion(contenido, regla)
|
||
end
|
||
|
||
@resultados_cache = resultados
|
||
mostrar_resumen_verificacion(resultados)
|
||
|
||
resultados
|
||
end
|
||
|
||
def verificar_estructura
|
||
puts "🏗️ VERIFICANDO ESTRUCTURA DEL DASHBOARD"
|
||
puts "=" * 60
|
||
|
||
contenido = leer_dashboard
|
||
regla = REGLAS_VERIFICACION[:estructura]
|
||
resultado = ejecutar_verificacion(contenido, regla)
|
||
|
||
mostrar_detalle_estructura(resultado)
|
||
resultado
|
||
end
|
||
|
||
def verificar_hitos
|
||
puts "🎯 VERIFICANDO HITOS DEL DASHBOARD"
|
||
puts "=" * 60
|
||
|
||
contenido = leer_dashboard
|
||
regla = REGLAS_VERIFICACION[:hitos]
|
||
resultado = ejecutar_verificacion(contenido, regla)
|
||
|
||
mostrar_detalle_hitos(resultado, contenido)
|
||
resultado
|
||
end
|
||
|
||
def verificar_estados
|
||
puts "📊 VERIFICANDO ESTADOS DE HITOS"
|
||
puts "=" * 60
|
||
|
||
contenido = leer_dashboard
|
||
regla = REGLAS_VERIFICACION[:estados]
|
||
resultado = ejecutar_verificacion(contenido, regla)
|
||
|
||
mostrar_detalle_estados(resultado, contenido)
|
||
resultado
|
||
end
|
||
|
||
def analizar_hitos_detallado
|
||
puts "📋 ANÁLISIS DETALLADO DE HITOS"
|
||
puts "=" * 60
|
||
|
||
contenido = leer_dashboard
|
||
hitos = extraer_hitos(contenido)
|
||
|
||
if hitos.empty?
|
||
puts "ℹ️ No se encontraron hitos en el dashboard"
|
||
return nil
|
||
end
|
||
|
||
analisis = {
|
||
total: hitos.size,
|
||
por_estado: analizar_por_estado(hitos),
|
||
por_fase: analizar_por_fase(hitos),
|
||
por_tag: analizar_por_tag(hitos),
|
||
duplicados: buscar_hitos_duplicados(contenido),
|
||
ids_invalidos: hitos.reject { |h| HitoIdGenerator.es_valido?(h[:id]) }
|
||
}
|
||
|
||
mostrar_analisis_detallado(analisis, hitos)
|
||
analisis
|
||
end
|
||
|
||
def generar_reporte_verificacion
|
||
resultados = @resultados_cache || verificar_completo
|
||
|
||
reporte = "📋 REPORTE DE VERIFICACIÓN - DASHBOARD P2601\n"
|
||
reporte += "=" * 70 + "\n"
|
||
reporte += "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||
reporte += "Dashboard: #{CONFIG[:production_url]}\n\n"
|
||
|
||
# Resumen ejecutivo
|
||
reporte += "📊 RESUMEN EJECUTIVO\n"
|
||
reporte += "-" * 40 + "\n"
|
||
|
||
total_reglas = resultados.size
|
||
reglas_exitosas = resultados.values.count { |r| r[:exito] }
|
||
porcentaje_exito = (reglas_exitosas * 100.0 / total_reglas).round(1)
|
||
|
||
reporte += "Reglas verificadas: #{total_reglas}\n"
|
||
reporte += "Reglas exitosas: #{reglas_exitosas}\n"
|
||
reporte += "Porcentaje de éxito: #{porcentaje_exito}%\n\n"
|
||
|
||
# Detalle por regla
|
||
reporte += "🔍 DETALLE POR REGLA DE VERIFICACIÓN\n"
|
||
reporte += "-" * 40 + "\n"
|
||
|
||
resultados.each do |nombre, resultado|
|
||
regla = REGLAS_VERIFICACION[nombre]
|
||
emoji = resultado[:exito] ? '✅' : '❌'
|
||
|
||
reporte += "#{emoji} #{regla[:descripcion]} (#{regla[:nivel].upcase})\n"
|
||
|
||
if resultado[:detalles].is_a?(Hash)
|
||
resultado[:detalles].each do |detalle, valor|
|
||
reporte += " • #{detalle}: #{valor}\n"
|
||
end
|
||
end
|
||
|
||
unless resultado[:exito]
|
||
reporte += " ⚠️ Problemas detectados:\n"
|
||
resultado[:problemas].each do |problema|
|
||
reporte += " - #{problema}\n"
|
||
end
|
||
end
|
||
|
||
reporte += "\n"
|
||
end
|
||
|
||
# Recomendaciones
|
||
reporte += "💡 RECOMENDACIONES\n"
|
||
reporte += "-" * 40 + "\n"
|
||
|
||
recomendaciones = generar_recomendaciones(resultados)
|
||
if recomendaciones.any?
|
||
recomendaciones.each do |recomendacion|
|
||
reporte += "• #{recomendacion}\n"
|
||
end
|
||
else
|
||
reporte += "✅ Todas las verificaciones pasaron exitosamente.\n"
|
||
end
|
||
|
||
reporte += "\n🔗 Dashboard: #{CONFIG[:production_url]}\n"
|
||
reporte += "📅 Reporte generado: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||
|
||
reporte
|
||
end
|
||
|
||
private
|
||
|
||
def ejecutar_verificacion(contenido, regla)
|
||
begin
|
||
detalles = regla[:verificar].call(contenido)
|
||
problemas = identificar_problemas(detalles, regla[:nivel])
|
||
|
||
{
|
||
exito: problemas.empty?,
|
||
detalles: detalles,
|
||
problemas: problemas,
|
||
nivel: regla[:nivel],
|
||
descripcion: regla[:descripcion]
|
||
}
|
||
rescue => e
|
||
{
|
||
exito: false,
|
||
detalles: { error: e.message },
|
||
problemas: ["Error durante verificación: #{e.message}"],
|
||
nivel: regla[:nivel],
|
||
descripcion: regla[:descripcion]
|
||
}
|
||
end
|
||
end
|
||
|
||
def identificar_problemas(detalles, nivel)
|
||
problemas = []
|
||
|
||
case nivel
|
||
when 'crítico'
|
||
# Para estructura, todos los elementos deben estar presentes
|
||
if detalles.is_a?(Hash) && detalles.key?('DOCTYPE html')
|
||
detalles.each do |elemento, presente|
|
||
problemas << "Falta elemento: #{elemento}" unless presente
|
||
end
|
||
end
|
||
when 'alto'
|
||
# Para hitos, no debe haber duplicados y todos deben ser válidos
|
||
if detalles.is_a?(Hash)
|
||
if detalles[:duplicados] && detalles[:duplicados] > 0
|
||
problemas << "Se encontraron #{detalles[:duplicados]} hitos duplicados"
|
||
end
|
||
|
||
if detalles[:total_hitos] && detalles[:hitos_validos]
|
||
if detalles[:total_hitos] > 0 && detalles[:hitos_validos] < detalles[:total_hitos]
|
||
problemas << "#{detalles[:total_hitos] - detalles[:hitos_validos]} hitos no válidos"
|
||
end
|
||
end
|
||
end
|
||
when 'medio'
|
||
# Para estados, todos deben ser válidos
|
||
if detalles.is_a?(Hash) && detalles[:estados_invalidos] && detalles[:estados_invalidos] > 0
|
||
problemas << "Se encontraron #{detalles[:estados_invalidos]} estados inválidos"
|
||
end
|
||
end
|
||
|
||
problemas
|
||
end
|
||
|
||
def extraer_valores_metricas(contenido)
|
||
valores = {}
|
||
|
||
# Buscar porcentaje de progreso
|
||
if match = contenido.match(/id="progressPct">([^<]+)</)
|
||
valores['Progreso'] = match[1]
|
||
end
|
||
|
||
# Buscar hitos completados
|
||
if match = contenido.match(/Hitos Completados[^<]*<[^>]*>([^<]+)</)
|
||
valores['Hitos Completados'] = match[1]
|
||
end
|
||
|
||
# Buscar hitos pendientes
|
||
if match = contenido.match(/Hitos Pendientes[^<]*<[^>]*>([^<]+)</)
|
||
valores['Hitos Pendientes'] = match[1]
|
||
end
|
||
|
||
# Buscar esfuerzo total
|
||
if match = contenido.match(/Esfuerzo Total[^<]*<[^>]*>([^<]+)</)
|
||
valores['Esfuerzo Total'] = match[1]
|
||
end
|
||
|
||
valores
|
||
end
|
||
|
||
def analizar_por_estado(hitos)
|
||
conteo = Hash.new(0)
|
||
hitos.each { |h| conteo[h[:estado]] += 1 }
|
||
conteo
|
||
end
|
||
|
||
def analizar_por_fase(hitos)
|
||
conteo = Hash.new(0)
|
||
hitos.each do |hito|
|
||
if info = HitoIdGenerator.parsear_id(hito[:id])
|
||
conteo[info[:fase]] += 1
|
||
else
|
||
conteo['sin_fase'] += 1
|
||
end
|
||
end
|
||
conteo
|
||
end
|
||
|
||
def analizar_por_tag(hitos)
|
||
conteo = Hash.new(0)
|
||
hitos.each do |hito|
|
||
hito[:tags].each { |tag| conteo[tag] += 1 }
|
||
end
|
||
conteo
|
||
end
|
||
|
||
def mostrar_resumen_verificacion(resultados)
|
||
puts "\n📊 RESUMEN DE VERIFICACIÓN"
|
||
puts "=" * 60
|
||
|
||
total_reglas = resultados.size
|
||
reglas_exitosas = resultados.values.count { |r| r[:exito] }
|
||
reglas_fallidas = total_reglas - reglas_exitosas
|
||
|
||
puts "Reglas verificadas: #{total_reglas}"
|
||
puts "Reglas exitosas: #{reglas_exitosas}"
|
||
puts "Reglas fallidas: #{reglas_fallidas}"
|
||
puts "Porcentaje de éxito: #{(reglas_exitosas * 100.0 / total_reglas).round(1)}%"
|
||
|
||
# Mostrar resumen por nivel
|
||
puts "\n📈 DISTRIBUCIÓN POR NIVEL DE IMPORTANCIA:"
|
||
['crítico', 'alto', 'medio', 'bajo'].each do |nivel|
|
||
reglas_nivel = resultados.select { |_, r| r[:nivel] == nivel }
|
||
exitos_nivel = reglas_nivel.count { |_, r| r[:exito] }
|
||
total_nivel = reglas_nivel.size
|
||
|
||
if total_nivel > 0
|
||
puts " • #{nivel.capitalize}: #{exitos_nivel}/#{total_nivel} exitosas"
|
||
end
|
||
end
|
||
|
||
# Mostrar problemas críticos
|
||
problemas_criticos = resultados.select { |_, r| !r[:exito] && r[:nivel] == 'crítico' }
|
||
if problemas_criticos.any?
|
||
puts "\n⚠️ PROBLEMAS CRÍTICOS DETECTADOS:"
|
||
problemas_criticos.each do |nombre, resultado|
|
||
puts " • #{resultado[:descripcion]}"
|
||
resultado[:problemas].each do |problema|
|
||
puts " - #{problema}"
|
||
end
|
||
end
|
||
end
|
||
|
||
if reglas_exitosas == total_reglas
|
||
puts "\n🎉 ¡Todas las verificaciones pasaron exitosamente!"
|
||
else
|
||
puts "\n⚠️ Se encontraron problemas que requieren atención"
|
||
end
|
||
end
|
||
|
||
def mostrar_detalle_estructura(resultado)
|
||
puts "\n🏗️ DETALLE DE ESTRUCTURA:"
|
||
|
||
if resultado[:exito]
|
||
puts "✅ Estructura HTML básica correcta"
|
||
else
|
||
puts "❌ Problemas en la estructura:"
|
||
resultado[:problemas].each do |problema|
|
||
puts " • #{problema}"
|
||
end
|
||
end
|
||
|
||
puts "\n📋 ELEMENTOS VERIFICADOS:"
|
||
resultado[:detalles].each do |elemento, presente|
|
||
puts " • #{elemento}: #{presente ? '✅ Presente' : '❌ Ausente'}"
|
||
end
|
||
end
|
||
|
||
def mostrar_detalle_hitos(resultado, contenido)
|
||
hitos = extraer_hitos(contenido)
|
||
|
||
puts "\n🎯 DETALLE DE HITOS:"
|
||
puts " • Total de hitos: #{resultado[:detalles][:total_hitos]}"
|
||
puts " • Hitos válidos: #{resultado[:detalles][:hitos_validos]}"
|
||
puts " • IDs válidos: #{resultado[:detalles][:ids_validos]}"
|
||
puts " • Hitos duplicados: #{resultado[:detalles][:duplicados]}"
|
||
|
||
if resultado[:detalles][:duplicados] > 0
|
||
duplicados = buscar_hitos_duplicados(contenido)
|
||
puts " • IDs duplicados: #{duplicados.join(', ')}"
|
||
end
|
||
|
||
unless resultado[:exito]
|
||
puts "\n⚠️ PROBLEMAS DETECTADOS:"
|
||
resultado[:problemas].each do |problema|
|
||
puts " • #{problema}"
|
||
end
|
||
end
|
||
end
|
||
|
||
def mostrar_detalle_estados(resultado, contenido)
|
||
hitos = extraer_hitos(contenido)
|
||
|
||
puts "\n📊 DETALLE DE ESTADOS:"
|
||
puts " • Estados válidos: #{resultado[:detalles][:estados_validos]}"
|
||
puts " • Estados inválidos: #{resultado[:detalles][:estados_invalidos]}"
|
||
puts " • Estados presentes: #{resultado[:detalles][:estados_presentes].join(', ')}"
|
||
|
||
unless resultado[:exito]
|
||
puts "\n⚠️ PROBLEMAS DETECTADOS:"
|
||
resultado[:problemas].each do |problema|
|
||
puts " • #{problema}"
|
||
end
|
||
end
|
||
|
||
if resultado[:detalles][:estados_invalidos] > 0
|
||
puts "\n🔍 ESTADOS INVÁLIDOS ENCONTRADOS:"
|
||
hitos.each do |hito|
|
||
unless Validator.validar_estado(hito[:estado])
|
||
puts " • #{hito[:id]}: '#{hito[:estado]}'"
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
def mostrar_analisis_detallado(analisis, hitos)
|
||
puts "\n📈 ANÁLISIS DETALLADO DE HITOS"
|
||
puts "=" * 60
|
||
|
||
puts "📊 ESTADÍSTICAS GENERALES:"
|
||
puts " • Total de hitos: #{analisis[:total]}"
|
||
|
||
if analisis[:por_estado].any?
|
||
puts "\n📊 DISTRIBUCIÓN POR ESTADO:"
|
||
analisis[:por_estado].each do |estado, cantidad|
|
||
porcentaje = (cantidad * 100.0 / analisis[:total]).round(1)
|
||
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
|
||
puts " • #{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
|
||
end
|
||
end
|
||
|
||
if analisis[:por_fase].any?
|
||
puts "\n📁 DISTRIBUCIÓN POR FASE:"
|
||
analisis[:por_fase].each do |fase, cantidad|
|
||
puts " • Fase #{fase}: #{cantidad} hitos"
|
||
end
|
||
end
|
||
|
||
if analisis[:por_tag].any?
|
||
puts "\n🏷️ DISTRIBUCIÓN POR TAG:"
|
||
analisis[:por_tag].sort_by { |_, count| -count }.first(10).each do |tag, cantidad|
|
||
puts " • #{tag}: #{cantidad} hitos"
|
||
end
|
||
end
|
||
|
||
if analisis[:duplicados].any?
|
||
puts "\n⚠️ HITOS DUPLICADOS:"
|
||
puts " • Total: #{analisis[:duplicados].size}"
|
||
puts " • IDs: #{analisis[:duplicados].join(', ')}"
|
||
end
|
||
|
||
if analisis[:ids_invalidos].any?
|
||
puts "\n❌ IDs DE HITOS INVÁLIDOS:"
|
||
analisis[:ids_invalidos].each do |hito|
|
||
puts " • #{hito[:id]}: #{hito[:titulo]}"
|
||
end
|
||
end
|
||
|
||
# Mostrar hitos por fase y estado
|
||
puts "\n🎯 HITOS POR FASE Y ESTADO:"
|
||
hitos.group_by do |hito|
|
||
info = HitoIdGenerator.parsear_id(hito[:id])
|
||
info ? info[:fase] : 'sin_fase'
|
||
end.each do |fase, hitos_fase|
|
||
puts " 📁 Fase #{fase}:"
|
||
hitos_fase.group_by { |h| h[:estado] }.each do |estado, hitos_estado|
|
||
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
|
||
puts " • #{estado_info[:nombre]}: #{hitos_estado.size}"
|
||
end
|
||
end
|
||
end
|
||
|
||
def generar_recomendaciones(resultados)
|
||
recomendaciones = []
|
||
|
||
# Recomendaciones basadas en problemas encontrados
|
||
resultados.each do |nombre, resultado|
|
||
next if resultado[:exito]
|
||
|
||
case nombre
|
||
when :estructura
|
||
recomendaciones << "Corregir estructura HTML básica del dashboard"
|
||
when :hitos
|
||
if resultado[:detalles][:duplicados] > 0
|
||
recomendaciones << "Eliminar hitos duplicados usando la herramienta 'limpiar'"
|
||
end
|
||
if resultado[:detalles][:total_hitos] > 0 &&
|
||
resultado[:detalles][:hitos_validos] < resultado[:detalles][:total_hitos]
|
||
recomendaciones << "Corregir hitos con estructura inválida"
|
||
end
|
||
when :estados
|
||
if resultado[:detalles][:estados_invalidos] > 0
|
||
recomendaciones << "Corregir estados inválidos de hitos"
|
||
end
|
||
when :timeline
|
||
unless resultado[:detalles][:presente]
|
||
recomendaciones << "Agregar sección timeline al dashboard"
|
||
end
|
||
end
|
||
end
|
||
|
||
# Recomendaciones generales
|
||
contenido = leer_dashboard
|
||
hitos = extraer_hitos(contenido)
|
||
|
||
if hitos.empty?
|
||
recomendaciones << "Agregar hitos al dashboard usando la herramienta 'agregar_hitos'"
|
||
elsif hitos.size < 5
|
||
recomendaciones << "Considerar agregar más hitos para mejor seguimiento del proyecto"
|
||
end
|
||
|
||
estados = hitos.map { |h| h[:estado] }
|
||
if estados.count('pending') > hitos.size * 0.7
|
||
recomendaciones << "Iniciar más hitos pendientes para avanzar en el proyecto"
|
||
end
|
||
|
||
if estados.count('blocked') > 0
|
||
recomendaciones << "Revisar hitos bloqueados y resolver dependencias"
|
||
end
|
||
|
||
recomendaciones.uniq
|
||
end
|
||
end
|
||
end
|