Reestructuración modular integral de servicios, reorganización de gestión en docs/ y limpieza profunda del repositorio

This commit is contained in:
Ricardo Monla
2026-03-06 18:03:07 -03:00
parent 15725e0b3c
commit ac9bdf516b
286 changed files with 36187 additions and 112775 deletions
+540
View File
@@ -0,0 +1,540 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/dashboard_core.rb
# =========================================
# Módulo central con funcionalidades comunes para todas las herramientas
# del orquestador del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require 'fileutils'
require 'time'
module DashboardCore
# Configuración centralizada
CONFIG = {
dashboard_path: File.expand_path('../../../../dashboard/index.html', __dir__),
backups_dir: File.expand_path('../../../../dashboard/backups', __dir__),
production_url: 'https://ns8.frlr.utn.edu.ar/bitacoras/p2601',
static_url: 'https://ns8.frlr.utn.edu.ar/P2601/'
}.freeze
# Estados válidos para hitos
ESTADOS_VALIDOS = {
'pending' => { nombre: 'Pendiente', clase: 'pending', color: '#eab308' },
'in-progress' => { nombre: 'En Progreso', clase: 'in-progress', color: '#3b82f6' },
'completed' => { nombre: 'Completado', clase: 'completed', color: '#22c55e' },
'blocked' => { nombre: 'Bloqueado', clase: 'blocked', color: '#ef4444' },
'on-hold' => { nombre: 'En Espera', clase: 'on-hold', color: '#8b5cf6' }
}.freeze
# Generador de IDs de hitos por fase
class HitoIdGenerator
def self.generar_id(fase_numero, secuencia)
# Formato: f{numero_fase}{secuencia de 3 dígitos}
# Ejemplo: fase 5, secuencia 1 -> f5001
# Ejemplo: fase 10, secuencia 25 -> f1025
"f#{fase_numero.to_s.rjust(2, '0')}#{secuencia.to_s.rjust(3, '0')}"
end
def self.parsear_id(id_hito)
# Extraer fase y secuencia de un ID
match = id_hito.match(/^f(\d{2})(\d{3})$/)
return nil unless match
{
fase: match[1].to_i,
secuencia: match[2].to_i,
id_completo: id_hito
}
end
def self.es_valido?(id_hito)
!parsear_id(id_hito).nil?
end
def self.siguiente_id(fase_numero, ids_existentes)
# Encontrar la secuencia más alta para esta fase
max_secuencia = ids_existentes
.select { |id| es_valido?(id) }
.map { |id| parsear_id(id) }
.select { |info| info && info[:fase] == fase_numero }
.map { |info| info[:secuencia] }
.max || 0
generar_id(fase_numero, max_secuencia + 1)
end
end
# Hitos predefinidos por fase con nuevo sistema de IDs
HITOS_POR_FASE = {
'fase5' => [
{
id: 'f05001',
titulo: '🔒 Backup Automático de VMs en Proxmox',
descripcion: 'Configurar backups automáticos VZDump programados para DC y SQL Server. Establecer retención de 7 días, almacenamiento en NAS/SMB compartido.',
tags: ['tag-remote', 'tag-phys'],
esfuerzo: 'Remoto: ~2h',
estado: 'pending'
},
{
id: 'f05002',
titulo: '🗄️ Backup Automático de Base de Datos sysdasuten',
descripcion: 'Configurar jobs de SQL Server Agent para backup diario completo y log cada 15 minutos. Verificar restauración periódica en ambiente de pruebas.',
tags: ['tag-remote', 'SQL Agent'],
esfuerzo: 'Remoto: ~2h',
estado: 'pending'
},
{
id: 'f05003',
titulo: '📊 Monitoreo de Recursos del Hipervisor',
descripcion: 'Implementar sistema de monitoreo básico para CPU, RAM, almacenamiento y red de srv-dasu. Alertas por email/Telegram para recursos críticos.',
tags: ['tag-remote', 'Herramientas ADN'],
esfuerzo: 'Remoto: ~3h',
estado: 'pending'
},
{
id: 'f05004',
titulo: '📋 Documentación Operativa del Sistema',
descripcion: 'Crear manual de operaciones: procedimientos de backup/restore, acceso remoto, resolución de problemas comunes, contactos de soporte, diagramas de red.',
tags: ['tag-remote', 'Herramientas ADN', 'Git'],
esfuerzo: 'Remoto: ~6h',
estado: 'pending'
},
{
id: 'f05005',
titulo: '⚡ Pruebas de Rendimiento del Sistema',
descripcion: 'Realizar pruebas de carga simulada en sistema DASUTEN. Medir tiempos de respuesta, consumo de recursos, identificar cuellos de botella. Generar reporte de performance.',
tags: ['tag-phys', 'tag-remote', 'SQL Profiler'],
esfuerzo: 'Físico: 2h, Remoto: ~2h',
estado: 'pending'
}
]
}.freeze
# Clase base para todas las herramientas del dashboard
class Base
attr_reader :dashboard_path, :backup_dir
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || CONFIG[:dashboard_path]
@backup_dir = CONFIG[:backups_dir]
@verbose = ENV['DEBUG'] == 'true'
end
# Métodos comunes para todas las herramientas
def crear_backup(tipo = 'general')
FileUtils.mkdir_p(@backup_dir) unless Dir.exist?(@backup_dir)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
backup_file = File.join(@backup_dir, "dashboard_#{tipo}_#{timestamp}.html")
puts "💾 Creando backup en: #{backup_file}" if @verbose
FileUtils.cp(@dashboard_path, backup_file)
puts "✅ Backup creado" if @verbose
backup_file
end
def leer_dashboard
unless File.exist?(@dashboard_path)
raise "No se encuentra el dashboard en #{@dashboard_path}"
end
File.read(@dashboard_path)
end
def guardar_dashboard(contenido)
File.write(@dashboard_path, contenido)
puts "💾 Dashboard guardado" if @verbose
end
def extraer_hitos(contenido)
hitos = []
contenido.scan(/<div class="tl-item">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m) do |hito_html|
hito = hito_html[0]
# Extraer información
id_match = hito.match(/<span class="tl-id">([^<]+)<\/span>/)
titulo_match = hito.match(/<div class="tl-title">([^<]+)<\/div>/)
estado_match = hito.match(/<div class="tl-dot ([^"]+)"><\/div>/)
fecha_match = hito.match(/<span class="tl-date">([^<]+)<\/span>/)
descripcion_match = hito.match(/<div class="tl-desc">([^<]+)<\/div>/)
next unless id_match && titulo_match
hitos << {
id: id_match[1],
titulo: titulo_match[1],
estado: estado_match ? estado_match[1] : 'pending',
fecha: fecha_match ? fecha_match[1] : 'Sin fecha',
descripcion: descripcion_match ? descripcion_match[1] : nil,
tags: hito.scan(/<span class="[^"]*tag[^"]*">([^<]+)<\/span>/).flatten,
html_completo: hito_html[0]
}
end
hitos.uniq { |h| h[:id] }
end
def buscar_hitos_duplicados(contenido)
hitos = extraer_hitos(contenido)
ids_encontrados = {}
duplicados = []
hitos.each do |hito|
if ids_encontrados[hito[:id]]
duplicados << hito[:id]
else
ids_encontrados[hito[:id]] = true
end
end
duplicados
end
def generar_html_hito(hito)
tags_html = hito[:tags].map do |tag|
if tag.start_with?('tag-')
"<span class=\"#{tag}\">#{tag.gsub('tag-', '').capitalize}</span>"
else
"<span class=\"tag\">#{tag}</span>"
end
end.join
estado_info = ESTADOS_VALIDOS[hito[:estado]] || ESTADOS_VALIDOS['pending']
<<~HTML
<div class="tl-item">
<div class="tl-dot #{hito[:estado]}"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">#{hito[:id]}</span><span class="tl-date">#{estado_info[:nombre]}</span></div>
<div class="tl-title">#{hito[:titulo]}</div>
<div class="tl-desc">#{hito[:descripcion]}</div>
<div class="tl-tags">#{tags_html}</div>
</div>
</div>
HTML
end
def actualizar_hito_en_contenido(contenido, hito_id, nuevo_estado, fecha = nil)
fecha_actual = fecha || Time.now.strftime('%Y-%m-%d')
estado_info = ESTADOS_VALIDOS[nuevo_estado]
# Patrón para encontrar el hito específico
patron_hito = /(<div class="tl-item">[\s\S]*?<span class="tl-id">#{Regexp.escape(hito_id)}<\/span>[\s\S]*?<\/div>\s*<\/div>\s*<\/div>)/
hito_encontrado = false
nuevo_contenido = contenido.gsub(patron_hito) do |hito_completo|
hito_encontrado = true
# Actualizar estado (clase del dot)
hito_actualizado = hito_completo.gsub(
/<div class="tl-dot ([^"]+)"><\/div>/,
"<div class=\"tl-dot #{nuevo_estado}\"></div>"
)
# Actualizar fecha
hito_actualizado = hito_actualizado.gsub(
/<span class="tl-date">([^<]+)<\/span>/,
"<span class=\"tl-date\">#{estado_info[:nombre]} - #{fecha_actual}</span>"
)
hito_actualizado
end
[nuevo_contenido, hito_encontrado]
end
def insertar_hitos_en_timeline(contenido, hitos_html, modo = 'append')
case modo
when 'append'
# Buscar el cierre de la sección timeline
timeline_end_pattern = /<\/div>\s*<!-- Timeline de Hitos -->/
if contenido.match?(timeline_end_pattern)
contenido.gsub(timeline_end_pattern) do |match|
"#{hitos_html}\n #{match}"
end
else
# Buscar el último </div> antes del footer
footer_start = '<div class="footer">'
if contenido.include?(footer_start)
contenido.gsub(footer_start) do |match|
"#{hitos_html}\n #{match}"
end
else
# Agregar al final del body
body_end = '</body>'
contenido.gsub(body_end) do |match|
"#{hitos_html}\n #{match}"
end
end
end
when 'replace'
# Reemplazar hitos existentes de una fase específica
fase_pattern = /<!-- Hitos de .*? agregados automáticamente - .*?-->[\s\S]*?(?=<!--|<\/div>\s*<div class="footer">)/m
if contenido.match?(fase_pattern)
contenido.gsub(fase_pattern, hitos_html)
else
insertar_hitos_en_timeline(contenido, hitos_html, 'append')
end
else
raise "Modo '#{modo}' no válido. Use 'append' o 'replace'."
end
end
def verificar_estructura_basica(contenido)
elementos_requeridos = {
'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"')
}
{
exito: elementos_requeridos.values.all?,
detalles: elementos_requeridos
}
end
def mostrar_resumen_estadisticas(hitos)
return if hitos.empty?
puts "📊 RESUMEN DE ESTADÍSTICAS"
puts "=" * 50
# Conteo por estado
conteo_estados = Hash.new(0)
hitos.each { |h| conteo_estados[h[:estado]] += 1 }
total = hitos.size
conteo_estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / total).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts "#{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
# Tags más comunes
todos_tags = hitos.flat_map { |h| h[:tags] }
if todos_tags.any?
tags_frecuentes = Hash.new(0)
todos_tags.each { |tag| tags_frecuentes[tag] += 1 }
puts "\n🏷️ TAGS MÁS COMUNES:"
tags_frecuentes.sort_by { |_, count| -count }.first(5).each do |tag, count|
puts "#{tag}: #{count} hitos"
end
end
# IDs por fase
ids_por_fase = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
ids_por_fase[info[:fase]] += 1
end
end
if ids_por_fase.any?
puts "\n📁 DISTRIBUCIÓN POR FASE:"
ids_por_fase.sort.each do |fase, cantidad|
puts " • Fase #{fase}: #{cantidad} hitos"
end
end
end
# Métodos de utilidad
def mostrar_encabezado(titulo)
puts "\n#{titulo}"
puts "=" * [titulo.length, 60].min
end
def mostrar_pie
puts "\n🔗 Dashboard: #{CONFIG[:production_url]}"
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
end
def debug(mensaje)
puts "🔍 DEBUG: #{mensaje}" if @verbose
end
end
# Módulo para manejo de fases
module FaseManager
def self.hitos_por_fase(fase_nombre)
HITOS_POR_FASE[fase_nombre] || []
end
def self.fases_disponibles
HITOS_POR_FASE.keys
end
def self.generar_hitos_html(fase_nombre, timestamp = nil)
hitos = hitos_por_fase(fase_nombre)
timestamp ||= Time.now.strftime('%Y-%m-%d %H:%M')
html = <<~HTML
<!-- Hitos de #{fase_nombre.upcase} agregados automáticamente - #{timestamp} -->
HTML
hitos.each do |hito|
html += Base.new.generar_html_hito(hito) + "\n\n"
end
html
end
def self.extraer_numero_fase(fase_nombre)
match = fase_nombre.match(/fase(\d+)/)
match ? match[1].to_i : nil
end
end
# Módulo para validaciones
module Validator
def self.validar_estado(estado)
ESTADOS_VALIDOS.key?(estado)
end
def self.validar_id_hito(id_hito)
HitoIdGenerator.es_valido?(id_hito)
end
def self.validar_fase(fase_nombre)
HITOS_POR_FASE.key?(fase_nombre)
end
def self.validar_modo_insercion(modo)
['append', 'replace', 'insert'].include?(modo)
end
end
# Módulo para generación de reportes
module Reporter
def self.generar_reporte_hitos(hitos)
return "No hay hitos para reportar" if hitos.empty?
reporte = "📋 REPORTE DE HITOS\n"
reporte += "=" * 50 + "\n"
reporte += "Total de hitos: #{hitos.size}\n\n"
# Agrupar por estado
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
reporte += "📊 DISTRIBUCIÓN POR ESTADO:\n"
estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
reporte += "#{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)\n"
end
# Agrupar por fase
fases = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
fases[info[:fase]] += 1
end
end
if fases.any?
reporte += "\n📁 DISTRIBUCIÓN POR FASE:\n"
fases.sort.each do |fase, cantidad|
reporte += " • Fase #{fase}: #{cantidad} hitos\n"
end
end
# Últimos 5 hitos
reporte += "\n🎯 ÚLTIMOS 5 HITOS:\n"
hitos.last(5).reverse.each do |hito|
estado_info = ESTADOS_VALIDOS[hito[:estado]] || ESTADOS_VALIDOS['pending']
reporte += " • [#{hito[:id]}] #{hito[:titulo]} (#{estado_info[:nombre]})\n"
end
reporte
end
def self.generar_reporte_estadisticas(contenido)
base = Base.new
hitos = base.extraer_hitos(contenido)
reporte = "📈 REPORTE ESTADÍSTICO DEL DASHBOARD\n"
reporte += "=" * 60 + "\n"
# Estadísticas del archivo
reporte += "📄 INFORMACIÓN DEL ARCHIVO:\n"
reporte += " • Tamaño: #{contenido.bytesize} bytes\n"
reporte += " • Líneas: #{contenido.lines.count}\n"
reporte += " • Hitos encontrados: #{hitos.size}\n"
# Verificación de estructura
estructura = base.verificar_estructura_basica(contenido)
reporte += "\n🏗️ VERIFICACIÓN DE ESTRUCTURA:\n"
estructura[:detalles].each do |elemento, presente|
reporte += "#{elemento}: #{presente ? '✅' : '❌'}\n"
end
# Hitos duplicados
duplicados = base.buscar_hitos_duplicados(contenido)
if duplicados.any?
reporte += "\n⚠️ HITOS DUPLICADOS DETECTADOS:\n"
duplicados.each do |id|
reporte += "#{id}\n"
end
end
reporte
end
end
# Módulo para manejo de backups
module BackupManager
def self.listar_backups(backup_dir)
return [] unless Dir.exist?(backup_dir)
backups = Dir.glob(File.join(backup_dir, "dashboard_*.html"))
.map { |path| File.basename(path) }
.sort
.reverse
backups
end
def self.crear_backup_temporal(contenido, tipo = 'temp')
temp_dir = File.join(Dir.tmpdir, 'dashboard_backups')
FileUtils.mkdir_p(temp_dir) unless Dir.exist?(temp_dir)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
temp_file = File.join(temp_dir, "dashboard_#{tipo}_#{timestamp}.html")
File.write(temp_file, contenido)
temp_file
end
def self.limpiar_backups_antiguos(backup_dir, dias = 7)
return unless Dir.exist?(backup_dir)
limite = Time.now - (dias * 24 * 60 * 60)
backups = Dir.glob(File.join(backup_dir, "dashboard_*.html"))
backups_eliminados = 0
backups.each do |backup|
if File.mtime(backup) < limite
File.delete(backup)
backups_eliminados += 1
end
end
backups_eliminados
end
end
end
+440
View File
@@ -0,0 +1,440 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/estados/manager.rb
# ========================================
# Clase para manejo centralizado de estados de hitos del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require_relative '../dashboard_core'
module DashboardCore
class EstadosManager < Base
attr_reader :estados_cache
def initialize(dashboard_path = nil)
super(dashboard_path)
@estados_cache = nil
end
# Métodos principales para gestión de estados
def cargar_estados(force_reload = false)
return @estados_cache if @estados_cache && !force_reload
hitos = cargar_hitos
@estados_cache = analizar_estados(hitos)
@estados_cache
end
def listar_estados_disponibles
ESTADOS_VALIDOS.map do |codigo, info|
{
codigo: codigo,
nombre: info[:nombre],
clase: info[:clase],
color: info[:color],
descripcion: descripcion_estado(codigo)
}
end
end
def actualizar_estado_hito(id_hito, nuevo_estado, fecha = nil)
# Validar estado
unless Validator.validar_estado(nuevo_estado)
raise "Estado '#{nuevo_estado}' no válido. Estados disponibles: #{ESTADOS_VALIDOS.keys.join(', ')}"
end
# Verificar que el hito exista
hito = buscar_hito_por_id(id_hito)
unless hito
raise "Hito #{id_hito} no encontrado"
end
# No hacer nada si el estado ya es el mismo
if hito[:estado] == nuevo_estado
puts "️ El hito #{id_hito} ya tiene estado '#{nuevo_estado}'"
return true
end
# Crear backup
crear_backup('actualizar_estado')
# Leer contenido actual
contenido = leer_dashboard
# Actualizar estado en el contenido
fecha_actual = fecha || Time.now.strftime('%Y-%m-%d')
estado_info = ESTADOS_VALIDOS[nuevo_estado]
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
contenido, id_hito, nuevo_estado, fecha_actual
)
unless encontrado
raise "No se pudo encontrar el hito #{id_hito} para actualizar estado"
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@estados_cache = nil
cargar_estados(true)
# Registrar cambio
registrar_cambio_estado(id_hito, hito[:estado], nuevo_estado, fecha_actual)
puts "✅ Estado del hito #{id_hito} actualizado: #{hito[:estado]}#{nuevo_estado}"
true
end
def actualizar_estados_multiples(cambios)
# Validar todos los cambios primero
cambios.each do |id_hito, nuevo_estado|
unless Validator.validar_estado(nuevo_estado)
raise "Estado '#{nuevo_estado}' no válido para hito #{id_hito}"
end
end
# Crear backup
crear_backup('actualizar_estados_multiples')
# Leer contenido actual
contenido = leer_dashboard
nuevo_contenido = contenido.dup
# Aplicar cada cambio
resultados = {}
cambios.each do |id_hito, nuevo_estado|
puts "🔍 Actualizando hito: #{id_hito}#{nuevo_estado}"
# Verificar que el hito exista
hito = buscar_hito_por_id(id_hito)
if hito
# Actualizar en el contenido
fecha_actual = Time.now.strftime('%Y-%m-%d')
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
nuevo_contenido, id_hito, nuevo_estado, fecha_actual
)
if encontrado && hito[:estado] != nuevo_estado
resultados[id_hito] = { exito: true, anterior: hito[:estado], nuevo: nuevo_estado }
registrar_cambio_estado(id_hito, hito[:estado], nuevo_estado, fecha_actual)
else
resultados[id_hito] = { exito: false, error: 'No encontrado o estado igual' }
end
else
resultados[id_hito] = { exito: false, error: 'Hito no encontrado' }
end
end
# Guardar cambios si hubo actualizaciones exitosas
exitos = resultados.values.count { |r| r[:exito] }
if exitos > 0
guardar_dashboard(nuevo_contenido)
@estados_cache = nil
cargar_estados(true)
end
# Mostrar resultados
mostrar_resultados_actualizacion(resultados)
exitos > 0
end
def avanzar_estado_hito(id_hito)
# Obtener hito actual
hito = buscar_hito_por_id(id_hito)
unless hito
raise "Hito #{id_hito} no encontrado"
end
# Determinar siguiente estado según flujo de trabajo
siguiente_estado = siguiente_estado_en_flujo(hito[:estado])
if siguiente_estado
actualizar_estado_hito(id_hito, siguiente_estado)
else
puts "️ El hito #{id_hito} ya está en el estado final del flujo"
false
end
end
def retroceder_estado_hito(id_hito)
# Obtener hito actual
hito = buscar_hito_por_id(id_hito)
unless hito
raise "Hito #{id_hito} no encontrado"
end
# Determinar estado anterior según flujo de trabajo
estado_anterior = estado_anterior_en_flujo(hito[:estado])
if estado_anterior
actualizar_estado_hito(id_hito, estado_anterior)
else
puts "️ El hito #{id_hito} ya está en el estado inicial del flujo"
false
end
end
def reiniciar_estado_hito(id_hito)
actualizar_estado_hito(id_hito, 'pending')
end
def completar_hito(id_hito, fecha = nil)
actualizar_estado_hito(id_hito, 'completed', fecha)
end
def bloquear_hito(id_hito, fecha = nil)
actualizar_estado_hito(id_hito, 'blocked', fecha)
end
def poner_en_espera_hito(id_hito, fecha = nil)
actualizar_estado_hito(id_hito, 'on-hold', fecha)
end
def iniciar_hito(id_hito, fecha = nil)
actualizar_estado_hito(id_hito, 'in-progress', fecha)
end
# Métodos de consulta y análisis
def estadisticas_estados
estados = cargar_estados
{
total_hitos: estados[:total_hitos],
distribucion: estados[:distribucion],
porcentajes: estados[:porcentajes],
tendencia: calcular_tendencia(estados),
ultima_actualizacion: Time.now.strftime('%Y-%m-%d %H:%M:%S')
}
end
def reporte_estados
estados = cargar_estados
reporte = "📊 REPORTE DE ESTADOS DE HITOS\n"
reporte += "=" * 60 + "\n"
reporte += "Total de hitos: #{estados[:total_hitos]}\n\n"
reporte += "📈 DISTRIBUCIÓN POR ESTADO:\n"
estados[:distribucion].each do |estado, cantidad|
porcentaje = estados[:porcentajes][estado]
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
reporte += "#{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)\n"
end
# Análisis de progreso
completados = estados[:distribucion]['completed'] || 0
en_progreso = estados[:distribucion]['in-progress'] || 0
total = estados[:total_hitos]
if total > 0
progreso_total = ((completados + (en_progreso * 0.5)) / total * 100).round(1)
reporte += "\n🎯 PROGRESO GENERAL DEL PROYECTO:\n"
reporte += " • Progreso estimado: #{progreso_total}%\n"
reporte += " • Hitos completados: #{completados}/#{total}\n"
reporte += " • Hitos en progreso: #{en_progreso}/#{total}\n"
end
# Recomendaciones
reporte += "\n💡 RECOMENDACIONES:\n"
if (estados[:distribucion]['pending'] || 0) > total * 0.7
reporte += " • Muchos hitos pendientes (#{estados[:distribucion]['pending']}/#{total})\n"
reporte += " • Considerar iniciar más hitos\n"
end
if (estados[:distribucion]['blocked'] || 0) > 0
reporte += " • Hay hitos bloqueados (#{estados[:distribucion]['blocked']})\n"
reporte += " • Revisar dependencias y obstáculos\n"
end
if (estados[:distribucion]['on-hold'] || 0) > 0
reporte += " • Hay hitos en espera (#{estados[:distribucion]['on-hold']})\n"
reporte += " • Evaluar prioridades y recursos\n"
end
reporte
end
def hitos_por_estado(estado)
hitos = cargar_hitos
hitos.select { |h| h[:estado] == estado }
end
def hitos_pendientes
hitos_por_estado('pending')
end
def hitos_en_progreso
hitos_por_estado('in-progress')
end
def hitos_completados
hitos_por_estado('completed')
end
def hitos_bloqueados
hitos_por_estado('blocked')
end
def hitos_en_espera
hitos_por_estado('on-hold')
end
def progreso_por_fase
hitos = cargar_hitos
progreso_por_fase = {}
hitos.group_by do |hito|
info = HitoIdGenerator.parsear_id(hito[:id])
info ? info[:fase] : 'sin_fase'
end.each do |fase, hitos_fase|
total = hitos_fase.size
completados = hitos_fase.count { |h| h[:estado] == 'completed' }
en_progreso = hitos_fase.count { |h| h[:estado] == 'in-progress' }
progreso = total > 0 ? ((completados + (en_progreso * 0.5)) / total * 100).round(1) : 0
progreso_por_fase[fase] = {
total: total,
completados: completados,
en_progreso: en_progreso,
progreso: progreso
}
end
progreso_por_fase
end
private
def cargar_hitos
contenido = leer_dashboard
extraer_hitos(contenido)
end
def buscar_hito_por_id(id_hito)
hitos = cargar_hitos
hitos.find { |h| h[:id] == id_hito }
end
def analizar_estados(hitos)
total_hitos = hitos.size
distribucion = Hash.new(0)
hitos.each { |h| distribucion[h[:estado]] += 1 }
porcentajes = {}
distribucion.each do |estado, cantidad|
porcentajes[estado] = total_hitos > 0 ? (cantidad * 100.0 / total_hitos).round(1) : 0
end
{
total_hitos: total_hitos,
distribucion: distribucion,
porcentajes: porcentajes,
hitos: hitos
}
end
def descripcion_estado(codigo_estado)
case codigo_estado
when 'pending'
"Hito pendiente de iniciar. Color amarillo."
when 'in-progress'
"Hito en ejecución activa. Color azul."
when 'completed'
"Hito finalizado exitosamente. Color verde."
when 'blocked'
"Hito bloqueado por dependencias u obstáculos. Color rojo."
when 'on-hold'
"Hito en espera por priorización o recursos. Color púrpura."
else
"Estado desconocido."
end
end
def siguiente_estado_en_flujo(estado_actual)
flujo = {
'pending' => 'in-progress',
'in-progress' => 'completed',
'completed' => nil, # Estado final
'blocked' => 'in-progress', # Desbloqueado
'on-hold' => 'in-progress' # Reanudado
}
flujo[estado_actual]
end
def estado_anterior_en_flujo(estado_actual)
flujo_inverso = {
'in-progress' => 'pending',
'completed' => 'in-progress',
'blocked' => 'in-progress',
'on-hold' => 'in-progress'
}
flujo_inverso[estado_actual]
end
def registrar_cambio_estado(id_hito, estado_anterior, estado_nuevo, fecha)
# Podría guardarse en un log o base de datos
log_entry = {
id_hito: id_hito,
estado_anterior: estado_anterior,
estado_nuevo: estado_nuevo,
fecha: fecha,
timestamp: Time.now.to_i
}
# Por ahora solo imprimimos para debug
debug("Cambio de estado registrado: #{log_entry}")
end
def mostrar_resultados_actualizacion(resultados)
puts "\n📊 RESULTADOS DE ACTUALIZACIÓN DE ESTADOS"
puts "=" * 60
exitos = resultados.values.count { |r| r[:exito] }
fallos = resultados.values.count { |r| !r[:exito] }
resultados.each do |id_hito, resultado|
if resultado[:exito]
puts "#{id_hito}: #{resultado[:anterior]}#{resultado[:nuevo]}"
else
puts "#{id_hito}: #{resultado[:error]}"
end
end
puts "\n📈 RESUMEN:"
puts " • Hitós actualizados exitosamente: #{exitos}"
puts " • Hitós con errores: #{fallos}"
end
def calcular_tendencia(estados)
# Simple análisis de tendencia (podría mejorarse con histórico)
completados = estados[:distribucion]['completed'] || 0
en_progreso = estados[:distribucion]['in-progress'] || 0
total = estados[:total_hitos]
if total == 0
return 'sin_datos'
end
porcentaje_completados = (completados * 100.0 / total).round(1)
if porcentaje_completados > 70
'avanzado'
elsif porcentaje_completados > 30
'en_progreso'
elsif en_progreso > 0
'iniciando'
else
'pendiente'
end
end
end
end
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/hitos/manager.rb
# ======================================
# Clase para manejo centralizado de hitos del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require_relative '../dashboard_core'
module DashboardCore
class HitosManager < Base
attr_reader :hitos_cache
def initialize(dashboard_path = nil)
super(dashboard_path)
@hitos_cache = nil
end
# Métodos principales para gestión de hitos
def cargar_hitos(force_reload = false)
return @hitos_cache if @hitos_cache && !force_reload
contenido = leer_dashboard
@hitos_cache = extraer_hitos(contenido)
@hitos_cache
end
def buscar_hito_por_id(id_hito)
hitos = cargar_hitos
hitos.find { |h| h[:id] == id_hito }
end
def buscar_hitos_por_fase(numero_fase)
hitos = cargar_hitos
hitos.select do |hito|
info = HitoIdGenerator.parsear_id(hito[:id])
info && info[:fase] == numero_fase
end
end
def buscar_hitos_por_estado(estado)
hitos = cargar_hitos
hitos.select { |h| h[:estado] == estado }
end
def buscar_hitos_por_tag(tag)
hitos = cargar_hitos
hitos.select { |h| h[:tags].include?(tag) }
end
def agregar_hito(hito_data)
# Validar datos del hito
unless hito_data[:id] && hito_data[:titulo]
raise "ID y título son requeridos para agregar un hito"
end
# Validar formato del ID
unless HitoIdGenerator.es_valido?(hito_data[:id])
raise "ID de hito inválido. Formato esperado: fNNNNN (ej: f05001)"
end
# Verificar que no exista ya
if buscar_hito_por_id(hito_data[:id])
raise "El hito con ID #{hito_data[:id]} ya existe"
end
# Crear backup
crear_backup('agregar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Generar HTML del hito
hito_html = generar_html_hito(hito_data)
# Insertar en la timeline
nuevo_contenido = insertar_hitos_en_timeline(contenido, hito_html, 'append')
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{hito_data[:id]} agregado exitosamente"
true
end
def actualizar_hito(id_hito, cambios)
# Verificar que el hito exista
hito_existente = buscar_hito_por_id(id_hito)
unless hito_existente
raise "Hito #{id_hito} no encontrado"
end
# Validar cambios permitidos
cambios_permitidos = [:estado, :fecha, :titulo, :descripcion, :tags]
cambios.each_key do |campo|
unless cambios_permitidos.include?(campo)
raise "Campo '#{campo}' no es modificable"
end
end
# Validar estado si se está actualizando
if cambios[:estado] && !Validator.validar_estado(cambios[:estado])
raise "Estado '#{cambios[:estado]}' no válido"
end
# Crear backup
crear_backup('actualizar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Actualizar hito en el contenido
nuevo_estado = cambios[:estado] || hito_existente[:estado]
nueva_fecha = cambios[:fecha] || Time.now.strftime('%Y-%m-%d')
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
contenido, id_hito, nuevo_estado, nueva_fecha
)
unless encontrado
raise "No se pudo encontrar el hito #{id_hito} para actualizar"
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{id_hito} actualizado exitosamente"
true
end
def eliminar_hito(id_hito)
# Verificar que el hito exista
unless buscar_hito_por_id(id_hito)
raise "Hito #{id_hito} no encontrado"
end
# Crear backup
crear_backup('eliminar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Patrón para encontrar y eliminar el hito específico
patron_hito = /<div class="tl-item">[\s\S]*?<span class="tl-id">#{Regexp.escape(id_hito)}<\/span>[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/
nuevo_contenido = contenido.gsub(patron_hito, '')
# Verificar que se eliminó algo
if nuevo_contenido == contenido
raise "No se pudo eliminar el hito #{id_hito}"
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{id_hito} eliminado exitosamente"
true
end
def agregar_hitos_fase(fase_nombre, modo = 'append')
# Validar fase
unless Validator.validar_fase(fase_nombre)
raise "Fase '#{fase_nombre}' no válida. Fases disponibles: #{FaseManager.fases_disponibles.join(', ')}"
end
# Validar modo
unless Validator.validar_modo_insercion(modo)
raise "Modo '#{modo}' no válido. Use 'append' o 'replace'."
end
# Crear backup
crear_backup("agregar_#{fase_nombre}")
# Leer contenido actual
contenido = leer_dashboard
# Generar HTML de hitos de la fase
hitos_html = FaseManager.generar_hitos_html(fase_nombre)
# Insertar en la timeline
nuevo_contenido = insertar_hitos_en_timeline(contenido, hitos_html, modo)
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
hitos_agregados = cargar_hitos(true)
# Filtrar hitos de esta fase
numero_fase = FaseManager.extraer_numero_fase(fase_nombre)
hitos_fase = buscar_hitos_por_fase(numero_fase)
puts "#{hitos_fase.size} hitos de #{fase_nombre} agregados exitosamente"
hitos_fase
end
# Métodos de consulta y reporte
def estadisticas
hitos = cargar_hitos
{
total: hitos.size,
por_estado: contar_por_estado(hitos),
por_fase: contar_por_fase(hitos),
por_tag: contar_por_tag(hitos),
ultima_actualizacion: Time.now.strftime('%Y-%m-%d %H:%M:%S')
}
end
def reporte_detallado
hitos = cargar_hitos
Reporter.generar_reporte_hitos(hitos)
end
def verificar_duplicados
contenido = leer_dashboard
duplicados = buscar_hitos_duplicados(contenido)
if duplicados.empty?
"✅ No se encontraron hitos duplicados"
else
reporte = "⚠️ HITOS DUPLICADOS ENCONTRADOS:\n"
reporte += "=" * 40 + "\n"
duplicados.each do |id|
reporte += "#{id}\n"
end
reporte += "\nTotal: #{duplicados.size} duplicados"
reporte
end
end
def limpiar_duplicados
contenido = leer_dashboard
duplicados = buscar_hitos_duplicados(contenido)
if duplicados.empty?
puts "✅ No hay hitos duplicados para limpiar"
return true
end
puts "🧹 Limpiando #{duplicados.size} hitos duplicados..."
# Crear backup
crear_backup('limpiar_duplicados')
# Extraer todos los hitos únicos
hitos_unicos = extraer_hitos(contenido)
# Reconstruir timeline con hitos únicos
timeline_html = hitos_unicos.map { |h| generar_html_hito(h) }.join("\n\n")
# Reemplazar la timeline completa
timeline_pattern = /<div class="timeline">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m
nuevo_contenido = contenido.gsub(timeline_pattern) do |match|
<<~HTML
<div class="timeline">
#{timeline_html}
</div>
HTML
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ #{duplicados.size} hitos duplicados eliminados"
true
end
private
def contar_por_estado(hitos)
conteo = Hash.new(0)
hitos.each { |h| conteo[h[:estado]] += 1 }
conteo
end
def contar_por_fase(hitos)
conteo = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
conteo[info[:fase]] += 1
end
end
conteo
end
def contar_por_tag(hitos)
conteo = Hash.new(0)
hitos.each do |hito|
hito[:tags].each { |tag| conteo[tag] += 1 }
end
conteo
end
end
end
@@ -0,0 +1,173 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/sincronizacion/manager.rb
# ==============================================
# Clase para sincronización centralizada del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require_relative '../dashboard_core'
module DashboardCore
class SincronizacionManager < Base
# Configuración de rutas de sincronización
CONFIG_SINCRONIZACION = {
local: {
dashboard_dir: File.expand_path('../../../../dashboard', __dir__),
index_file: 'index.html',
backups_dir: 'backups'
},
production: {
dashboard_dir: '/var/www/html/P2601',
index_file: 'index.html',
backups_dir: '/var/www/html/P2601/backups'
},
staging: {
dashboard_dir: '/var/www/html/P2601-staging',
index_file: 'index.html',
backups_dir: '/var/www/html/P2601-staging/backups'
}
}.freeze
def initialize(dashboard_path = nil, options = {})
super(dashboard_path)
@dry_run = options[:dry_run] || false
@verbose = options[:verbose] || false
@backup = options[:backup] || true
@ambiente_destino = options[:ambiente] || 'production'
end
# Métodos principales de sincronización
def sincronizar_a_produccion
puts "🔄 SINCRONIZANDO DASHBOARD A PRODUCCIÓN"
puts "=" * 60
# Verificar rutas locales
unless verificar_ruta_local
puts "❌ Error: No se puede acceder al dashboard local"
return false
end
# Verificar rutas de producción
unless verificar_ruta_produccion
puts "❌ Error: No se puede acceder al dashboard de producción"
puts " Ruta: #{CONFIG_SINCRONIZACION[:production][:dashboard_dir]}"
return false
end
# Crear backup en producción si está habilitado
if @backup && !@dry_run
unless crear_backup_produccion
puts "❌ Error: No se pudo crear backup en producción"
return false
end
end
# Sincronizar archivos
puts "📁 Sincronizando archivos..."
archivos_sincronizados = sincronizar_archivos
# Mostrar resumen
mostrar_resumen_sincronizacion(archivos_sincronizados)
archivos_sincronizados.any?
end
def sincronizar_a_staging
puts "🔄 SINCRONIZANDO DASHBOARD A STAGING"
puts "=" * 60
# Verificar rutas locales
unless verificar_ruta_local
puts "❌ Error: No se puede acceder al dashboard local"
return false
end
# Verificar rutas de staging
unless verificar_ruta_staging
puts "❌ Error: No se puede acceder al dashboard de staging"
puts " Ruta: #{CONFIG_SINCRONIZACION[:staging][:dashboard_dir]}"
return false
end
# Crear backup en staging si está habilitado
if @backup && !@dry_run
unless crear_backup_staging
puts "❌ Error: No se pudo crear backup en staging"
return false
end
end
# Sincronizar archivos
puts "📁 Sincronizando archivos..."
archivos_sincronizados = sincronizar_archivos_staging
# Mostrar resumen
mostrar_resumen_sincronizacion(archivos_sincronizados, 'staging')
archivos_sincronizados.any?
end
def verificar_estado_sincronizacion
puts "🔍 VERIFICANDO ESTADO DE SINCRONIZACIÓN"
puts "=" * 60
# Verificar estado local
estado_local = verificar_estado_local
puts "📁 ESTADO LOCAL:"
mostrar_estado_verificacion(estado_local)
# Verificar estado producción
estado_produccion = verificar_estado_produccion
puts "\n🌐 ESTADO PRODUCCIÓN:"
mostrar_estado_verificacion(estado_produccion)
# Comparar archivos
puts "\n📊 COMPARACIÓN DE ARCHIVOS:"
comparacion = comparar_archivos
mostrar_comparacion_archivos(comparacion)
# Recomendaciones
puts "\n💡 RECOMENDACIONES:"
generar_recomendaciones_sincronizacion(estado_local, estado_produccion, comparacion).each do |recomendacion|
puts "#{recomendacion}"
end
true
end
def listar_backups(ambiente = 'production')
puts "💾 LISTANDO BACKUPS - #{ambiente.upcase}"
puts "=" * 60
config = case ambiente
when 'production'
CONFIG_SINCRONIZACION[:production]
when 'staging'
CONFIG_SINCRONIZACION[:staging]
when 'local'
CONFIG_SINCRONIZACION[:local]
else
puts "❌ Ambiente '#{ambiente}' no válido"
return false
end
backup_dir = config[:backups_dir]
unless Dir.exist?(backup_dir)
puts "️ No existe el directorio de backups: #{backup_dir}"
return false
end
backups = Dir.glob(File.join(backup_dir, "dashboard_*.html"))
.map { |path| File.basename(path) }
.sort
.reverse
if backups.empty?
puts "️ No se encontraron backups"
return true
end
puts "📋 BACKUPS DISPONIBLES (#{back
@@ -0,0 +1,582 @@
#!/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
@@ -0,0 +1,653 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/vista_previa/manager.rb
# ============================================
# Clase para vista previa centralizada del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require_relative '../dashboard_core'
module DashboardCore
class VistaPreviaManager < Base
# Modos de vista previa disponibles
MODOS_VISTA_PREVIA = {
'resumen' => {
descripcion: 'Resumen completo del dashboard',
metodo: :mostrar_resumen_completo
},
'hitos' => {
descripcion: 'Hitos detallados',
metodo: :mostrar_hitos_detallados
},
'estructura' => {
descripcion: 'Estructura del dashboard',
metodo: :mostrar_estructura
},
'estadisticas' => {
descripcion: 'Estadísticas detalladas',
metodo: :mostrar_estadisticas_detalladas
},
'rapido' => {
descripcion: 'Resumen rápido',
metodo: :mostrar_resumen_rapido
},
'metricas' => {
descripcion: 'Métricas clave',
metodo: :mostrar_metricas_clave
},
'estados' => {
descripcion: 'Análisis de estados',
metodo: :mostrar_analisis_estados
},
'fases' => {
descripcion: 'Análisis por fases',
metodo: :mostrar_analisis_fases
}
}.freeze
def initialize(dashboard_path = nil)
super(dashboard_path)
@contenido_cache = nil
@hitos_cache = nil
end
# Métodos principales de vista previa
def mostrar_vista_previa(modo = 'resumen')
unless MODOS_VISTA_PREVIA.key?(modo)
raise "Modo '#{modo}' no válido. Modos disponibles: #{MODOS_VISTA_PREVIA.keys.join(', ')}"
end
modo_info = MODOS_VISTA_PREVIA[modo]
puts "👁️ VISTA PREVIA - #{modo_info[:descripcion].upcase}"
puts "=" * 60
send(modo_info[:metodo])
true
end
def mostrar_resumen_completo
contenido = leer_contenido
hitos = cargar_hitos
# Información del archivo
puts "📄 INFORMACIÓN DEL ARCHIVO:"
puts " • Ruta: #{@dashboard_path}"
puts " • Tamaño: #{contenido.bytesize} bytes (#{contenido.bytesize / 1024} KB)"
puts " • Líneas: #{contenido.lines.count}"
puts " • Última modificación: #{File.mtime(@dashboard_path).strftime('%Y-%m-%d %H:%M')}"
# Estructura básica
puts "\n🏗️ ESTRUCTURA BÁSICA:"
estructura = verificar_estructura_basica(contenido)
estructura[:detalles].each do |elemento, presente|
puts "#{elemento}: #{presente ? '✅ Presente' : '❌ Ausente'}"
end
# Hitos
puts "\n🎯 INFORMACIÓN DE HITOS:"
puts " • Total de hitos: #{hitos.size}"
if hitos.any?
# Distribución por estado
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
puts " • Distribución por estado:"
estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts " - #{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
# Distribución por fase
fases = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
fases[info[:fase]] += 1
end
end
if fases.any?
puts " • Distribución por fase:"
fases.sort.each do |fase, cantidad|
puts " - Fase #{fase}: #{cantidad} hitos"
end
end
end
# Timeline
puts "\n📅 TIMELINE:"
timeline_presente = contenido.include?('class="timeline"')
puts " • Presente: #{timeline_presente ? '✅ Sí' : '❌ No'}"
if timeline_presente
timeline_match = contenido.match(/<div class="timeline">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m)
if timeline_match
hitos_en_timeline = timeline_match[1].scan(/<div class="tl-item">/).size
puts " • Hitos en timeline: #{hitos_en_timeline}"
end
end
# Métricas
puts "\n📈 MÉTRICAS:"
metricas = extraer_metricas_clave(contenido)
if metricas.any?
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
else
puts " • No se encontraron métricas específicas"
end
# Recomendaciones rápidas
puts "\n💡 RECOMENDACIONES RÁPIDAS:"
generar_recomendaciones_rapidas(contenido, hitos).each do |recomendacion|
puts "#{recomendacion}"
end
mostrar_pie
end
def mostrar_hitos_detallados
hitos = cargar_hitos
if hitos.empty?
puts "️ No se encontraron hitos en el dashboard"
return
end
puts "📋 HITOS DETALLADOS (#{hitos.size} total)"
puts "=" * 60
hitos.each_with_index do |hito, i|
puts "\n#{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
puts " 📊 Estado: #{hito[:estado]}"
puts " 📅 Fecha: #{hito[:fecha]}"
if hito[:tags].any?
puts " 🏷️ Tags: #{hito[:tags].join(', ')}"
end
if hito[:descripcion]
if hito[:descripcion].length > 100
puts " 📝 Descripción: #{hito[:descripcion][0..100]}..."
else
puts " 📝 Descripción: #{hito[:descripcion]}"
end
end
end
# Resumen de estados
puts "\n📊 RESUMEN DE ESTADOS:"
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts "#{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
mostrar_pie
end
def mostrar_estructura
contenido = leer_contenido
puts "🏗️ ESTRUCTURA DEL DASHBOARD"
puts "=" * 60
# Secciones principales
puts "📂 SECCIONES PRINCIPALES:"
secciones = [
{ nombre: 'Head', inicio: '<head>', fin: '</head>' },
{ nombre: 'Body', inicio: '<body>', fin: '</body>' },
{ nombre: 'Header', inicio: 'class="header"', fin: nil },
{ nombre: 'Timeline', inicio: 'class="timeline"', fin: '</div><!-- Timeline de Hitos -->' },
{ nombre: 'Footer', inicio: 'class="footer"', fin: '</footer>' }
]
secciones.each do |seccion|
presente = contenido.include?(seccion[:inicio])
puts "#{seccion[:nombre]}: #{presente ? '✅ Presente' : '❌ Ausente'}"
if presente && seccion[:fin] && contenido.include?(seccion[:fin])
inicio_idx = contenido.index(seccion[:inicio])
fin_idx = contenido.index(seccion[:fin], inicio_idx)
if inicio_idx && fin_idx
tamaño = fin_idx - inicio_idx + seccion[:fin].length
puts " Tamaño: ~#{tamaño} bytes"
end
end
end
# Elementos HTML
puts "\n🔢 CONTEO DE ELEMENTOS HTML:"
elementos = {
'Divs' => contenido.scan(/<div/).size,
'Spans' => contenido.scan(/<span/).size,
'Headers (h1-h6)' => contenido.scan(/<h[1-6]/).size,
'Párrafos (p)' => contenido.scan(/<p>/).size,
'Enlaces (a)' => contenido.scan(/<a /).size
}
elementos.each do |elemento, count|
puts "#{elemento}: #{count}"
end
# Clases CSS
puts "\n🎨 CLASES CSS MÁS COMUNES:"
clases = contenido.scan(/class="([^"]+)"/).flatten
clases_frecuentes = Hash.new(0)
clases.each do |clase_lista|
clase_lista.split(/\s+/).each do |clase|
clases_frecuentes[clase] += 1
end
end
# Mostrar las 10 clases más comunes
clases_frecuentes.sort_by { |_, count| -count }.first(10).each do |clase, count|
puts " • .#{clase}: #{count} ocurrencias"
end
mostrar_pie
end
def mostrar_estadisticas_detalladas
contenido = leer_contenido
hitos = cargar_hitos
puts "📈 ESTADÍSTICAS DETALLADAS"
puts "=" * 60
# Estadísticas del archivo
puts "📄 ESTADÍSTICAS DEL ARCHIVO:"
puts " • Caracteres totales: #{contenido.length}"
texto_sin_html = contenido.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ')
puts " • Caracteres sin HTML: #{texto_sin_html.length}"
puts " • Palabras aproximadas: #{texto_sin_html.split.size}"
puts " • Líneas de código: #{contenido.lines.count}"
# Densidad de información
if hitos.any?
palabras_por_hito = texto_sin_html.split.size / hitos.size.to_f
puts " • Palabras por hito: #{palabras_por_hito.round(1)}"
end
# Estadísticas de hitos
puts "\n🎯 ESTADÍSTICAS DE HITOS:"
puts " • Total de hitos: #{hitos.size}"
if hitos.any?
# Por estado
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
puts " • Distribución por estado:"
estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts " - #{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
# Por fase
fases = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
fases[info[:fase]] += 1
end
end
if fases.any?
puts " • Distribución por fase:"
fases.sort.each do |fase, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
puts " - Fase #{fase}: #{cantidad} (#{porcentaje}%)"
end
end
# Por tag
todos_tags = hitos.flat_map { |h| h[:tags] }
if todos_tags.any?
tags_frecuentes = Hash.new(0)
todos_tags.each { |tag| tags_frecuentes[tag] += 1 }
puts " • Tags más comunes:"
tags_frecuentes.sort_by { |_, count| -count }.first(5).each do |tag, count|
porcentaje = (count * 100.0 / hitos.size).round(1)
puts " - #{tag}: #{count} hitos (#{porcentaje}%)"
end
end
end
# Métricas extraídas
puts "\n📊 MÉTRICAS EXTRAÍDAS:"
metricas = extraer_todas_metricas(contenido)
if metricas.any?
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
else
puts " • No se encontraron métricas específicas"
end
mostrar_pie
end
def mostrar_resumen_rapido
contenido = leer_contenido
hitos = cargar_hitos
puts "⚡ RESUMEN RÁPIDO DASHBOARD"
puts "=" * 40
# Información básica
puts "📄 Archivo: #{File.basename(@dashboard_path)}"
puts " • Tamaño: #{contenido.bytesize / 1024} KB"
puts " • Líneas: #{contenido.lines.count}"
puts " • Modificado: #{File.mtime(@dashboard_path).strftime('%Y-%m-%d %H:%M')}"
# Hitos
puts "🎯 Hitos: #{hitos.size} totales"
# Estados
if hitos.any?
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
puts "📊 Estados:"
estados.each do |estado, count|
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts "#{estado_info[:nombre]}: #{count}"
end
end
# Timeline
timeline_presente = contenido.include?('class="timeline"')
puts "📅 Timeline: #{timeline_presente ? '✅ Presente' : '❌ Ausente'}"
# Métricas clave
metricas = extraer_metricas_clave(contenido)
if metricas.any?
puts "📈 Métricas clave:"
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
end
puts "\n🔗 URL: #{CONFIG[:production_url]}"
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 40
end
def mostrar_metricas_clave
contenido = leer_contenido
puts "📊 MÉTRICAS CLAVE DEL DASHBOARD"
puts "=" * 60
metricas = extraer_todas_metricas(contenido)
if metricas.empty?
puts "️ No se encontraron métricas específicas en el dashboard"
return
end
puts "📈 MÉTRICAS ENCONTRADAS:"
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
# Análisis de progreso si hay métricas relevantes
if metricas['Progreso']
progreso = metricas['Progreso'].to_i rescue 0
puts "\n🎯 ANÁLISIS DE PROGRESO:"
puts " • Progreso general: #{progreso}%"
if progreso >= 80
puts " • Estado: 🎉 Avanzado"
elsif progreso >= 50
puts " • Estado: ⚡ En progreso"
elsif progreso >= 20
puts " • Estado: 🚀 Iniciando"
else
puts " • Estado: 📋 Pendiente"
end
end
mostrar_pie
end
def mostrar_analisis_estados
hitos = cargar_hitos
puts "📊 ANÁLISIS DE ESTADOS DE HITOS"
puts "=" * 60
if hitos.empty?
puts "️ No se encontraron hitos para analizar"
return
end
# Distribución por estado
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
puts "📈 DISTRIBUCIÓN POR ESTADO:"
estados.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts "#{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
# Análisis de progreso
completados = estados['completed'] || 0
en_progreso = estados['in-progress'] || 0
total = hitos.size
if total > 0
progreso_total = ((completados + (en_progreso * 0.5)) / total * 100).round(1)
puts "\n🎯 ANÁLISIS DE PROGRESO:"
puts " • Progreso estimado: #{progreso_total}%"
puts " • Hitos completados: #{completados}/#{total}"
puts " • Hitos en progreso: #{en_progreso}/#{total}"
puts " • Hitos pendientes: #{estados['pending'] || 0}/#{total}"
end
# Recomendaciones basadas en estados
puts "\n💡 RECOMENDACIONES:"
if (estados['pending'] || 0) > total * 0.7
puts " • Muchos hitos pendientes (#{estados['pending']}/#{total})"
puts " • Considerar iniciar más hitos"
end
if (estados['blocked'] || 0) > 0
puts " • Hay hitos bloqueados (#{estados['blocked']})"
puts " • Revisar dependencias y obstáculos"
end
if (estados['on-hold'] || 0) > 0
puts " • Hay hitos en espera (#{estados['on-hold']})"
puts " • Evaluar prioridades y recursos"
end
mostrar_pie
end
def mostrar_analisis_fases
hitos = cargar_hitos
puts "📁 ANÁLISIS POR FASES"
puts "=" * 60
if hitos.empty?
puts "️ No se encontraron hitos para analizar"
return
end
# Agrupar hitos por fase
hitos_por_fase = Hash.new { |h, k| h[k] = [] }
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
hitos_por_fase[info[:fase]] << hito
else
hitos_por_fase['sin_fase'] << hito
end
end
puts "📊 DISTRIBUCIÓN POR FASE:"
hitos_por_fase.sort.each do |fase, hitos_fase|
puts " • Fase #{fase}: #{hitos_fase.size} hitos"
end
# Análisis detallado por fase
puts "\n📈 ANÁLISIS DETALLADO POR FASE:"
hitos_por_fase.sort.each do |fase, hitos_fase|
next if hitos_fase.empty?
puts "\n 📁 FASE #{fase}:"
puts " • Total hitos: #{hitos_fase.size}"
# Estados en esta fase
estados_fase = Hash.new(0)
hitos_fase.each { |h| estados_fase[h[:estado]] += 1 }
if estados_fase.any?
puts " • Distribución por estado:"
estados_fase.each do |estado, cantidad|
porcentaje = (cantidad * 100.0 / hitos_fase.size).round(1)
estado_info = ESTADOS_VALIDOS[estado] || ESTADOS_VALIDOS['pending']
puts " - #{estado_info[:nombre]}: #{cantidad} (#{porcentaje}%)"
end
end
# Progreso de la fase
completados = estados_fase['completed'] || 0
en_progreso = estados_fase['in-progress'] || 0
progreso_fase = hitos_fase.size > 0 ? ((completados + (en_progreso * 0.5)) / hitos_fase.size * 100).round(1) : 0
puts " • Progreso de la fase: #{progreso_fase}%"
end
# Hitos sin fase
if hitos_por_fase['sin_fase'] && !hitos_por_fase['sin_fase'].empty?
puts "\n⚠️ HITOS SIN FASE ASIGNADA:"
hitos_por_fase['sin_fase'].each do |hito|
puts "#{hito[:id]}: #{hito[:titulo]}"
end
end
mostrar_pie
end
private
def leer_contenido
@contenido_cache ||= leer_dashboard
end
def cargar_hitos
@hitos_cache ||= begin
contenido = leer_contenido
extraer_hitos(contenido)
end
end
def extraer_metricas_clave(contenido)
metricas = {}
# Buscar porcentaje de progreso
if match = contenido.match(/id="progressPct">([^<]+)</)
metricas['Progreso'] = match[1]
end
# Buscar hitos completados
if match = contenido.match(/Hitos Completados[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Completados'] = match[1]
end
# Buscar hitos pendientes
if match = contenido.match(/Hitos Pendientes[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Pendientes'] = match[1]
end
metricas
end
def extraer_todas_metricas(contenido)
metricas = {}
# Buscar porcentaje de progreso
if match = contenido.match(/id="progressPct">([^<]+)</)
metricas['Progreso'] = match[1]
end
# Buscar hitos completados
if match = contenido.match(/Hitos Completados[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Completados'] = match[1]
end
# Buscar hitos pendientes
if match = contenido.match(/Hitos Pendientes[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Pendientes'] = match[1]
end
# Buscar esfuerzo total
if match = contenido.match(/Esfuerzo Total[^<]*<[^>]*>([^<]+)</)
metricas['Esfuerzo Total'] = match[1]
end
# Buscar horas físicas
if match = contenido.match(/Horas Físicas[^<]*<[^>]*>([^<]+)</)
metricas['Horas Físicas'] = match[1]
end
metricas
end
def generar_recomendaciones_rapidas(contenido, hitos)
recomendaciones = []
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
# Verificar duplicados
duplicados = buscar_hitos_duplicados(contenido)
if duplicados.any?
recomendaciones << "Eliminar hitos duplicados usando la herramienta 'limpiar'"
end
# Verificar estructura
estructura = verificar_estructura_basica(contenido)
unless estructura[:exito]
recomendaciones << "Corregir estructura HTML básica del dashboard"
end
# Verificar timeline
unless contenido.include?('class="timeline"')
recomendaciones << "Agregar sección timeline al dashboard"
end
# Verificar estados
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