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