Refactor(ADN): Retiro de scripts de prueba, rutinas obsoletas de dashboard, y herramientas de prototipado que ensucian el entorno de tools, cumpliendo axioma de simplicidad

This commit is contained in:
Ricardo Monla
2026-03-10 20:24:38 -03:00
parent 937bff070a
commit a290832d52
26 changed files with 0 additions and 10373 deletions
-485
View File
@@ -1,485 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/actualizar_dashboard_p2601.rb — Herramienta para actualizar dashboard P2601 con hitos adicionales
# ================================================================================
# Esta herramienta actualiza el dashboard P2601 (dashboard/index.html) agregando
# hitos adicionales propuestos para la fase de implementación final del proyecto DASUTEN.
#
# Características:
# • Lee el dashboard HTML actual
# • Agrega hitos adicionales basados en análisis del proyecto
# • Mantiene la estructura y estilo existentes
# • Integra con herramientas ADN para seguimiento
# • Crea backup del archivo original
#
# Uso:
# ruby tools/actualizar_dashboard_p2601.rb [OPCIONES]
#
# Opciones:
# --dry-run Mostrar cambios sin aplicar
# --backup Crear backup antes de modificar
# --hitos N Agregar solo N hitos (por defecto: todos)
# --categoria CAT Filtrar por categoría (infraestructura, monitoreo, etc.)
# --help Mostrar ayuda
require 'nokogiri'
require 'fileutils'
require 'optparse'
require 'date'
class ActualizadorDashboardP2601
VERSION = "1.0.0"
# Hitos adicionales propuestos para la fase de implementación
HITOS_ADICIONALES = [
{
id: "BKP02",
fecha: "Pendiente",
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: [
'<span class="tag tag-remote">Remoto: ~2h</span>',
'<span class="tag tag-phys">Requiere acceso físico</span>'
],
estado: "pending", # pending, active, done
categoria: "infraestructura",
prioridad: "alta",
prerequisitos: ["FÍSICO"]
},
{
id: "MON01",
fecha: "Pendiente",
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: [
'<span class="tag tag-remote">Remoto: ~3h</span>',
'<span class="tag">Herramientas ADN</span>'
],
estado: "pending",
categoria: "monitoreo",
prioridad: "media",
prerequisitos: ["FÍSICO"]
},
{
id: "DB02",
fecha: "Pendiente",
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: [
'<span class="tag tag-remote">Remoto: ~2h</span>',
'<span class="tag">SQL Agent</span>'
],
estado: "pending",
categoria: "base_datos",
prioridad: "alta",
prerequisitos: ["ADDOM"]
},
{
id: "SEC01",
fecha: "Pendiente",
titulo: "Auditoría de Seguridad del Dominio",
descripcion: "Revisar y configurar políticas de auditoría en DC. Logs de acceso, cambios en AD, intentos fallidos. Centralización de logs en syslog server.",
tags: [
'<span class="tag tag-remote">Remoto: ~4h</span>',
'<span class="tag">Windows Event Log</span>'
],
estado: "pending",
categoria: "seguridad",
prioridad: "media",
prerequisitos: ["GPO"]
},
{
id: "DOC01",
fecha: "Pendiente",
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: [
'<span class="tag tag-remote">Remoto: ~6h</span>',
'<span class="tag">Herramientas ADN</span>',
'<span class="tag">Git</span>'
],
estado: "pending",
categoria: "documentacion",
prioridad: "media",
prerequisitos: ["UAT"]
},
{
id: "RED01",
fecha: "Pendiente",
titulo: "Optimización de Red y Firewall",
descripcion: "Revisar reglas de firewall en todas las VMs. Optimizar rutas Tailscale. Configurar QoS para tráfico crítico. Documentar reglas de firewall.",
tags: [
'<span class="tag tag-remote">Remoto: ~3h</span>',
'<span class="tag">Tailscale</span>',
'<span class="tag">Windows Firewall</span>'
],
estado: "pending",
categoria: "red",
prioridad: "baja",
prerequisitos: ["NET01"]
},
{
id: "PERF01",
fecha: "Pendiente",
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: [
'<span class="tag tag-phys">Físico: 2h</span>',
'<span class="tag tag-remote">Remoto: ~2h</span>',
'<span class="tag">SQL Profiler</span>'
],
estado: "pending",
categoria: "performance",
prioridad: "media",
prerequisitos: ["CLI"]
},
{
id: "MIG02",
fecha: "Pendiente",
titulo: "Plan de Migración de Datos Final",
descripcion: "Desarrollar script/procedimiento para migración incremental de datos desde sistema antiguo. Validar integridad de datos migrados. Crear rollback plan.",
tags: [
'<span class="tag tag-remote">Remoto: ~8h</span>',
'<span class="tag">SSIS</span>',
'<span class="tag">PowerShell</span>'
],
estado: "pending",
categoria: "migracion",
prioridad: "alta",
prerequisitos: ["P02.APP"]
}
].freeze
def initialize(opciones = {})
@opciones = opciones
@dashboard_path = '/home/rmonla/Documentos/GitHub/dtic-srv-NS8/dashboard/index.html'
@backup_path = nil
@cambios = []
@errores = []
end
def ejecutar
mostrar_encabezado
unless validar_entorno
puts "❌ No se puede continuar debido a errores en el entorno."
exit 1
end
if @opciones[:dry_run]
puts "🔍 MODO SIMULACIÓN (dry-run) - No se aplicarán cambios reales"
puts "-" * 50
end
contenido_original = leer_dashboard
return if contenido_original.nil?
if @opciones[:backup] && !@opciones[:dry_run]
crear_backup(contenido_original)
end
hitos_filtrados = filtrar_hitos
puts "📋 Hitos a agregar: #{hitos_filtrados.size} de #{HITOS_ADICIONALES.size} disponibles"
contenido_actualizado = agregar_hitos_al_dashboard(contenido_original, hitos_filtrados)
if contenido_actualizado && !@opciones[:dry_run]
guardar_dashboard(contenido_actualizado)
elsif @opciones[:dry_run]
mostrar_cambios_simulados(contenido_actualizado)
end
mostrar_resumen
rescue => e
puts "❌ Error fatal: #{e.message}"
puts e.backtrace if @opciones[:debug]
exit 1
end
private
def mostrar_encabezado
puts "🔄 ACTUALIZADOR DE DASHBOARD P2601 - v#{VERSION}"
puts "=" * 70
puts "Proyecto: DASUTEN (P2601)"
puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "Dashboard: #{@dashboard_path}"
puts ""
end
def validar_entorno
puts "🔍 Validando entorno..."
# Verificar que el dashboard existe
unless File.exist?(@dashboard_path)
@errores << "Dashboard no encontrado en: #{@dashboard_path}"
puts "#{@errores.last}"
return false
end
# Verificar permisos de escritura
unless File.writable?(@dashboard_path)
@errores << "Sin permisos de escritura en: #{@dashboard_path}"
puts "#{@errores.last}"
return false
end
# Verificar que es un archivo HTML válido
contenido = File.read(@dashboard_path, 500)
unless contenido.include?('<!DOCTYPE html>') || contenido.include?('<html')
@errores << "El archivo no parece ser un HTML válido"
puts "⚠️ #{@errores.last}"
end
puts "✅ Entorno validado correctamente"
true
end
def leer_dashboard
puts "📖 Leyendo dashboard actual..."
begin
contenido = File.read(@dashboard_path)
puts "✅ Dashboard leído (#{contenido.length} bytes, #{contenido.lines.count} líneas)"
contenido
rescue => e
@errores << "Error leyendo dashboard: #{e.message}"
puts "#{@errores.last}"
nil
end
end
def crear_backup(contenido)
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
@backup_path = "#{@dashboard_path}.backup_#{timestamp}"
puts "💾 Creando backup en: #{@backup_path}"
begin
File.write(@backup_path, contenido)
puts "✅ Backup creado exitosamente"
rescue => e
@errores << "Error creando backup: #{e.message}"
puts "⚠️ #{@errores.last}"
@backup_path = nil
end
end
def filtrar_hitos
hitos = HITOS_ADICIONALES.dup
# Filtrar por categoría si se especificó
if @opciones[:categoria]
categoria = @opciones[:categoria].downcase
hitos.select! { |h| h[:categoria] == categoria }
puts " • Filtrado por categoría: #{categoria} (#{hitos.size} hitos)"
end
# Limitar número de hitos si se especificó
if @opciones[:hitos] && @opciones[:hitos] > 0
limite = [@opciones[:hitos], hitos.size].min
hitos = hitos.first(limite)
puts " • Limitado a: #{limite} hitos"
end
# Ordenar por prioridad (alta -> media -> baja)
orden_prioridad = { 'alta' => 1, 'media' => 2, 'baja' => 3 }
hitos.sort_by! { |h| orden_prioridad[h[:prioridad]] }
hitos
end
def agregar_hitos_al_dashboard(contenido, hitos)
puts "✏️ Agregando hitos al dashboard..."
return contenido if hitos.empty?
# Buscar el final de la sección de timeline
timeline_end_marker = '</div><!-- Timeline de Hitos -->'
unless contenido.include?(timeline_end_marker)
timeline_end_marker = '</div>'
puts "⚠️ Marcador de timeline no encontrado, usando marcador genérico"
end
# Generar HTML para los nuevos hitos
hitos_html = hitos.map { |hito| generar_html_hito(hito) }.join("\n\n")
# Insertar los nuevos hitos antes del marcador de fin
nuevo_contenido = contenido.gsub(timeline_end_marker) do |match|
"#{hitos_html}\n\n#{match}"
end
@cambios = hitos.map { |h| h[:id] }
puts "#{hitos.size} hitos preparados para agregar: #{@cambios.join(', ')}"
nuevo_contenido
end
def generar_html_hito(hito)
clase_dot = case hito[:estado]
when 'done' then 'tl-dot done'
when 'active' then 'tl-dot active'
else 'tl-dot pending'
end
<<~HTML
<div class="tl-item">
<div class="#{clase_dot}"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">#{hito[:id]}</span><span class="tl-date">#{hito[:fecha]}</span></div>
<div class="tl-title">#{hito[:titulo]}</div>
<div class="tl-desc">#{hito[:descripcion]}</div>
<div class="tl-tags">#{hito[:tags].join}</div>
</div>
</div>
HTML
end
def guardar_dashboard(contenido)
puts "💾 Guardando dashboard actualizado..."
begin
# Crear backup temporal
temp_backup = "#{@dashboard_path}.tmp"
File.write(temp_backup, contenido)
# Verificar que el HTML es válido
doc = Nokogiri::HTML(contenido)
if doc.errors.empty?
# Reemplazar el archivo original
FileUtils.mv(temp_backup, @dashboard_path)
puts "✅ Dashboard actualizado exitosamente"
puts " • Nuevo tamaño: #{contenido.length} bytes"
puts " • Nuevas líneas: #{contenido.lines.count}"
else
@errores << "HTML inválido generado"
puts "#{@errores.last}"
FileUtils.rm(temp_backup) if File.exist?(temp_backup)
end
rescue => e
@errores << "Error guardando dashboard: #{e.message}"
puts "#{@errores.last}"
end
end
def mostrar_cambios_simulados(contenido)
puts "📋 CAMBIOS SIMULADOS (dry-run):"
puts "-" * 50
if @cambios.any?
puts "✅ Se agregarían #{@cambios.size} hitos:"
@cambios.each_with_index do |hito_id, index|
hito = HITOS_ADICIONALES.find { |h| h[:id] == hito_id }
puts " #{index + 1}. #{hito_id}: #{hito[:titulo]}"
puts " Categoría: #{hito[:categoria]}, Prioridad: #{hito[:prioridad]}"
puts " Prerequisitos: #{hito[:prerequisitos].join(', ')}" if hito[:prerequisitos].any?
end
puts "\n📊 Estadísticas del nuevo contenido:"
puts " • Tamaño: #{contenido.length} bytes"
puts " • Líneas: #{contenido.lines.count}"
# Mostrar ejemplo del HTML generado
puts "\n📝 Ejemplo de HTML generado:"
ejemplo = contenido.lines.grep(/tl-item/).first(3)
ejemplo.each { |line| puts " #{line.strip}" }
else
puts "️ No se generarían cambios"
end
end
def mostrar_resumen
puts "\n" + "=" * 70
puts "📊 RESUMEN DE LA ACTUALIZACIÓN"
puts "=" * 70
if @opciones[:dry_run]
puts "🔍 MODO SIMULACIÓN - No se aplicaron cambios reales"
else
puts "🔄 CAMBIOS APLICADOS:"
if @cambios.any?
puts "#{@cambios.size} hitos agregados exitosamente"
@cambios.each { |h| puts "#{h}" }
else
puts "️ No se agregaron hitos nuevos"
end
if @backup_path
puts "💾 Backup creado en: #{@backup_path}"
end
end
if @errores.any?
puts "\n⚠️ ERRORES ENCONTRADOS (#{@errores.size}):"
@errores.each_with_index do |error, i|
puts " #{i + 1}. #{error}"
end
end
puts "\n🚀 PRÓXIMOS PASOS SUGERIDOS:"
puts " 1. Verificar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 2. Revisar que los nuevos hitos se muestren correctamente"
puts " 3. Actualizar estado de hitos según progreso real"
puts " 4. Integrar con herramientas ADN para seguimiento automático"
puts " 5. Documentar cambios en bitácora del proyecto"
puts "\n📅 " + Time.now.strftime('%Y-%m-%d %H:%M:%S')
puts "=" * 70
end
end
# Parsear opciones de línea de comandos
if __FILE__ == $0
opciones = {
dry_run: false,
backup: true,
hitos: nil,
categoria: nil,
debug: false
}
OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/actualizar_dashboard_p2601.rb [OPCIONES]"
opts.on("--dry-run", "Mostrar cambios sin aplicar") do
opciones[:dry_run] = true
end
opts.on("--no-backup", "No crear backup antes de modificar") do
opciones[:backup] = false
end
opts.on("--hitos N", Integer, "Agregar solo N hitos (por defecto: todos)") do |n|
opciones[:hitos] = n
end
opts.on("--categoria CAT", String, "Filtrar por categoría (infraestructura, monitoreo, etc.)") do |cat|
opciones[:categoria] = cat
end
opts.on("--debug", "Mostrar información de depuración") do
opciones[:debug] = true
end
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
puts "\nEjemplos:"
puts " ruby tools/actualizar_dashboard_p2601.rb --dry-run"
puts " ruby tools/actualizar_dashboard_p2601.rb --hitos 3 --categoria infraestructura"
puts " ruby tools/actualizar_dashboard_p2601.rb --no-backup"
exit
end
end.parse!
# Ejecutar actualizador
begin
actualizador = ActualizadorDashboardP2601.new(opciones)
actualizador.ejecutar
rescue => e
puts "❌ Error fatal: #{e.message}"
puts e.backtrace if opciones[:debug]
exit 1
end
end
-190
View File
@@ -1,190 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/agregar_hitos_dashboard.rb — Script simple para agregar hitos al dashboard P2601
# ================================================================================
# Este script agrega hitos adicionales al dashboard P2601 de forma manual y controlada.
# Es una versión simplificada que evita problemas de parsing complejo.
require 'fileutils'
# Hitos adicionales para agregar (formato simplificado)
HITOS_ADICIONALES = <<~HTML
<!-- Hitos adicionales agregados automáticamente - #{Time.now.strftime('%Y-%m-%d %H:%M')} -->
<div class="tl-item">
<div class="tl-dot pending"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">BKP02</span><span class="tl-date">Pendiente</span></div>
<div class="tl-title">🔒 Backup Automático de VMs en Proxmox</div>
<div class="tl-desc">Configurar backups automáticos VZDump programados para DC y SQL Server. Establecer retención de 7 días, almacenamiento en NAS/SMB compartido.</div>
<div class="tl-tags"><span class="tag tag-remote">Remoto: ~2h</span><span class="tag tag-phys">Requiere acceso físico</span></div>
</div>
</div>
<div class="tl-item">
<div class="tl-dot pending"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">DB02</span><span class="tl-date">Pendiente</span></div>
<div class="tl-title">🗄 Backup Automático de Base de Datos sysdasuten</div>
<div class="tl-desc">Configurar jobs de SQL Server Agent para backup diario completo y log cada 15 minutos. Verificar restauración periódica en ambiente de pruebas.</div>
<div class="tl-tags"><span class="tag tag-remote">Remoto: ~2h</span><span class="tag">SQL Agent</span></div>
</div>
</div>
<div class="tl-item">
<div class="tl-dot pending"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">MON01</span><span class="tl-date">Pendiente</span></div>
<div class="tl-title">📊 Monitoreo de Recursos del Hipervisor</div>
<div class="tl-desc">Implementar sistema de monitoreo básico para CPU, RAM, almacenamiento y red de srv-dasu. Alertas por email/Telegram para recursos críticos.</div>
<div class="tl-tags"><span class="tag tag-remote">Remoto: ~3h</span><span class="tag">Herramientas ADN</span></div>
</div>
</div>
<div class="tl-item">
<div class="tl-dot pending"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">DOC01</span><span class="tl-date">Pendiente</span></div>
<div class="tl-title">📋 Documentación Operativa del Sistema</div>
<div class="tl-desc">Crear manual de operaciones: procedimientos de backup/restore, acceso remoto, resolución de problemas comunes, contactos de soporte, diagramas de red.</div>
<div class="tl-tags"><span class="tag tag-remote">Remoto: ~6h</span><span class="tag">Herramientas ADN</span><span class="tag">Git</span></div>
</div>
</div>
<div class="tl-item">
<div class="tl-dot pending"></div>
<div class="tl-content">
<div class="tl-header"><span class="tl-id">PERF01</span><span class="tl-date">Pendiente</span></div>
<div class="tl-title">⚡ Pruebas de Rendimiento del Sistema</div>
<div class="tl-desc">Realizar pruebas de carga simulada en sistema DASUTEN. Medir tiempos de respuesta, consumo de recursos, identificar cuellos de botella. Generar reporte de performance.</div>
<div class="tl-tags"><span class="tag tag-phys">Físico: 2h</span><span class="tag tag-remote">Remoto: ~2h</span><span class="tag">SQL Profiler</span></div>
</div>
</div>
HTML
def main
puts "🔄 AGREGANDO HITOS AL DASHBOARD P2601"
puts "=" * 60
# Ruta del dashboard
dashboard_path = '/home/rmonla/Documentos/GitHub/dtic-srv-NS8/dashboard/index.html'
# Verificar que el archivo existe
unless File.exist?(dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{dashboard_path}"
exit 1
end
# Leer contenido actual
puts "📖 Leyendo dashboard actual..."
contenido = File.read(dashboard_path)
puts "✅ Dashboard leído (#{contenido.length} bytes, #{contenido.lines.count} líneas)"
# Crear backup
backup_path = "#{dashboard_path}.backup_#{Time.now.strftime('%Y%m%d_%H%M%S')}"
puts "💾 Creando backup en: #{backup_path}"
File.write(backup_path, contenido)
puts "✅ Backup creado exitosamente"
# Buscar el lugar donde insertar los hitos (antes del cierre del div .timeline)
# Buscamos el último </div> antes del footer
if contenido.include?('</div><!-- Timeline de Hitos -->')
puts "📍 Marcador de timeline encontrado"
marcador = '</div><!-- Timeline de Hitos -->'
elsif contenido.include?('</div>\n <div class="footer">')
puts "📍 Marcador antes del footer encontrado"
marcador = '</div>\n <div class="footer">'
else
# Buscar el patrón: cierre de timeline y luego footer
timeline_end_index = contenido.index('</div>\n <div class="footer">')
if timeline_end_index
puts "📍 Patrón timeline->footer encontrado"
marcador = '</div>\n <div class="footer">'
else
puts "⚠️ No se encontró marcador específico, usando búsqueda genérica"
# Buscar el último </div> antes del footer
footer_index = contenido.index('<div class="footer">')
if footer_index
# Retroceder para encontrar el </div> anterior
prev_div_index = contenido.rindex('</div>', footer_index)
if prev_div_index
marcador = contenido[prev_div_index..prev_div_index+5] # '</div>'
puts "📍 Último </div> antes del footer encontrado"
else
puts "❌ No se pudo encontrar lugar para insertar hitos"
exit 1
end
else
puts "❌ No se encontró el footer del dashboard"
exit 1
end
end
end
# Insertar los hitos antes del marcador
puts "✏️ Insertando #{HITOS_ADICIONALES.lines.count} líneas de hitos adicionales..."
nuevo_contenido = contenido.gsub(marcador) do |match|
"#{HITOS_ADICIONALES}\n\n #{match}"
end
# Verificar que se insertó correctamente
if nuevo_contenido.length > contenido.length
lineas_agregadas = nuevo_contenido.lines.count - contenido.lines.count
puts "✅ #{lineas_agregadas} líneas agregadas al dashboard"
puts "📊 Nuevo tamaño: #{nuevo_contenido.length} bytes (#{nuevo_contenido.lines.count} líneas)"
else
puts "⚠️ No se detectaron cambios en el contenido"
puts " Intentando método alternativo..."
# Método alternativo: insertar antes del footer
footer_start = '<div class="footer">'
if nuevo_contenido.include?(footer_start)
nuevo_contenido = nuevo_contenido.gsub(footer_start) do |match|
"#{HITOS_ADICIONALES}\n\n #{match}"
end
puts "✅ Hitos insertados antes del footer"
else
puts "❌ No se pudo insertar los hitos"
exit 1
end
end
# Guardar el archivo actualizado
puts "💾 Guardando dashboard actualizado..."
File.write(dashboard_path, nuevo_contenido)
puts "✅ Dashboard actualizado exitosamente"
# Verificación rápida
puts "\n🔍 VERIFICACIÓN RÁPIDA:"
puts " • Archivo original: #{contenido.lines.count} líneas"
puts " • Archivo nuevo: #{nuevo_contenido.lines.count} líneas"
puts " • Diferencia: #{nuevo_contenido.lines.count - contenido.lines.count} líneas agregadas"
# Mostrar algunos de los hitos agregados
puts "\n📋 HITOS AGREGADOS:"
puts " 1. 🔒 BKP02 - Backup Automático de VMs en Proxmox"
puts " 2. 🗄️ DB02 - Backup Automático de Base de Datos sysdasuten"
puts " 3. 📊 MON01 - Monitoreo de Recursos del Hipervisor"
puts " 4. 📋 DOC01 - Documentación Operativa del Sistema"
puts " 5. ⚡ PERF01 - Pruebas de Rendimiento del Sistema"
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Verificar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 2. Revisar que los hitos se muestren correctamente"
puts " 3. Actualizar estado según progreso real del proyecto"
puts " 4. Usar herramientas ADN para seguimiento automático"
puts " 5. Registrar cambios en bitácora del proyecto"
puts "\n📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
end
if __FILE__ == $0
begin
main
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-620
View File
@@ -1,620 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/analizar_dashboard_p2601.rb — Analizador del dashboard P2601 para proponer hitos adicionales
# ================================================================================
# Este script analiza el estado actual del dashboard P2601 y propone hitos
# intermedios adicionales para la fase de implementación final, utilizando
# herramientas ADN existentes y mejorándolas según sea necesario.
#
# Características:
# • Análisis del dashboard actual (HTML estático)
# • Integración con base de datos dtic-BITACORAs
# • Uso de herramientas ADN existentes
# • Propuesta de hitos basados en mejores prácticas
# • Generación de reporte estructurado
#
# Uso:
# ruby tools/analizar_dashboard_p2601.rb [OPCIONES]
#
# Opciones:
# --analizar-html Analizar dashboard/index.html actual
# --consultar-bd Consultar estado en base de datos
# --proponer-hitos Proponer hitos adicionales
# --generar-reporte Generar reporte completo
# --integrar-adn Integrar con herramientas ADN existentes
# --todo Ejecutar todas las opciones
require 'pg'
require 'yaml'
require 'json'
require 'nokogiri'
require 'optparse'
require 'date'
class AnalizadorDashboardP2601
def initialize(opciones = {})
@opciones = opciones
@config = cargar_configuracion_bd
@conn = nil
@dashboard_html = nil
@hitos_actuales = []
@hitos_propuestos = []
@estadisticas = {}
end
def ejecutar
puts "🔍 ANALIZADOR DE DASHBOARD P2601 - PROYECTO DASUTEN"
puts "=" * 70
puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts ""
if @opciones[:todo] || @opciones[:analizar_html]
analizar_dashboard_html
end
if @opciones[:todo] || @opciones[:consultar_bd]
consultar_base_datos
end
if @opciones[:todo] || @opciones[:proponer_hitos]
proponer_hitos_adicionales
end
if @opciones[:todo] || @opciones[:generar_reporte]
generar_reporte_completo
end
if @opciones[:todo] || @opciones[:integrar_adn]
integrar_con_herramientas_adn
end
mostrar_resumen
rescue => e
puts "❌ Error durante el análisis: #{e.message}"
puts e.backtrace if @opciones[:debug]
exit 1
ensure
desconectar_bd if @conn
end
private
def cargar_configuracion_bd
config_path = File.join(__dir__, 'config', 'database.yml')
unless File.exist?(config_path)
puts "⚠️ Archivo de configuración no encontrado: #{config_path}"
return {}
end
config = YAML.load_file(config_path)
config['development'] || config['production'] || config['test']
end
def conectar_bd
return unless @config && @config['host']
puts "🔌 Conectando a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}"
@conn = PG.connect(
host: @config['host'],
port: @config['port'],
dbname: @config['database'],
user: @config['username'],
password: @config['password']
)
puts "✅ Conexión establecida\n"
rescue PG::Error => e
puts "⚠️ Error de conexión a BD: #{e.message}"
puts " Continuando sin conexión a base de datos\n"
end
def desconectar_bd
@conn&.close
puts "\n🔌 Conexión cerrada" if @conn
end
def analizar_dashboard_html
puts "📋 ANALIZANDO DASHBOARD HTML ACTUAL"
puts "-" * 50
dashboard_path = File.join(__dir__, '..', '..', 'dashboard', 'index.html')
unless File.exist?(dashboard_path)
puts "❌ Archivo dashboard no encontrado: #{dashboard_path}"
return
end
@dashboard_html = File.read(dashboard_path)
doc = Nokogiri::HTML(@dashboard_html)
# Extraer hitos del timeline
@hitos_actuales = []
doc.css('.tl-item').each do |item|
hito = {
id: item.css('.tl-id').text.strip,
fecha: item.css('.tl-date').text.strip,
titulo: item.css('.tl-title').text.strip,
descripcion: item.css('.tl-desc').text.strip.gsub(/\s+/, ' '),
estado: determinar_estado_hito(item),
tags: item.css('.tag').map(&:text).map(&:strip)
}
@hitos_actuales << hito
end
# Extraer nodos
nodos = []
doc.css('.node-card').each do |node|
nodos << {
nombre: node.css('.node-name').text.strip,
estado: node.css('.status-dot').attr('title')&.value || 'desconocido',
detalles: node.css('.node-detail').text.strip.gsub(/\s+/, ' ')
}
end
# Extraer métricas
metricas = {}
doc.css('.metric').each do |metric|
label = metric.css('.label').text.strip
value = metric.css('.value').text.strip
metricas[label] = value if label && value
end
@estadisticas[:hitos_total] = @hitos_actuales.size
@estadisticas[:hitos_completados] = @hitos_actuales.count { |h| h[:estado] == 'completado' }
@estadisticas[:hitos_activos] = @hitos_actuales.count { |h| h[:estado] == 'activo' }
@estadisticas[:hitos_pendientes] = @hitos_actuales.count { |h| h[:estado] == 'pendiente' }
@estadisticas[:nodos_total] = nodos.size
@estadisticas[:nodos_online] = nodos.count { |n| n[:estado] == 'Online' }
puts "📊 Estadísticas extraídas:"
puts " • Hitos totales: #{@estadisticas[:hitos_total]}"
puts " • Hitos completados: #{@estadisticas[:hitos_completados]}"
puts " • Hitos activos: #{@estadisticas[:hitos_activos]}"
puts " • Hitos pendientes: #{@estadisticas[:hitos_pendientes]}"
puts " • Nodos totales: #{@estadisticas[:nodos_total]}"
puts " • Nodos online: #{@estadisticas[:nodos_online]}"
# Mostrar últimos hitos
puts "\n📅 ÚLTIMOS HITOS REGISTRADOS:"
@hitos_actuales.last(5).each do |hito|
estado_emoji = case hito[:estado]
when 'completado' then '✅'
when 'activo' then '🔵'
when 'pendiente' then '⏳'
else '❓'
end
puts " #{estado_emoji} #{hito[:id]}: #{hito[:titulo][0..60]}..."
end
puts ""
end
def determinar_estado_hito(item)
dot_class = item.css('.tl-dot').attr('class').to_s
if dot_class.include?('done')
'completado'
elsif dot_class.include?('active')
'activo'
elsif dot_class.include?('pending')
'pendiente'
else
'desconocido'
end
end
def consultar_base_datos
puts "📋 CONSULTANDO BASE DE DATOS dtic-BITACORAs"
puts "-" * 50
conectar_bd
return unless @conn
begin
# Consultar si existen tablas de proyectos
tablas = @conn.exec("SELECT table_name FROM information_schema.tables WHERE table_schema = 'bitacoras'")
tablas_proyecto = tablas.select { |t| t['table_name'] =~ /proyectos|fases|hitos/ }
if tablas_proyecto.any?
puts "✅ Tablas de proyectos encontradas: #{tablas_proyecto.map { |t| t['table_name'] }.join(', ')}"
# Consultar proyecto P2601
proyecto = @conn.exec_params(
"SELECT * FROM bitacoras.proyectos WHERE codigo = $1",
['P2601']
)
if proyecto.ntuples > 0
puts "✅ Proyecto P2601 encontrado en base de datos"
proyecto_info = proyecto[0]
puts " • Nombre: #{proyecto_info['nombre']}"
puts " • Estado: #{proyecto_info['estado']}"
puts " • Descripción: #{proyecto_info['descripcion'][0..100]}..." if proyecto_info['descripcion']
# Consultar fases del proyecto
fases = @conn.exec_params(
"SELECT * FROM bitacoras.fases WHERE proyecto_id = $1 ORDER BY numero",
[proyecto_info['id']]
)
puts " • Fases: #{fases.ntuples} fases encontradas"
fases.each do |fase|
puts " - Fase #{fase['numero']}: #{fase['nombre']} (#{fase['estado']})"
end
else
puts "⚠️ Proyecto P2601 no encontrado en base de datos"
end
else
puts "️ Tablas de proyectos no encontradas en el esquema 'bitacoras'"
puts " Considerar crear estructura: proyectos, fases, hitos"
end
# Consultar estadísticas de entradas relacionadas con DASUTEN
entradas_dasuten = @conn.exec_params(
"SELECT COUNT(*) as total FROM bitacoras.entradas WHERE descripcion ILIKE $1",
['%dasuten%']
)
if entradas_dasuten.ntuples > 0
total = entradas_dasuten[0]['total'].to_i
puts "📝 Entradas relacionadas con DASUTEN: #{total}"
@estadisticas[:entradas_dasuten] = total
end
rescue PG::Error => e
puts "⚠️ Error consultando base de datos: #{e.message}"
end
puts ""
end
def proponer_hitos_adicionales
puts "🎯 PROPONIENDO HITOS ADICIONALES PARA FASE DE IMPLEMENTACIÓN"
puts "-" * 50
# Basado en el análisis del dashboard y mejores prácticas
hitos_propuestos = [
{
id: "BKP02",
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.",
categoria: "Infraestructura",
prioridad: "alta",
estimacion_horas: 2,
prerequisitos: ["FÍSICO"],
herramientas: ["Proxmox VE", "VZDump", "Scripts Bash"]
},
{
id: "MON01",
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.",
categoria: "Monitoreo",
prioridad: "media",
estimacion_horas: 3,
prerequisitos: ["FÍSICO"],
herramientas: ["Prometheus Node Exporter", "Grafana", "Alertmanager"]
},
{
id: "DB02",
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.",
categoria: "Base de Datos",
prioridad: "alta",
estimacion_horas: 2,
prerequisitos: ["ADDOM"],
herramientas: ["SQL Server Management Studio", "SQL Agent", "PowerShell"]
},
{
id: "SEC01",
titulo: "Auditoría de Seguridad del Dominio",
descripcion: "Revisar y configurar políticas de auditoría en DC. Logs de acceso, cambios en AD, intentos fallidos. Centralización de logs.",
categoria: "Seguridad",
prioridad: "media",
estimacion_horas: 4,
prerequisitos: ["GPO"],
herramientas: ["Windows Event Viewer", "Audit Policy", "SIEM básico"]
},
{
id: "DOC01",
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.",
categoria: "Documentación",
prioridad: "media",
estimacion_horas: 6,
prerequisitos: ["UAT"],
herramientas: ["Markdown", "Git", "Herramientas ADN"]
},
{
id: "RED01",
titulo: "Optimización de Red y Firewall",
descripcion: "Revisar reglas de firewall en todas las VMs. Optimizar rutas Tailscale. Configurar QoS si es necesario.",
categoria: "Red",
prioridad: "baja",
estimacion_horas: 3,
prerequisitos: ["NET01"],
herramientas: ["Windows Firewall", "Tailscale", "PowerShell"]
},
{
id: "PERF01",
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.",
categoria: "Performance",
prioridad: "media",
estimacion_horas: 4,
prerequisitos: ["CLI"],
herramientas: ["SQL Profiler", "Performance Monitor", "Scripts de prueba"]
},
{
id: "MIG02",
titulo: "Plan de Migración de Datos Final",
descripcion: "Desarrollar script/procedimiento para migración incremental de datos desde sistema antiguo. Validar integridad de datos migrados.",
categoria: "Migración",
prioridad: "alta",
estimacion_horas: 8,
prerequisitos: ["P02.APP"],
herramientas: ["SQL Server Integration Services", "PowerShell", "Validación de datos"]
}
]
@hitos_propuestos = hitos_propuestos
puts "💡 HITOS PROPUESTOS (#{hitos_propuestos.size} nuevos):"
puts ""
hitos_propuestos.each do |hito|
prioridad_emoji = case hito[:prioridad]
when 'alta' then '🔴'
when 'media' then '🟡'
when 'baja' then '🟢'
else '⚪'
end
puts " #{prioridad_emoji} #{hito[:id]}: #{hito[:titulo]}"
puts " 📍 Categoría: #{hito[:categoria]}"
puts " ⏱️ Estimación: #{hito[:estimacion_horas]} horas"
puts " 📋 Prerequisitos: #{hito[:prerequisitos].join(', ')}" if hito[:prerequisitos].any?
puts " 🛠️ Herramientas: #{hito[:herramientas].join(', ')}"
puts ""
end
# Estadísticas de propuestas
categorias = hitos_propuestos.group_by { |h| h[:categoria] }
puts "📈 DISTRIBUCIÓN POR CATEGORÍA:"
categorias.each do |categoria, hitos|
puts "#{categoria}: #{hitos.size} hitos"
end
puts ""
end
def generar_reporte_completo
puts "📊 GENERANDO REPORTE COMPLETO"
puts "-" * 50
reporte = {
metadata: {
generado_en: Time.now.iso8601,
proyecto: "P2601 - DASUTEN",
version_analizador: "1.0.0"
},
estado_actual: @estadisticas,
hitos_actuales: @hitos_actuales,
hitos_propuestos: @hitos_propuestos,
recomendaciones: generar_recomendaciones,
acciones_sugeridas: generar_acciones_sugeridas
}
# Guardar reporte JSON
reporte_path = File.join(__dir__, '..', '..', 'tmp', 'reporte_p2601_analisis.json')
FileUtils.mkdir_p(File.dirname(reporte_path))
File.write(reporte_path, JSON.pretty_generate(reporte))
puts "✅ Reporte guardado en: #{reporte_path}"
puts " • Tamaño: #{File.size(reporte_path)} bytes"
puts " • Elementos: #{reporte[:hitos_actuales].size} hitos actuales, #{reporte[:hitos_propuestos].size} hitos propuestos"
puts ""
end
def generar_recomendaciones
recomendaciones = []
# Recomendaciones basadas en análisis
if @estadisticas[:hitos_pendientes] && @estadisticas[:hitos_pendientes] > 3
recomendaciones << "Priorizar hitos pendientes críticos (FÍSICO, ADDOM) antes de agregar nuevos"
end
if @estadisticas[:entradas_dasuten] && @estadisticas[:entradas_dasuten] < 20
recomendaciones << "Aumentar registro de actividades específicas de DASUTEN en bitácoras"
end
recomendaciones << "Integrar hitos propuestos en timeline del dashboard de forma gradual"
recomendaciones << "Utilizar herramientas ADN para automatizar seguimiento de hitos"
recomendaciones << "Considerar implementar sistema de notificaciones para hitos críticos"
recomendaciones << "Documentar cada hito completado con lecciones aprendidas"
recomendaciones
end
def generar_acciones_sugeridas
acciones = [
{
accion: "Actualizar dashboard HTML",
descripcion: "Agregar hitos propuestos al timeline",
prioridad: "media",
herramientas: ["HTML/CSS", "Herramientas ADN"]
},
{
accion: "Crear estructura en base de datos",
descripcion: "Implementar tablas proyectos/fases/hitos si no existen",
prioridad: "alta",
herramientas: ["PostgreSQL", "SQL migrations"]
},
{
accion: "Integrar con sistema de búsqueda",
descripcion: "Conectar hitos con sistema de búsqueda ADN BITACORAs",
prioridad: "media",
herramientas: ["BuscadorBDSimple", "API REST"]
},
{
accion: "Automatizar actualizaciones",
descripcion: "Crear script para actualizar dashboard desde base de datos",
prioridad: "baja",
herramientas: ["Ruby", "Cron jobs", "Herramientas ADN"]
},
{
accion: "Implementar métricas avanzadas",
descripcion: "Agregar KPIs de progreso, desviaciones, pronósticos",
prioridad: "baja",
herramientas: ["JavaScript", "Chart.js", "API"]
}
]
acciones
end
def integrar_con_herramientas_adn
puts "🔄 INTEGRANDO CON HERRAMIENTAS ADN EXISTENTES"
puts "-" * 50
puts "🔍 Herramientas ADN disponibles:"
# Verificar herramientas existentes
herramientas_adn = [
{ nombre: "Sistema de búsqueda", archivo: "buscador_bd_simple.rb", estado: verificar_herramienta("adn/db/core/buscador_bd_simple.rb") },
{ nombre: "Seguimiento de plan", archivo: "subcomando_plan.rb", estado: verificar_herramienta("adn/subcomando_plan.rb") },
{ nombre: "Operaciones BD", archivo: "subcomando_db.rb", estado: verificar_herramienta("adn/subcomando_db.rb") },
{ nombre: "Sistema de triggers", archivo: "subcomando_triggers.rb", estado: verificar_herramienta("adn/subcomando_triggers.rb") },
{ nombre: "Logger ADN", archivo: "logger.rb", estado: verificar_herramienta("adn/logger.rb") }
]
herramientas_adn.each do |herramienta|
estado_emoji = herramienta[:estado] ? "" : ""
puts " #{estado_emoji} #{herramienta[:nombre]}: #{herramienta[:archivo]}"
end
puts "\n💡 OPORTUNIDADES DE INTEGRACIÓN:"
puts " 1. Usar BuscadorBDSimple para buscar hitos relacionados con DASUTEN"
puts " 2. Integrar hitos del dashboard con el sistema de seguimiento de plan ADN"
puts " 3. Usar subcomando_db para registrar hitos completados en base de datos"
puts " 4. Implementar triggers para notificar cambios en estado de hitos"
puts " 5. Usar logger ADN para registrar actividades de seguimiento"
puts "\n🚀 ACCIONES RECOMENDADAS:"
puts " • Crear comando: adn proyecto:hitos --proyecto P2601"
puts " • Integrar dashboard con API de proyectos en dtic-BITACORAs"
puts " • Automatizar actualización de métricas del dashboard"
puts " • Implementar sistema de notificaciones para hitos críticos"
puts ""
end
def verificar_herramienta(ruta_relativa)
File.exist?(File.join(__dir__, '..', ruta_relativa))
end
def mostrar_resumen
puts "=" * 70
puts "📊 RESUMEN DEL ANÁLISIS DASHBOARD P2601"
puts "=" * 70
puts "\n🎯 ESTADO ACTUAL:"
puts " • Hitos totales: #{@estadisticas[:hitos_total] || 'N/A'}"
puts " • Completados: #{@estadisticas[:hitos_completados] || 'N/A'}"
puts " • Activos: #{@estadisticas[:hitos_activos] || 'N/A'}"
puts " • Pendientes: #{@estadisticas[:hitos_pendientes] || 'N/A'}"
puts " • Nodos online: #{@estadisticas[:nodos_online] || 'N/A'}/#{@estadisticas[:nodos_total] || 'N/A'}"
puts "\n💡 HITOS PROPUESTOS: #{@hitos_propuestos.size}"
categorias = @hitos_propuestos.group_by { |h| h[:categoria] }
categorias.each do |categoria, hitos|
puts "#{categoria}: #{hitos.size} hitos"
end
puts "\n🔄 INTEGRACIÓN ADN:"
puts " • Herramientas disponibles: 5 verificadas"
puts " • Oportunidades identificadas: 5 áreas"
puts "\n📈 RECOMENDACIONES PRINCIPALES:"
puts " 1. Priorizar hitos FÍSICO y ADDOM antes de agregar nuevos"
puts " 2. Integrar hitos propuestos gradualmente en el dashboard"
puts " 3. Usar herramientas ADN para automatizar seguimiento"
puts " 4. Considerar implementar sistema de notificaciones"
puts " 5. Documentar lecciones aprendidas de cada hito"
puts "\n🚀 PRÓXIMOS PASOS SUGERIDOS:"
puts " 1. Ejecutar: ruby tools/analizar_dashboard_p2601.rb --proponer-hitos"
puts " 2. Revisar reporte en tmp/reporte_p2601_analisis.json"
puts " 3. Actualizar dashboard/index.html con hitos adicionales"
puts " 4. Integrar con sistema de búsqueda ADN BITACORAs"
puts " 5. Automatizar actualizaciones del dashboard"
puts "\n📅 " + Time.now.strftime('%Y-%m-%d %H:%M:%S')
puts "=" * 70
end
end
# Parsear opciones de línea de comandos
if __FILE__ == $0
opciones = {
analizar_html: false,
consultar_bd: false,
proponer_hitos: false,
generar_reporte: false,
integrar_adn: false,
debug: false
}
OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/analizar_dashboard_p2601.rb [OPCIONES]"
opts.on("--analizar-html", "Analizar dashboard/index.html actual") do
opciones[:analizar_html] = true
end
opts.on("--consultar-bd", "Consultar estado en base de datos") do
opciones[:consultar_bd] = true
end
opts.on("--proponer-hitos", "Proponer hitos adicionales") do
opciones[:proponer_hitos] = true
end
opts.on("--generar-reporte", "Generar reporte completo") do
opciones[:generar_reporte] = true
end
opts.on("--integrar-adn", "Integrar con herramientas ADN existentes") do
opciones[:integrar_adn] = true
end
opts.on("--todo", "Ejecutar todas las opciones") do
opciones.each_key { |k| opciones[k] = true }
end
opts.on("--debug", "Mostrar información de depuración") do
opciones[:debug] = true
end
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
exit
end
end.parse!
# Si no se especificó ninguna opción, mostrar ayuda
if opciones.values.none? { |v| v == true }
puts "Ejecutar con --help para ver opciones disponibles"
puts "Ejemplo: ruby tools/analizar_dashboard_p2601.rb --todo"
exit
end
# Ejecutar analizador
begin
analizador = AnalizadorDashboardP2601.new(opciones)
analizador.ejecutar
rescue => e
puts "❌ Error fatal: #{e.message}"
puts e.backtrace if opciones[:debug]
exit 1
end
end
-307
View File
@@ -1,307 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/analizar_esquema_busqueda.rb — Análisis de esquema para sistema de búsqueda
# ================================================================================
# Este script analiza el esquema de la base de datos dtic-BITACORAs para diseñar
# el sistema de búsqueda y filtrado (Tarea 2.2 del Plan ADN BITACORAs).
#
# Uso:
# ruby tools/analizar_esquema_busqueda.rb
#
# Salida:
# • Estructura completa de tablas y columnas
# • Índices existentes
# • Relaciones entre tablas
# • Campos recomendados para búsqueda
# • Índices recomendados para optimización
require 'pg'
require 'yaml'
class AnalizadorEsquemaBusqueda
def initialize
@config = cargar_configuracion
@conn = nil
end
def ejecutar
conectar_bd
analizar_esquema
ensure
desconectar_bd
end
private
def cargar_configuracion
config_path = File.join(__dir__, 'config', 'database.yml')
unless File.exist?(config_path)
puts "❌ Archivo de configuración no encontrado: #{config_path}"
exit 1
end
config = YAML.load_file(config_path)
config['development'] || config['production'] || config['test']
end
def conectar_bd
puts "🔌 Conectando a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}"
@conn = PG.connect(
host: @config['host'],
port: @config['port'],
dbname: @config['database'],
user: @config['username'],
password: @config['password']
)
puts "✅ Conexión establecida\n\n"
rescue PG::Error => e
puts "❌ Error de conexión: #{e.message}"
exit 1
end
def desconectar_bd
@conn&.close
puts "\n🔌 Conexión cerrada"
end
def analizar_esquema
puts "📊 ANÁLISIS DE ESQUEMA PARA SISTEMA DE BÚSQUEDA"
puts "=" * 70
# Tablas principales del sistema
tablas = ['nodos', 'bitacoras', 'temas', 'entradas']
tablas.each do |tabla|
analizar_tabla(tabla)
end
analizar_relaciones
analizar_campos_busqueda
recomendar_indices
diseñar_sistema_busqueda
end
def analizar_tabla(nombre_tabla)
puts "\n🔍 TABLA: #{nombre_tabla}"
puts "-" * 50
# Obtener columnas
columnas = @conn.exec_params(
"SELECT column_name, data_type, character_maximum_length,
is_nullable, column_default
FROM information_schema.columns
WHERE table_name = $1 AND table_schema = 'bitacoras'
ORDER BY ordinal_position",
[nombre_tabla]
)
puts " 📋 Columnas (#{columnas.ntuples}):"
columnas.each do |col|
tipo = col['data_type']
longitud = col['character_maximum_length'] ? "(#{col['character_maximum_length']})" : ''
nulo = col['is_nullable'] == 'YES' ? 'NULL' : 'NOT NULL'
defecto = col['column_default'] ? " [DEFAULT: #{col['column_default']}]" : ''
puts "#{col['column_name']}: #{tipo}#{longitud} #{nulo}#{defecto}"
end
# Obtener índices
indices = @conn.exec_params(
"SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = $1 AND schemaname = 'bitacoras'
ORDER BY indexname",
[nombre_tabla]
)
puts "\n 🔗 Índices (#{indices.ntuples}):"
if indices.ntuples > 0
indices.each do |idx|
definicion = idx['indexdef']
.gsub('CREATE ', '')
.gsub(" ON bitacoras.#{nombre_tabla} USING ", ' → ')
puts "#{idx['indexname']}: #{definicion}"
end
else
puts " • (sin índices)"
end
# Contar registros
conteo = @conn.exec_params(
"SELECT COUNT(*) as total FROM bitacoras.#{nombre_tabla}",
[]
)[0]['total']
puts "\n 📊 Registros: #{conteo}"
end
def analizar_relaciones
puts "\n🔗 RELACIONES ENTRE TABLAS"
puts "-" * 50
query = <<-SQL
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM
information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE
tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'bitacoras'
ORDER BY tc.table_name, kcu.column_name;
SQL
relaciones = @conn.exec(query)
if relaciones.ntuples > 0
relaciones.each do |rel|
puts "#{rel['table_name']}.#{rel['column_name']}#{rel['foreign_table_name']}.#{rel['foreign_column_name']}"
end
else
puts " • (no se encontraron relaciones explícitas con FOREIGN KEY)"
end
# Análisis de relaciones implícitas
puts "\n 🔍 Relaciones implícitas (por nombre de columna):"
puts " • entradas.nodo_id → nodos.id"
puts " • entradas.tema_id → temas.id"
puts " • entradas.bitacora_id → bitacoras.id"
puts " • temas.nodo_id → nodos.id"
end
def analizar_campos_busqueda
puts "\n🎯 CAMPOS PARA BÚSQUEDA Y FILTRADO"
puts "-" * 50
campos_por_tabla = {
'nodos' => [
{nombre: 'nombre', tipo: 'texto', busqueda: 'exacta/parcial', filtro: 'si'},
{nombre: 'tipo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'}
],
'bitacoras' => [
{nombre: 'fecha', tipo: 'date', busqueda: 'rango', filtro: 'si'},
{nombre: 'resumen', tipo: 'texto', busqueda: 'full-text', filtro: 'no'}
],
'temas' => [
{nombre: 'nombre', tipo: 'texto', busqueda: 'exacta/parcial', filtro: 'si'},
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
{nombre: 'modo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'}
],
'entradas' => [
{nombre: 'inicio', tipo: 'time', busqueda: 'rango', filtro: 'si'},
{nombre: 'fin', tipo: 'time', busqueda: 'rango', filtro: 'si'},
{nombre: 'descripcion', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
{nombre: 'detalle', tipo: 'texto', busqueda: 'full-text', filtro: 'no'},
{nombre: 'estado', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
{nombre: 'modo', tipo: 'texto', busqueda: 'exacta', filtro: 'si'},
{nombre: 'created_at', tipo: 'timestamp', busqueda: 'rango', filtro: 'si'}
]
}
campos_por_tabla.each do |tabla, campos|
puts "\n 📋 #{tabla}:"
campos.each do |campo|
# Verificar si el campo existe
existe = @conn.exec_params(
"SELECT column_name FROM information_schema.columns
WHERE table_name = $1 AND table_schema = 'bitacoras'
AND column_name = $2",
[tabla, campo[:nombre]]
)
if existe.ntuples > 0
icono = campo[:filtro] == 'si' ? '🔍' : '📝'
puts " #{icono} #{campo[:nombre]} (#{campo[:tipo]}) - Búsqueda: #{campo[:busqueda]}, Filtro: #{campo[:filtro]}"
end
end
end
end
def recomendar_indices
puts "\n💡 ÍNDICES RECOMENDADOS PARA OPTIMIZACIÓN"
puts "-" * 50
puts " 1. Para búsqueda full-text:"
puts " • CREATE INDEX idx_entradas_descripcion_fts ON bitacoras.entradas"
puts " USING gin(to_tsvector('spanish', descripcion));"
puts " • CREATE INDEX idx_entradas_detalle_fts ON bitacoras.entradas"
puts " USING gin(to_tsvector('spanish', detalle));"
puts " • CREATE INDEX idx_temas_descripcion_fts ON bitacoras.temas"
puts " USING gin(to_tsvector('spanish', descripcion));"
puts "\n 2. Para filtrado frecuente:"
puts " • CREATE INDEX idx_entradas_estado ON bitacoras.entradas(estado);"
puts " • CREATE INDEX idx_entradas_modo ON bitacoras.entradas(modo);"
puts " • CREATE INDEX idx_entradas_created_at ON bitacoras.entradas(created_at);"
puts " • CREATE INDEX idx_bitacoras_fecha ON bitacoras.bitacoras(fecha);"
puts " • CREATE INDEX idx_nodos_tipo ON bitacoras.nodos(tipo);"
puts "\n 3. Para joins y relaciones:"
puts " • CREATE INDEX idx_entradas_nodo_id ON bitacoras.entradas(nodo_id);"
puts " • CREATE INDEX idx_entradas_tema_id ON bitacoras.entradas(tema_id);"
puts " • CREATE INDEX idx_entradas_bitacora_id ON bitacoras.entradas(bitacora_id);"
puts " • CREATE INDEX idx_temas_nodo_id ON bitacoras.temas(nodo_id);"
end
def diseñar_sistema_busqueda
puts "\n🚀 DISEÑO DEL SISTEMA DE BÚSQUEDA Y FILTRADO"
puts "-" * 50
puts " 1. 📋 Comandos CLI propuestos:"
puts " • adn db buscar:entradas [TEXTO] [OPCIONES]"
puts " • adn db filtrar:entradas [--estado ESTADO] [--modo MODO] [--fecha DESDE-HASTA]"
puts " • adn db buscar:nodos [TEXTO] [--tipo TIPO]"
puts " • adn db buscar:temas [TEXTO] [--nodo NODO]"
puts " • adn db estadisticas:busquedas [--periodo PERIODO]"
puts "\n 2. 🔧 Opciones de búsqueda avanzada:"
puts " • --exacto: Búsqueda exacta (no parcial)"
puts " • --campo CAMPO: Buscar solo en campo específico"
puts " • --limite N: Limitar resultados"
puts " • --ordenar CAMPO: Ordenar por campo (fecha, estado, etc.)"
puts " • --formato FORMATO: Salida en texto, JSON o CSV"
puts "\n 3. 📊 Tipos de búsqueda implementados:"
puts " • Full-text search: PostgreSQL tsvector/tsquery"
puts " • Búsqueda parcial: ILIKE '%texto%'"
puts " • Búsqueda exacta: = 'texto'"
puts " • Filtrado por rango: BETWEEN, >, <"
puts " • Filtrado por lista: IN (...)"
puts "\n 4. 🏗️ Arquitectura del módulo:"
puts " • Clase BuscadorBD: Lógica central de búsqueda"
puts " • Clase FiltrosBD: Sistema de filtrado"
puts " • Clase ExportadorResultados: Formatos de salida"
puts " • Integración con BitacoraDB existente"
puts "\n 5. 📈 Métricas de rendimiento:"
puts " • Tiempo de respuesta < 500ms para 10k registros"
puts " • Cache de consultas frecuentes"
puts " • Paginación automática (100 resultados/página)"
end
end
# Ejecutar análisis
if __FILE__ == $0
begin
analizador = AnalizadorEsquemaBusqueda.new
analizador.ejecutar
rescue => e
puts "❌ Error durante el análisis: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-415
View File
@@ -1,415 +0,0 @@
# Orquestador de Herramientas - Dashboard P2601
## Filosofía: "Pequeñas herramientas coordinadas hacen al todo"
> **NOTA**: Esta versión ha sido optimizada con una estructura modular.
> Ver [README_OPTIMIZADO.md](README_OPTIMIZADO.md) para la nueva arquitectura.
Este directorio contiene herramientas pequeñas, específicas y reutilizables para gestionar el dashboard del proyecto P2601. Cada herramienta tiene un propósito único y claro, siguiendo el principio de que herramientas pequeñas combinadas son más efectivas que una herramienta monolítica.
## 🚀 Nueva Estructura Modular (Optimizada)
Se ha implementado una nueva arquitectura modular que centraliza funcionalidades comunes y promueve la reutilización de código:
```
tools/orquestador/
├── dashboard/ # Herramientas específicas (legado)
├── lib/ # 📦 Módulos centrales reutilizables
│ ├── dashboard_core.rb # Módulo central con funcionalidades comunes
│ ├── hitos/manager.rb # Gestión centralizada de hitos
│ ├── estados/manager.rb # Gestión centralizada de estados
│ ├── verificacion/manager.rb # Verificación centralizada
│ ├── vista_previa/manager.rb # Vista previa multi-modo
│ └── sincronizacion/manager.rb # Sincronización multi-ambiente
├── config.rb # Configuración centralizada
├── migrar_ids_hitos.rb # Migración a nuevo sistema de IDs
├── ejemplo_modulos_optimizados.rb # Ejemplos de uso
└── README_OPTIMIZADO.md # Documentación completa
```
### 🎯 Nuevo Sistema de IDs de Hitos
- **Formato**: `f{numero_fase}{secuencia de 3 dígitos}`
- **Ejemplos**: `f05001` (Fase 5, secuencia 1), `f05002` (Fase 5, secuencia 2)
### 🔄 Migración Recomendada
1. Verificar estado actual: `./migrar_ids_hitos.rb verificar`
2. Simular migración: `./migrar_ids_hitos.rb simular`
3. Realizar migración: `./migrar_ids_hitos.rb migrar`
### 📚 Documentación Completa
- [README_OPTIMIZADO.md](README_OPTIMIZADO.md) - Nueva arquitectura modular
- [config.rb](config.rb) - Configuración centralizada
- [ejemplo_modulos_optimizados.rb](ejemplo_modulos_optimizados.rb) - Ejemplos de uso
## Estructura (Versión Original)
```
tools/orquestador/
├── dashboard/ # Herramientas específicas para el dashboard
│ ├── limpiar.rb # Limpiar hitos duplicados
│ ├── agregar_hitos.rb # Agregar hitos de forma controlada
│ ├── verificar.rb # Verificar estructura y estado
│ ├── actualizar_estados.rb # Actualizar estados de hitos
│ ├── vista_previa.rb # Vista previa rápida del dashboard
│ ├── sincronizar.rb # Sincronizar dashboard local con producción
│ ├── agregar_hitos_db.rb # Agregar hitos a base de datos PostgreSQL
│ └── orquestador.rb # Script maestro para orquestar todas las herramientas
└── README.md # Este archivo
```
## Herramientas Disponibles (Versión Original)
> **NOTA**: Estas herramientas están siendo migradas a la nueva estructura modular.
> Para funcionalidades optimizadas, usar los módulos en `lib/`.
### 1. `limpiar.rb` - Limpiador de Dashboard
**Propósito**: Eliminar hitos duplicados y corregir estructura del dashboard.
**Equivalente modular**: `DashboardCore::HitosManager#limpiar_duplicados`
**Uso**:
```bash
cd tools/orquestador/dashboard
./limpiar.rb
```
**Características**:
- Crea backup automático del dashboard
- Detecta y elimina hitos duplicados
- Reconstruye estructura correcta de timeline
- Muestra estadísticas de limpieza
### 2. `agregar_hitos.rb` - Gestor de Hitos
**Propósito**: Agregar hitos al dashboard de forma controlada y estructurada.
**Equivalente modular**: `DashboardCore::HitosManager#agregar_hitos_fase`
**Uso**:
```bash
# Agregar hitos de fase 5 (modo predeterminado)
./agregar_hitos.rb agregar fase5 append
# Listar hitos disponibles
./agregar_hitos.rb listar fase5
# Mostrar ayuda
./agregar_hitos.rb help
```
**Modos de operación**:
- `append`: Agregar hitos al final de la timeline
- `replace`: Reemplazar hitos existentes de una fase
- `insert`: Insertar después de un marcador específico
**Hitos predefinidos (fase5)**:
- `BKP02`: Backup Automático de VMs en Proxmox
- `DB02`: Backup Automático de Base de Datos sysdasuten
- `MON01`: Monitoreo de Recursos del Hipervisor
- `DOC01`: Documentación Operativa del Sistema
- `PERF01`: Pruebas de Rendimiento del Sistema
### 3. `verificar.rb` - Verificador de Dashboard
**Propósito**: Verificar estructura y analizar estado del dashboard.
**Equivalente modular**: `DashboardCore::VerificacionManager#verificar_completo`
**Uso**:
```bash
# Verificar estructura básica
./verificar.rb verificar
# Analizar hitos en detalle
./verificar.rb analizar
# Mostrar ayuda
./verificar.rb help
```
**Verificaciones realizadas**:
- Estructura HTML básica
- Presencia de elementos requeridos
- Integridad de hitos (sin duplicados)
- Estado de la timeline
- Métricas y estadísticas
### 4. `actualizar_estados.rb` - Gestor de Estados de Hitos
**Propósito**: Actualizar el estado de hitos en el dashboard.
**Equivalente modular**: `DashboardCore::EstadosManager#actualizar_estado_hito`
**Uso**:
```bash
# Listar hitos con sus estados
./actualizar_estados.rb listar
# Actualizar estado de un hito
./actualizar_estados.rb actualizar BKP02 in-progress
# Actualizar múltiples hitos
./actualizar_estados.rb multiples BKP02 in-progress DB02 completed
# Mostrar estados disponibles
./actualizar_estados.rb estados
```
**Estados disponibles**:
- `pending`: Pendiente (amarillo)
- `in-progress`: En Progreso (azul)
- `completed`: Completado (verde)
- `blocked`: Bloqueado (rojo)
- `on-hold`: En Espera (púrpura)
### 5. `vista_previa.rb` - Vista Previa Rápida
**Propósito**: Obtener vista previa rápida del dashboard.
**Equivalente modular**: `DashboardCore::VistaPreviaManager#mostrar_vista_previa`
**Uso**:
```bash
# Resumen completo
./vista_previa.rb resumen
# Hitos detallados
./vista_previa.rb hitos
# Estructura del dashboard
./vista_previa.rb estructura
# Estadísticas detalladas
./vista_previa.rb estadisticas
# Resumen rápido
./vista_previa.rb rapido
```
**Características**:
- Múltiples modos de visualización
- Análisis de estructura HTML
- Estadísticas de hitos y estados
- Métricas clave del proyecto
### 6. `orquestador.rb` - Script Maestro de Orquestación
**Propósito**: Orquestar todas las herramientas en flujos de trabajo predefinidos.
### 7. `sincronizar.rb` - Sincronizador de Dashboard
**Propósito**: Sincronizar dashboard local con producción.
**Equivalente modular**: `DashboardCore::SincronizacionManager#sincronizar_a_produccion`
### 8. `agregar_hitos_db.rb` - Gestor de Hitos en Base de Datos
**Propósito**: Agregar hitos a la base de datos PostgreSQL.
**Uso**:
```bash
# Ejecutar una herramienta específica
./orquestador.rb herramienta limpiar
# Ejecutar flujo de trabajo predefinido
./orquestador.rb flujo seguimiento-diario
# Listar herramientas disponibles
./orquestador.rb listar-herramientas
# Listar flujos de trabajo
./orquestador.rb listar-flujos
# Mostrar estado del sistema
./orquestador.rb estado
```
**Flujos de trabajo predefinidos**:
- `inicializar`: Inicializar dashboard limpio con hitos de fase 5
- `seguimiento-diario`: Flujo para seguimiento diario del proyecto
- `revision-semanal`: Flujo para revisión semanal completa
- `agregar-fase5`: Agregar hitos de fase 5 y verificar
## Flujo de Trabajo Recomendado
### Para nuevo proyecto o limpieza completa:
1. **Verificar estado actual**:
```bash
./orquestador.rb flujo revision-semanal
```
2. **Inicializar dashboard limpio**:
```bash
./orquestador.rb flujo inicializar
```
### Para seguimiento diario:
```bash
./orquestador.rb flujo seguimiento-diario
```
### Para agregar hitos de fase 5:
```bash
./orquestador.rb flujo agregar-fase5
```
## Principios de Diseño
1. **Una herramienta, un propósito**: Cada herramienta hace una cosa y la hace bien.
2. **Composición sobre monolitos**: Las herramientas se pueden combinar para flujos complejos.
3. **Backup automático**: Todas las herramientas crean backups antes de modificar.
4. **Feedback claro**: Proporcionan información detallada sobre lo que hacen.
5. **Interfaz simple**: Uso intuitivo desde línea de comandos.
6. **Reutilización**: Código modular que se puede integrar en otros sistemas.
## Integración con Herramientas ADN
Estas herramientas están diseñadas para complementar (no reemplazar) las herramientas existentes en `tools/adn`. Se pueden usar conjuntamente:
1. **Herramientas ADN**: Para automatización compleja y gestión de eventos
2. **Herramientas Orquestador**: Para tareas específicas de dashboard y estructura
## 🚀 Migración a la Nueva Estructura
### Ventajas de la Nueva Arquitectura:
1. **Reutilización de código**: Funcionalidades comunes centralizadas
2. **Consistencia**: Sistema de IDs uniforme (`fNNNNN`)
3. **Mantenibilidad**: Módulos independientes con responsabilidades únicas
4. **Escalabilidad**: Fácil adición de nuevas funcionalidades
### Pasos de Migración:
```bash
# 1. Verificar estado actual de IDs
./migrar_ids_hitos.rb verificar
# 2. Simular migración (modo dry-run)
./migrar_ids_hitos.rb simular
# 3. Realizar migración completa
./migrar_ids_hitos.rb migrar
# 4. Probar nuevos módulos
./ejemplo_modulos_optimizados.rb
```
### Mapeo de IDs (Fase 5):
- `BKP02` → `f05001` (Backup Automático de VMs)
- `DB02` → `f05002` (Backup Automático de Base de Datos)
- `MON01` → `f05003` (Monitoreo de Recursos)
- `DOC01` → `f05004` (Documentación Operativa)
- `PERF01` → `f05005` (Pruebas de Rendimiento)
## Características Técnicas
### Backups Automáticos
- Todas las herramientas crean backups antes de modificar
- Backups organizados por fecha/hora en `dashboard/backups/`
- Formato: `dashboard_[tipo]_YYYYMMDD_HHMMSS.html`
### Manejo de Errores
- Validación de parámetros de entrada
- Mensajes de error descriptivos
- Códigos de salida significativos
- Opción `DEBUG=true` para detalles técnicos
### Compatibilidad
- Ruby estándar (sin dependencias externas)
- Compatible con cualquier sistema Unix/Linux
- Encoding UTF-8
## Ejemplos de Uso Avanzado
### Actualizar estados basados en progreso real:
```bash
# Marcar hitos en progreso
./actualizar_estados.rb actualizar BKP02 in-progress
# Marcar hitos completados con fecha específica
./actualizar_estados.rb actualizar DB02 completed 2024-03-05
# Actualizar múltiples hitos
./actualizar_estados.rb multiples \
MON01 in-progress \
DOC01 pending \
PERF01 on-hold
```
### Análisis detallado del dashboard:
```bash
# Ver estructura completa
./vista_previa.rb estructura
# Analizar estadísticas
./vista_previa.rb estadisticas
# Verificar integridad
./verificar.rb verificar
```
## Mantenimiento
### Backups
- Se guardan automáticamente en `dashboard/backups/`
- Se recomienda limpiar backups antiguos periódicamente
- Formato: `dashboard_[tipo]_YYYYMMDD_HHMMSS.html`
### Logs y Depuración
```bash
# Activar modo debug
DEBUG=true ./verificar.rb verificar
# Verificar sintaxis de herramientas
ruby -c limpiar.rb
```
### Actualizaciones
- Mantener la filosofía de herramientas pequeñas y específicas
- Documentar cambios en este README
- Probar flujos de trabajo después de modificaciones
## Próximos Pasos
### ✅ Completado (Nueva Estructura):
1. **Crear módulos centrales** reutilizables en `lib/`
2. **Implementar nuevo sistema de IDs** (`fNNNNN`)
3. **Documentar arquitectura modular** en `README_OPTIMIZADO.md`
4. **Crear scripts de migración** y ejemplos de uso
### 🔄 En Progreso:
1. **Migrar herramientas existentes** a la nueva arquitectura
2. **Crear tests automatizados** para cada módulo
3. **Implementar logging centralizado**
### 📋 Planificado:
1. **Integración con APIs externas**
2. **Dashboard en tiempo real**
3. **Reportes automatizados por email**
## 📚 Documentación Adicional
### Nueva Arquitectura:
- [README_OPTIMIZADO.md](README_OPTIMIZADO.md) - Documentación completa de la nueva estructura
- [config.rb](config.rb) - Configuración centralizada
- [ejemplo_modulos_optimizados.rb](ejemplo_modulos_optimizados.rb) - Ejemplos de uso práctico
### Herramientas de Migración:
- [migrar_ids_hitos.rb](migrar_ids_hitos.rb) - Migración de IDs al nuevo formato
- Script interactivo con modos: `verificar`, `simular`, `migrar`
### Módulos Principales:
- `DashboardCore::HitosManager` - Gestión completa de hitos
- `DashboardCore::EstadosManager` - Gestión de estados
- `DashboardCore::VerificacionManager` - Verificación exhaustiva
- `DashboardCore::VistaPreviaManager` - Vista previa multi-modo
- `DashboardCore::SincronizacionManager` - Sincronización multi-ambiente
## Contacto
Para problemas o sugerencias:
- Revisar la bitácora del proyecto P2601
- Verificar backups en `dashboard/backups/`
- Usar modo debug para detalles técnicos: `DEBUG=true`
## Dashboard en Producción
🔗 **URL**: https://ns8.frlr.utn.edu.ar/bitacoras/p2601
**Recomendaciones**:
- Verificar cambios en producción después de cada modificación
- Usar flujo `seguimiento-diario` para monitoreo regular
- Mantener backups organizados por fecha
---
*Última actualización: 2024-03-06*
*Versión del sistema: 2.0.0 (Estructura Modular)*
*Documentación optimizada: [README_OPTIMIZADO.md](README_OPTIMIZADO.md)*
*Autor: Sistema de Orquestación P2601*
*Filosofía: "Pequeñas herramientas coordinadas hacen al todo"*
-333
View File
@@ -1,333 +0,0 @@
# Orquestador de Herramientas - Dashboard P2601 (Estructura Optimizada)
## Filosofía: "Pequeñas herramientas coordinadas hacen al todo"
Esta versión optimizada del orquestador implementa una arquitectura modular basada en componentes reutilizables, siguiendo el principio de que herramientas pequeñas, específicas y bien coordinadas son más efectivas que una herramienta monolítica.
## 🏗️ Nueva Estructura Modular
```
tools/orquestador/
├── dashboard/ # Herramientas específicas para el dashboard (legado)
│ ├── limpiar.rb # Limpiar hitos duplicados
│ ├── agregar_hitos.rb # Agregar hitos de forma controlada
│ ├── verificar.rb # Verificar estructura y estado
│ ├── actualizar_estados.rb # Actualizar estados de hitos
│ ├── vista_previa.rb # Vista previa rápida del dashboard
│ ├── sincronizar.rb # Sincronizar dashboard local con producción
│ ├── agregar_hitos_db.rb # Agregar hitos a base de datos PostgreSQL
│ └── orquestador.rb # Script maestro para orquestar todas las herramientas
├── lib/ # 📦 Módulos centrales reutilizables
│ ├── dashboard_core.rb # Módulo central con funcionalidades comunes
│ │
│ ├── hitos/ # Gestión centralizada de hitos
│ │ └── manager.rb # HitosManager - CRUD completo de hitos
│ │
│ ├── estados/ # Gestión centralizada de estados
│ │ └── manager.rb # EstadosManager - Gestión de estados de hitos
│ │
│ ├── verificacion/ # Verificación centralizada
│ │ └── manager.rb # VerificacionManager - Verificación completa
│ │
│ ├── vista_previa/ # Vista previa centralizada
│ │ └── manager.rb # VistaPreviaManager - Múltiples modos de vista
│ │
│ └── sincronizacion/ # Sincronización centralizada
│ └── manager.rb # SincronizacionManager - Sincronización multi-ambiente
└── README_OPTIMIZADO.md # Este archivo
```
## 🎯 Sistema de IDs de Hitos
### Nuevo Formato: `fNNNNN`
- **Formato**: `f{numero_fase}{secuencia de 3 dígitos}`
- **Ejemplos**:
- `f05001`: Fase 5, secuencia 1
- `f05002`: Fase 5, secuencia 2
- `f10025`: Fase 10, secuencia 25
### Ventajas del nuevo sistema:
1. **Consistencia**: Todos los hitos siguen el mismo patrón
2. **Identificación clara**: Se puede identificar la fase al instante
3. **Ordenamiento natural**: Los IDs se ordenan por fase y secuencia
4. **Escalabilidad**: Soporta hasta 99 fases y 999 hitos por fase
## 📦 Módulos Centrales
### 1. `DashboardCore` - Módulo Base
**Propósito**: Proporcionar funcionalidades comunes a todos los módulos
**Características**:
- Configuración centralizada
- Gestión de backups automáticos
- Extracción y análisis de hitos
- Generación de HTML de hitos
- Validaciones comunes
- Métodos de utilidad
### 2. `HitosManager` - Gestión de Hitos
**Propósito**: CRUD completo de hitos del dashboard
**Funcionalidades**:
- Carga y cache de hitos
- Búsqueda por ID, fase, estado o tag
- Agregar hitos individuales o por fase
- Actualizar y eliminar hitos
- Estadísticas y reportes
- Limpieza de duplicados
### 3. `EstadosManager` - Gestión de Estados
**Propósito**: Gestión centralizada de estados de hitos
**Funcionalidades**:
- Actualización individual o múltiple de estados
- Flujo de trabajo de estados (avanzar/retroceder)
- Métodos de conveniencia (`completar_hito`, `bloquear_hito`, etc.)
- Análisis de progreso por estado
- Reportes de distribución de estados
### 4. `VerificacionManager` - Verificación Completa
**Propósito**: Verificación exhaustiva del dashboard
**Reglas de verificación**:
- **Estructura**: Elementos HTML básicos (crítico)
- **Hitos**: Integridad y formato (alto)
- **Estados**: Estados válidos (medio)
- **Timeline**: Estructura correcta (alto)
- **Métricas**: Métricas y estadísticas (bajo)
### 5. `VistaPreviaManager` - Vista Previa Multi-modo
**Propósito**: Múltiples modos de visualización del dashboard
**Modos disponibles**:
- `resumen`: Resumen completo
- `hitos`: Hitos detallados
- `estructura`: Estructura del dashboard
- `estadisticas`: Estadísticas detalladas
- `rapido`: Resumen rápido
- `metricas`: Métricas clave
- `estados`: Análisis de estados
- `fases`: Análisis por fases
### 6. `SincronizacionManager` - Sincronización Multi-ambiente
**Propósito**: Sincronización entre ambientes
**Ambientes soportados**:
- **Local**: Desarrollo local
- **Staging**: Ambiente de pruebas
- **Production**: Producción
**Funcionalidades**:
- Sincronización con backup automático
- Verificación de estado de sincronización
- Gestión de backups (listar, restaurar, limpiar)
- Modo dry-run para pruebas
## 🔄 Flujos de Trabajo Optimizados
### Flujo para Nuevo Proyecto:
```bash
# 1. Inicializar dashboard limpio
./orquestador.rb flujo inicializar
# 2. Agregar hitos de fase 5
./orquestador.rb herramienta hitos agregar-fase fase5
# 3. Verificar estructura
./orquestador.rb herramienta verificacion verificar-completo
# 4. Sincronizar a producción
./orquestador.rb herramienta sincronizacion sincronizar-a-produccion
```
### Flujo de Seguimiento Diario:
```bash
# 1. Vista previa rápida
./orquestador.rb herramienta vista-previa rapido
# 2. Verificar estados actuales
./orquestador.rb herramienta estados listar-estados
# 3. Actualizar hitos en progreso
./orquestador.rb herramienta estados actualizar f05001 in-progress
```
## 🚀 Ventajas de la Nueva Estructura
### 1. **Reutilización de Código**
- Funcionalidades comunes en `DashboardCore`
- Módulos independientes pero interoperables
- Reducción de código duplicado
### 2. **Mantenibilidad**
- Cada módulo tiene una responsabilidad única
- Interfaces claras y documentadas
- Fácil de extender y modificar
### 3. **Consistencia**
- Sistema de IDs uniforme
- Estados validados centralmente
- Formatos de salida consistentes
### 4. **Escalabilidad**
- Nuevos módulos se integran fácilmente
- Soporte para múltiples ambientes
- Arquitectura preparada para crecimiento
### 5. **Testing**
- Módulos independientes facilitan pruebas unitarias
- Interfaces claras para mocking
- Validaciones centralizadas
## 📊 Migración desde la Versión Anterior
### Hitos Renombrados (Fase 5):
| ID Antiguo | Nuevo ID | Descripción |
|------------|-----------|-------------|
| BKP02 | f05001 | Backup Automático de VMs |
| DB02 | f05002 | Backup Automático de Base de Datos |
| MON01 | f05003 | Monitoreo de Recursos |
| DOC01 | f05004 | Documentación Operativa |
| PERF01 | f05005 | Pruebas de Rendimiento |
### Herramientas Equivalentes:
| Herramienta Antigua | Nuevo Módulo | Método Equivalente |
|---------------------|--------------|-------------------|
| `limpiar.rb` | `HitosManager` | `limpiar_duplicados` |
| `agregar_hitos.rb` | `HitosManager` | `agregar_hitos_fase` |
| `verificar.rb` | `VerificacionManager` | `verificar_completo` |
| `actualizar_estados.rb` | `EstadosManager` | `actualizar_estado_hito` |
| `vista_previa.rb` | `VistaPreviaManager` | `mostrar_vista_previa` |
| `sincronizar.rb` | `SincronizacionManager` | `sincronizar_a_produccion` |
## 🔧 Uso de los Nuevos Módulos
### Ejemplo: Gestión de Hitos
```ruby
require_relative 'lib/dashboard_core'
# Crear manager
manager = DashboardCore::HitosManager.new
# Cargar hitos
hitos = manager.cargar_hitos
# Buscar hito específico
hito = manager.buscar_hito_por_id('f05001')
# Agregar nuevo hito
manager.agregar_hito({
id: 'f05006',
titulo: 'Nuevo Hito',
descripcion: 'Descripción del hito',
estado: 'pending',
tags: ['tag-remote']
})
# Estadísticas
stats = manager.estadisticas
```
### Ejemplo: Gestión de Estados
```ruby
require_relative 'lib/dashboard_core'
# Crear manager
estados_manager = DashboardCore::EstadosManager.new
# Actualizar estado
estados_manager.actualizar_estado_hito('f05001', 'in-progress')
# Avanzar estado automáticamente
estados_manager.avanzar_estado_hito('f05001')
# Reporte de estados
reporte = estados_manager.reporte_estados
```
## 📈 Métricas de Mejora
### Reducción de Código Duplicado:
- **Antes**: ~80% de código repetido entre herramientas
- **Después**: <10% de código repetido
### Tiempo de Desarrollo:
- **Nuevas funcionalidades**: 60% más rápido
- **Mantenimiento**: 75% menos tiempo
### Confiabilidad:
- **Validaciones**: Centralizadas y consistentes
- **Errores**: 40% menos errores de validación
- **Consistencia**: 100% de hitos con formato uniforme
## 🚀 Próximos Pasos
### Fase 1: Consolidación (Completada)
- [x] Crear módulos centrales
- [x] Implementar nuevo sistema de IDs
- [x] Migrar funcionalidades existentes
### Fase 2: Mejoras (En Progreso)
- [ ] Crear scripts de migración automática
- [ ] Implementar logging centralizado
- [ ] Agregar más validaciones
### Fase 3: Expansión (Planificada)
- [ ] Integración con APIs externas
- [ ] Dashboard en tiempo real
- [ ] Reportes automatizados por email
## 📚 Documentación Adicional
### Referencia de APIs:
- `DashboardCore::Base`: Clase base con métodos comunes
- `DashboardCore::HitoIdGenerator`: Generador de IDs de hitos
- `DashboardCore::Validator`: Módulo de validaciones
- `DashboardCore::Reporter`: Generador de reportes
- `DashboardCore::BackupManager`: Gestor de backups
### Configuración:
```ruby
# Configuración en dashboard_core.rb
CONFIG = {
dashboard_path: 'ruta/al/dashboard',
backups_dir: 'ruta/backups',
production_url: 'https://ejemplo.com/dashboard'
}
```
## 🤝 Contribución
### Guías de Estilo:
1. **Nombres de IDs**: Siempre usar formato `fNNNNN`
2. **Estados**: Usar solo los estados definidos en `ESTADOS_VALIDOS`
3. **Módulos**: Un módulo, una responsabilidad
4. **Documentación**: Documentar todas las interfaces públicas
### Proceso de Desarrollo:
1. Crear módulo en `lib/` si es nueva funcionalidad
2. Extender `DashboardCore::Base` para funcionalidades comunes
3. Usar `HitoIdGenerator` para nuevos hitos
4. Agregar pruebas unitarias
5. Actualizar este README
## 📞 Soporte
### Problemas Comunes:
1. **IDs inválidos**: Usar `HitoIdGenerator.es_valido?(id)`
2. **Estados inválidos**: Usar `Validator.validar_estado(estado)`
3. **Backups**: Usar `BackupManager.listar_backups`
### Recursos:
- Dashboard en producción: https://ns8.frlr.utn.edu.ar/bitacoras/p2601
- Código fuente: `tools/orquestador/`
- Documentación: Este README
---
**Última actualización**: 2024-03-06
**Versión del sistema**: 2.0.0 (Estructura Modular)
**Autor**: Sistema de Orquestación P2601
**Filosofía**: "Pequeñas herramientas coordinadas hacen al todo"
-371
View File
@@ -1,371 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/config.rb
# ============================
# Archivo de configuración centralizado para los módulos optimizados
# del orquestador del dashboard P2601
module DashboardConfig
# ==========================================================================
# CONFIGURACIÓN PRINCIPAL
# ==========================================================================
# Ruta al dashboard principal
DASHBOARD_PATH = File.expand_path('../../../dashboard/index.html', __dir__)
# Directorio de backups
BACKUPS_DIR = File.expand_path('../../../dashboard/backups', __dir__)
# URLs del dashboard
URLS = {
production: 'https://ns8.frlr.utn.edu.ar/bitacoras/p2601',
static: 'https://ns8.frlr.utn.edu.ar/P2601/',
staging: 'https://ns8.frlr.utn.edu.ar/P2601-staging/'
}.freeze
# ==========================================================================
# CONFIGURACIÓN DE SINCRONIZACIÓN
# ==========================================================================
SINCRONIZACION = {
local: {
dashboard_dir: File.expand_path('../../../dashboard', __dir__),
index_file: 'index.html',
backups_dir: BACKUPS_DIR
},
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
# ==========================================================================
# CONFIGURACIÓN DE ESTADOS
# ==========================================================================
ESTADOS = {
'pending' => {
nombre: 'Pendiente',
clase: 'pending',
color: '#eab308',
descripcion: 'Hito pendiente de iniciar'
},
'in-progress' => {
nombre: 'En Progreso',
clase: 'in-progress',
color: '#3b82f6',
descripcion: 'Hito en ejecución activa'
},
'completed' => {
nombre: 'Completado',
clase: 'completed',
color: '#22c55e',
descripcion: 'Hito finalizado exitosamente'
},
'blocked' => {
nombre: 'Bloqueado',
clase: 'blocked',
color: '#ef4444',
descripcion: 'Hito bloqueado por dependencias u obstáculos'
},
'on-hold' => {
nombre: 'En Espera',
clase: 'on-hold',
color: '#8b5cf6',
descripcion: 'Hito en espera por priorización o recursos'
}
}.freeze
# ==========================================================================
# CONFIGURACIÓN DE FASES Y HITOS
# ==========================================================================
# 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
# ==========================================================================
# CONFIGURACIÓN DE VALIDACIÓN
# ==========================================================================
# Reglas de validación para IDs de hitos
ID_HITO_REGEX = /^f\d{5}$/
# Estados válidos
ESTADOS_VALIDOS = ESTADOS.keys.freeze
# Fases válidas
FASES_VALIDAS = HITOS_POR_FASE.keys.freeze
# ==========================================================================
# CONFIGURACIÓN DE LOGGING
# ==========================================================================
LOGGING = {
nivel: ENV['DEBUG'] == 'true' ? :debug : :info,
directorio: File.expand_path('../../../logs', __dir__),
archivo: 'orquestador.log',
max_tamano: 10 * 1024 * 1024, # 10 MB
max_archivos: 5
}.freeze
# ==========================================================================
# CONFIGURACIÓN DE BACKUPS
# ==========================================================================
BACKUP = {
retencion_dias: 7,
max_backups: 50,
comprimir: false,
incluir_timestamp: true
}.freeze
# ==========================================================================
# CONFIGURACIÓN DE MÓDULOS
# ==========================================================================
MODULOS = {
hitos: {
cache_habilitado: true,
cache_ttl: 300, # 5 minutos en segundos
validar_ids: true
},
estados: {
historial_habilitado: true,
max_historial: 100
},
verificacion: {
niveles: {
critico: ['estructura'],
alto: ['hitos', 'timeline'],
medio: ['estados'],
bajo: ['metricas']
}
},
vista_previa: {
modos: {
resumen: { lineas_max: 100 },
hitos: { max_hitos: 50 },
rapido: { compacto: true }
}
}
}.freeze
# ==========================================================================
# MÉTODOS DE UTILIDAD
# ==========================================================================
# Obtener la ruta completa del dashboard
def self.dashboard_path
DASHBOARD_PATH
end
# Obtener el directorio de backups
def self.backups_dir
BACKUPS_DIR
end
# Obtener URL del dashboard en producción
def self.production_url
URLS[:production]
end
# Verificar si el modo debug está activado
def self.debug?
ENV['DEBUG'] == 'true'
end
# Obtener configuración de sincronización para un ambiente
def self.sincronizacion_config(ambiente = :local)
SINCRONIZACION[ambiente] || SINCRONIZACION[:local]
end
# Obtener información de un estado
def self.estado_info(estado)
ESTADOS[estado] || ESTADOS['pending']
end
# Validar un ID de hito
def self.id_hito_valido?(id)
id.match?(ID_HITO_REGEX)
end
# Validar un estado
def self.estado_valido?(estado)
ESTADOS_VALIDOS.include?(estado)
end
# Validar una fase
def self.fase_valida?(fase)
FASES_VALIDAS.include?(fase)
end
# Obtener hitos de una fase
def self.hitos_por_fase(fase)
HITOS_POR_FASE[fase] || []
end
# Obtener todas las fases disponibles
def self.fases_disponibles
FASES_VALIDAS
end
# Generar un nuevo ID de hito
def self.generar_id_hito(fase_numero, secuencia)
"f#{fase_numero.to_s.rjust(2, '0')}#{secuencia.to_s.rjust(3, '0')}"
end
# Parsear un ID de hito
def self.parsear_id_hito(id)
match = id.match(/^f(\d{2})(\d{3})$/)
return nil unless match
{
fase: match[1].to_i,
secuencia: match[2].to_i,
id_completo: id
}
end
# Obtener la ruta del archivo de log
def self.log_file
File.join(LOGGING[:directorio], LOGGING[:archivo])
end
# Crear directorios necesarios
def self.crear_directorios
dirs = [
BACKUPS_DIR,
LOGGING[:directorio],
File.dirname(DASHBOARD_PATH)
]
dirs.each do |dir|
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
end
end
# Cargar configuración desde variables de entorno
def self.cargar_desde_env
config = {
debug: ENV['DEBUG'] == 'true',
dashboard_path: ENV['DASHBOARD_PATH'] || DASHBOARD_PATH,
backups_dir: ENV['BACKUPS_DIR'] || BACKUPS_DIR
}
# Sobrescribir configuración de sincronización si está en ENV
if ENV['PRODUCTION_DIR']
SINCRONIZACION[:production][:dashboard_dir] = ENV['PRODUCTION_DIR']
end
config
end
# Mostrar configuración actual
def self.mostrar_configuracion
puts "⚙️ CONFIGURACIÓN DEL ORQUESTADOR"
puts "=" * 60
puts "📁 RUTAS:"
puts " • Dashboard: #{DASHBOARD_PATH}"
puts " • Backups: #{BACKUPS_DIR}"
puts " • Logs: #{log_file}"
puts "\n🌐 URLs:"
URLS.each do |nombre, url|
puts "#{nombre.capitalize}: #{url}"
end
puts "\n🎯 HITOS PREDEFINIDOS:"
HITOS_POR_FASE.each do |fase, hitos|
puts "#{fase}: #{hitos.size} hitos"
end
puts "\n📊 ESTADOS DISPONIBLES:"
ESTADOS.each do |codigo, info|
puts "#{codigo}: #{info[:nombre]} (#{info[:color]})"
end
puts "\n🔧 MÓDULOS CONFIGURADOS:"
MODULOS.each_key do |modulo|
puts "#{modulo}"
end
puts "\n🔍 VALIDACIONES:"
puts " • Formato ID hito: #{ID_HITO_REGEX}"
puts " • Estados válidos: #{ESTADOS_VALIDOS.join(', ')}"
puts " • Fases válidas: #{FASES_VALIDAS.join(', ')}"
puts "\n💾 BACKUP:"
puts " • Retención: #{BACKUP[:retencion_dias]} días"
puts " • Máximo backups: #{BACKUP[:max_backups]}"
puts " • Comprimir: #{BACKUP[:comprimir] ? 'Sí' : 'No'}"
puts "\n📝 LOGGING:"
puts " • Nivel: #{LOGGING[:nivel]}"
puts " • Tamaño máximo: #{LOGGING[:max_tamano] / (1024 * 1024)} MB"
puts " • Archivos máximos: #{LOGGING[:max_archivos]}"
puts "\n✅ Configuración cargada correctamente"
end
end
# Inicializar directorios al cargar la configuración
DashboardConfig.crear_directorios
# Cargar configuración desde variables de entorno
DashboardConfig.cargar_desde_env
# Mostrar configuración si se ejecuta directamente
if __FILE__ == $0
DashboardConfig.mostrar_configuracion
end
@@ -1,365 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/actualizar_estados.rb
# ================================================
# Herramienta para actualizar el estado de hitos en el dashboard P2601
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
class EstadoHitosManager
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
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
@backup_dir = File.join(File.dirname(@dashboard_path), 'backups')
end
def default_dashboard_path
File.expand_path('../../../../dashboard/index.html', __FILE__)
end
def listar_hitos
puts "📋 LISTADO DE HITOS DISPONIBLES"
puts "=" * 50
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
content = File.read(@dashboard_path)
hitos = extraer_hitos(content)
if hitos.empty?
puts "️ No se encontraron hitos en el dashboard"
return true
end
puts "🎯 Total de hitos encontrados: #{hitos.size}"
puts
hitos.each_with_index do |hito, i|
estado_info = ESTADOS_VALIDOS[hito[:estado]] || ESTADOS_VALIDOS['pending']
puts " #{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
puts " 📊 Estado actual: #{estado_info[:nombre]} (#{hito[:estado]})"
puts " 📅 Fecha: #{hito[:fecha]}"
puts " 🏷️ Tags: #{hito[:tags].join(', ')}" if hito[:tags].any?
puts
end
mostrar_resumen_estados(hitos)
true
end
def actualizar_estado(hito_id, nuevo_estado, fecha = nil)
puts "🔄 ACTUALIZANDO ESTADO DE HITO"
puts "=" * 50
# Validar estado
unless ESTADOS_VALIDOS.key?(nuevo_estado)
puts "❌ Error: Estado '#{nuevo_estado}' no válido"
puts " Estados válidos: #{ESTADOS_VALIDOS.keys.join(', ')}"
return false
end
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard"
return false
end
# Crear backup
create_backup
# Leer contenido
puts "📖 Leyendo dashboard..."
content = File.read(@dashboard_path)
# Buscar y actualizar hito
puts "🔍 Buscando hito: #{hito_id}"
nuevo_contenido, hito_encontrado = actualizar_hito_en_contenido(content, hito_id, nuevo_estado, fecha)
unless hito_encontrado
puts "❌ Error: No se encontró el hito con ID '#{hito_id}'"
puts " Use 'listar' para ver los hitos disponibles"
return false
end
# Guardar cambios
puts "💾 Guardando dashboard actualizado..."
File.write(@dashboard_path, nuevo_contenido)
# Mostrar resultados
estado_info = ESTADOS_VALIDOS[nuevo_estado]
puts "\n✅ Estado actualizado exitosamente"
puts " • Hito: #{hito_id}"
puts " • Nuevo estado: #{estado_info[:nombre]}"
puts " • Fecha: #{fecha || 'Actualizada automáticamente'}"
true
end
def actualizar_multiples(hitos_estados)
puts "🔄 ACTUALIZANDO MÚLTIPLES HITOS"
puts "=" * 50
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard"
return false
end
# Validar todos los estados primero
hitos_estados.each do |hito_id, estado|
unless ESTADOS_VALIDOS.key?(estado)
puts "❌ Error: Estado '#{estado}' no válido para hito '#{hito_id}'"
return false
end
end
# Crear backup
create_backup
# Leer contenido
puts "📖 Leyendo dashboard..."
content = File.read(@dashboard_path)
nuevo_contenido = content.dup
# Actualizar cada hito
resultados = {}
hitos_estados.each do |hito_id, estado|
puts "🔍 Actualizando hito: #{hito_id} -> #{estado}"
nuevo_contenido, encontrado = actualizar_hito_en_contenido(nuevo_contenido, hito_id, estado)
resultados[hito_id] = encontrado
end
# Guardar cambios
puts "💾 Guardando dashboard actualizado..."
File.write(@dashboard_path, nuevo_contenido)
# Mostrar resultados
puts "\n📊 RESULTADOS DE ACTUALIZACIÓN:"
exitos = resultados.values.count(true)
fallos = resultados.values.count(false)
resultados.each do |hito_id, exitoso|
emoji = exitoso ? '✅' : '❌'
puts " #{emoji} #{hito_id}: #{exitoso ? 'Actualizado' : 'No encontrado'}"
end
puts "\n📈 RESUMEN:"
puts " • Hitós actualizados exitosamente: #{exitos}"
puts " • Hitós no encontrados: #{fallos}"
exitos > 0
end
def mostrar_estados_disponibles
puts "🎨 ESTADOS DISPONIBLES"
puts "=" * 50
ESTADOS_VALIDOS.each do |codigo, info|
puts "#{codigo}: #{info[:nombre]}"
puts " Clase CSS: #{info[:clase]}"
puts " Color: #{info[:color]}"
puts
end
true
end
private
def create_backup
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_estados_#{timestamp}.html")
puts "💾 Creando backup en: #{backup_file}"
FileUtils.cp(@dashboard_path, backup_file)
puts "✅ Backup creado"
end
def extraer_hitos(content)
hitos = []
# Buscar todos los hitos
content.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>/)
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',
tags: hito.scan(/<span class="[^"]*tag[^"]*">([^<]+)<\/span>/).flatten,
html_completo: hito_html[0]
}
end
hitos.uniq { |h| h[:id] }
end
def actualizar_hito_en_contenido(content, 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 = content.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 mostrar_resumen_estados(hitos)
return if hitos.empty?
puts "📊 RESUMEN DE ESTADOS"
puts "=" * 50
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
puts "\n💡 RECOMENDACIONES:"
if conteo_estados['pending'] && conteo_estados['pending'] > total * 0.7
puts " • Muchos hitos pendientes (#{conteo_estados['pending']}/#{total})"
puts " • Considerar avanzar algunos a 'en-progreso'"
end
if conteo_estados['completed'] && conteo_estados['completed'] < total * 0.2
puts " • Pocos hitos completados (#{conteo_estados['completed'] || 0}/#{total})"
puts " • Revisar progreso real del proyecto"
end
puts
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
manager = EstadoHitosManager.new
# Parsear argumentos
command = ARGV[0] || 'listar'
case command
when 'listar', 'list', 'ls'
success = manager.listar_hitos
when 'actualizar', 'update'
if ARGV.length < 3
puts "❌ Error: Se requieren ID del hito y nuevo estado"
puts " Uso: #{$0} actualizar ID_HITO NUEVO_ESTADO [FECHA]"
puts " Ejemplo: #{$0} actualizar BKP02 in-progress 2024-03-06"
success = false
else
hito_id = ARGV[1]
nuevo_estado = ARGV[2]
fecha = ARGV[3] if ARGV.length >= 4
success = manager.actualizar_estado(hito_id, nuevo_estado, fecha)
end
when 'multiples', 'batch'
if ARGV.length < 2 || ARGV[1..-1].length.odd?
puts "❌ Error: Se requieren pares ID-ESTADO"
puts " Uso: #{$0} multiples ID1 ESTADO1 ID2 ESTADO2 ..."
puts " Ejemplo: #{$0} multiples BKP02 in-progress DB02 completed"
success = false
else
hitos_estados = {}
args = ARGV[1..-1]
args.each_slice(2) do |hito_id, estado|
hitos_estados[hito_id] = estado
end
success = manager.actualizar_multiples(hitos_estados)
end
when 'estados', 'states'
success = manager.mostrar_estados_disponibles
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [comando] [argumentos...]"
puts
puts "COMANDOS:"
puts " listar, list - Listar hitos disponibles con sus estados (predeterminado)"
puts " actualizar, update ID ESTADO [FECHA] - Actualizar estado de un hito"
puts " multiples, batch ID1 ESTADO1 ID2 ESTADO2 ... - Actualizar múltiples hitos"
puts " estados, states - Mostrar estados disponibles"
puts " help - Mostrar esta ayuda"
puts
puts "ESTADOS DISPONIBLES:"
puts " pending - Pendiente (amarillo)"
puts " in-progress - En Progreso (azul)"
puts " completed - Completado (verde)"
puts " blocked - Bloqueado (rojo)"
puts " on-hold - En Espera (púrpura)"
puts
puts "EJEMPLOS:"
puts " #{$0} listar"
puts " #{$0} actualizar BKP02 in-progress"
puts " #{$0} actualizar DB02 completed 2024-03-06"
puts " #{$0} multiples BKP02 in-progress MON01 completed"
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS SUGERIDOS:"
puts " 1. Revisar dashboard actualizado"
puts " 2. Verificar que los estados se muestren correctamente"
puts " 3. Actualizar bitácora del proyecto con los cambios"
puts "\n📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 50
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
@@ -1,354 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/agregar_hitos.rb
# ============================================
# Herramienta para agregar hitos al dashboard de forma controlada y estructurada
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
require 'json'
class HitosManager
# Definición de hitos por fase
HITOS_POR_FASE = {
'fase5' => [
{
id: 'BKP02',
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: 'DB02',
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: 'MON01',
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: 'DOC01',
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: 'PERF01',
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
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
@backup_dir = File.join(File.dirname(@dashboard_path), 'backups')
end
def default_dashboard_path
File.expand_path('../../../../dashboard/index.html', __FILE__)
end
def agregar_hitos(fase = 'fase5', modo = 'append')
puts "🎯 AGREGANDO HITOS - Fase: #{fase.upcase}"
puts "=" * 50
# Validar fase
unless HITOS_POR_FASE.key?(fase)
puts "❌ Error: Fase '#{fase}' no definida"
puts " Fases disponibles: #{HITOS_POR_FASE.keys.join(', ')}"
return false
end
# Verificar dashboard
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
# Crear backup
create_backup
# Leer contenido
puts "📖 Leyendo dashboard..."
content = File.read(@dashboard_path)
original_lines = content.lines.count
# Generar HTML de hitos
hitos_html = generar_hitos_html(fase)
# Insertar hitos según el modo
case modo
when 'append'
puts "📝 Modo: Agregar al final de la timeline"
new_content = insertar_al_final_timeline(content, hitos_html)
when 'replace'
puts "🔄 Modo: Reemplazar hitos existentes"
new_content = reemplazar_hitos_existentes(content, hitos_html, fase)
when 'insert'
puts "📍 Modo: Insertar después de hitos específicos"
new_content = insertar_despues_de_marcador(content, hitos_html)
else
puts "❌ Error: Modo '#{modo}' no válido"
puts " Modos disponibles: append, replace, insert"
return false
end
# Verificar cambios
if new_content == content
puts "⚠️ No se realizaron cambios (posiblemente los hitos ya existen)"
return true
end
# Guardar cambios
puts "💾 Guardando dashboard actualizado..."
File.write(@dashboard_path, new_content)
# Mostrar resultados
show_results(content, new_content, fase, modo)
true
end
def listar_hitos(fase = nil)
puts "📋 LISTADO DE HITOS DISPONIBLES"
puts "=" * 50
if fase
unless HITOS_POR_FASE.key?(fase)
puts "❌ Fase '#{fase}' no encontrada"
return false
end
puts "Fase: #{fase.upcase}"
HITOS_POR_FASE[fase].each_with_index do |hito, i|
puts " #{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
puts " 📝 #{hito[:descripcion][0..80]}..."
puts " 🏷️ #{hito[:tags].join(', ')}"
puts " ⏱️ #{hito[:esfuerzo]}"
puts
end
else
HITOS_POR_FASE.each do |fase_nombre, hitos|
puts "📁 #{fase_nombre.upcase} (#{hitos.size} hitos)"
hitos.each do |hito|
puts " • [#{hito[:id]}] #{hito[:titulo]}"
end
puts
end
end
true
end
private
def create_backup
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_hitos_#{timestamp}.html")
puts "💾 Creando backup en: #{backup_file}"
FileUtils.cp(@dashboard_path, backup_file)
puts "✅ Backup creado"
end
def generar_hitos_html(fase)
hitos = HITOS_POR_FASE[fase]
timestamp = Time.now.strftime('%Y-%m-%d %H:%M')
html = <<~HTML
<!-- Hitos de #{fase.upcase} agregados automáticamente - #{timestamp} -->
HTML
hitos.each do |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
html += <<~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">#{hito[:estado].capitalize}</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
html
end
def insertar_al_final_timeline(content, hitos_html)
# Buscar el cierre de la sección timeline
timeline_end_pattern = /<\/div>\s*<!-- Timeline de Hitos -->/
if content.match?(timeline_end_pattern)
# Insertar antes del marcador de cierre
content.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 content.include?(footer_start)
content.gsub(footer_start) do |match|
"#{hitos_html}\n #{match}"
end
else
puts "⚠️ No se encontró lugar para insertar hitos, agregando al final del body"
body_end = '</body>'
content.gsub(body_end) do |match|
"#{hitos_html}\n #{match}"
end
end
end
end
def reemplazar_hitos_existentes(content, hitos_html, fase)
# Buscar y reemplazar hitos existentes de la fase específica
fase_pattern = /<!-- Hitos de #{fase.upcase} agregados automáticamente - .*?-->[\s\S]*?(?=<!--|<\/div>\s*<div class="footer">)/m
if content.match?(fase_pattern)
# Reemplazar hitos existentes
content.gsub(fase_pattern, hitos_html)
else
# Si no hay hitos de esta fase, agregarlos al final
insertar_al_final_timeline(content, hitos_html)
end
end
def insertar_despues_de_marcador(content, hitos_html)
# Buscar un marcador específico (último hito existente)
last_hito_pattern = /<div class="tl-item">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>\s*(?=\s*<\/div>)/m
if content.match?(last_hito_pattern)
# Insertar después del último hito
content.gsub(last_hito_pattern) do |match|
"#{match}\n\n#{hitos_html}"
end
else
# Si no se encuentra marcador, usar método por defecto
insertar_al_final_timeline(content, hitos_html)
end
end
def show_results(original, new_content, fase, modo)
puts "\n📊 RESULTADOS:"
puts " • Fase: #{fase}"
puts " • Modo: #{modo}"
puts " • Líneas originales: #{original.lines.count}"
puts " • Líneas nuevas: #{new_content.lines.count}"
puts " • Líneas agregadas: #{new_content.lines.count - original.lines.count}"
# Contar hitos agregados
hitos_agregados = HITOS_POR_FASE[fase].size
puts " • Hitos agregados: #{hitos_agregados}"
# Mostrar hitos agregados
puts "\n🎯 HITOS AGREGADOS:"
HITOS_POR_FASE[fase].each_with_index do |hito, i|
puts " #{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
end
puts "\n✅ Hitos agregados exitosamente"
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
# Parsear argumentos
dashboard_path = nil
command = 'agregar'
fase = 'fase5'
modo = 'append'
ARGV.each_with_index do |arg, i|
if arg == '--dashboard' && ARGV[i + 1]
dashboard_path = ARGV[i + 1]
elsif i == 0 && !arg.start_with?('--')
command = arg
elsif i == 1 && !arg.start_with?('--') && command != 'help'
fase = arg
elsif i == 2 && !arg.start_with?('--') && command != 'help'
modo = arg
end
end
manager = HitosManager.new(dashboard_path)
case command
when 'agregar', 'add'
success = manager.agregar_hitos(fase, modo)
when 'listar', 'list', 'ls'
success = manager.listar_hitos(fase)
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [comando] [fase] [modo] [--dashboard RUTA]"
puts
puts "COMANDOS:"
puts " agregar, add - Agregar hitos al dashboard (predeterminado)"
puts " listar, list - Listar hitos disponibles"
puts " help - Mostrar esta ayuda"
puts
puts "FASES:"
puts " fase5 - Hitos de la fase 5 (predeterminado)"
puts
puts "MODOS (solo para comando 'agregar'):"
puts " append - Agregar al final (predeterminado)"
puts " replace - Reemplazar hitos existentes"
puts " insert - Insertar después de marcador"
puts
puts "OPCIONES:"
puts " --dashboard RUTA - Ruta personalizada al archivo dashboard"
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Verificar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 2. Revisar backups en: dashboard/backups/"
puts " 3. Actualizar estado de hitos según progreso real"
puts "\n📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 50
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
@@ -1,412 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/agregar_hitos_db.rb
# ==============================================
# Herramienta para agregar hitos de fase 5 a la base de datos PostgreSQL
# de dtic-BITACORAs (contenedor Docker)
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'pg'
require 'time'
class HitosDBManager
# Configuración de conexión a PostgreSQL
DB_CONFIG = {
host: 'localhost',
port: 5433,
dbname: 'dtic_bitacoras',
user: 'dtic_user',
password: 'dtic_pass_2026'
}.freeze
# Hitos de fase 5 para agregar
HITOS_FASE_5 = [
{
id_hito: 'BKP02',
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.',
estado: '⏳',
horas_presencial: 2,
horas_remoto: 2,
nodo_nombre: 'srv-dasu'
},
{
id_hito: 'DB02',
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.',
estado: '⏳',
horas_presencial: 0,
horas_remoto: 2,
nodo_nombre: 'sql-dasuten'
},
{
id_hito: 'MON01',
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.',
estado: '⏳',
horas_presencial: 0,
horas_remoto: 3,
nodo_nombre: 'srv-dasu'
},
{
id_hito: 'DOC01',
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.',
estado: '⏳',
horas_presencial: 0,
horas_remoto: 6,
nodo_nombre: 'srv-ns8'
},
{
id_hito: 'PERF01',
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.',
estado: '⏳',
horas_presencial: 2,
horas_remoto: 2,
nodo_nombre: 'pcv-dasu0'
}
].freeze
def initialize
@conn = nil
end
def conectar_db
puts "🔌 Conectando a PostgreSQL..."
puts " • Host: #{DB_CONFIG[:host]}"
puts " • Puerto: #{DB_CONFIG[:port]}"
puts " • Base de datos: #{DB_CONFIG[:dbname]}"
begin
@conn = PG.connect(DB_CONFIG)
puts "✅ Conexión exitosa"
true
rescue PG::Error => e
puts "❌ Error de conexión: #{e.message}"
puts " Verifique que:"
puts " 1. El contenedor PostgreSQL esté ejecutándose"
puts " 2. El puerto 5433 esté accesible"
puts " 3. Las credenciales sean correctas"
false
end
end
def desconectar_db
@conn&.close
puts "🔌 Conexión cerrada"
end
def verificar_estructura
puts "🔍 Verificando estructura de la base de datos..."
tablas_requeridas = ['proyectos', 'fases', 'hitos', 'nodos']
tablas_encontradas = []
tablas_requeridas.each do |tabla|
resultado = @conn.exec("SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'bitacoras' AND table_name = $1)", [tabla])
existe = resultado[0]['exists'] == 't'
tablas_encontradas << tabla if existe
puts "#{tabla}: #{existe ? '✅' : '❌'}"
end
if tablas_encontradas.size == tablas_requeridas.size
puts "✅ Todas las tablas requeridas existen"
true
else
puts "❌ Faltan tablas requeridas"
false
end
end
def obtener_proyecto_p2601
puts "📋 Buscando proyecto P2601..."
resultado = @conn.exec("SELECT id, codigo, nombre FROM bitacoras.proyectos WHERE codigo = $1", ['P2601'])
if resultado.ntuples.zero?
puts "❌ Proyecto P2601 no encontrado"
nil
else
proyecto = resultado[0]
puts "✅ Proyecto encontrado: #{proyecto['codigo']} - #{proyecto['nombre']}"
proyecto
end
end
def obtener_fase_5(proyecto_id)
puts "🔍 Buscando fase 5 del proyecto..."
resultado = @conn.exec("SELECT id, numero, nombre FROM bitacoras.fases WHERE proyecto_id = $1 AND numero = 5", [proyecto_id])
if resultado.ntuples.zero?
puts "❌ Fase 5 no encontrada"
nil
else
fase = resultado[0]
puts "✅ Fase encontrada: #{fase['numero']} - #{fase['nombre']}"
fase
end
end
def listar_hitos_existentes(fase_id)
puts "📋 Listando hitos existentes en fase 5..."
resultado = @conn.exec("SELECT id_hito, titulo, estado FROM bitacoras.hitos WHERE fase_id = $1 ORDER BY id_hito", [fase_id])
if resultado.ntuples.zero?
puts " ️ No hay hitos en esta fase"
[]
else
puts " 📊 Hitos encontrados: #{resultado.ntuples}"
resultado.each do |hito|
puts "#{hito['id_hito']}: #{hito['titulo']} (#{hito['estado']})"
end
resultado.to_a
end
end
def agregar_hitos_fase_5
puts "🎯 AGREGANDO HITOS A FASE 5"
puts "=" * 60
# Conectar a la base de datos
unless conectar_db
puts "❌ No se pudo conectar a la base de datos"
return false
end
begin
# Verificar estructura
unless verificar_estructura
puts "❌ La estructura de la base de datos no es válida"
return false
end
# Obtener proyecto P2601
proyecto = obtener_proyecto_p2601
return false unless proyecto
# Obtener fase 5
fase = obtener_fase_5(proyecto['id'])
return false unless fase
# Listar hitos existentes
hitos_existentes = listar_hitos_existentes(fase['id'])
ids_existentes = hitos_existentes.map { |h| h['id_hito'] }
# Filtrar hitos que no existen
hitos_a_agregar = HITOS_FASE_5.reject { |h| ids_existentes.include?(h[:id_hito]) }
if hitos_a_agregar.empty?
puts "️ Todos los hitos de fase 5 ya existen en la base de datos"
return true
end
puts "📝 Agregando #{hitos_a_agregar.size} hitos nuevos..."
# Agregar cada hito
hitos_agregados = []
hitos_a_agregar.each do |hito|
if agregar_hito(fase['id'], hito)
hitos_agregados << hito[:id_hito]
end
end
# Mostrar resultados
mostrar_resultados(hitos_agregados, hitos_a_agregar.size)
hitos_agregados.any?
ensure
desconectar_db
end
end
def agregar_hito(fase_id, hito)
puts " Agregando hito: #{hito[:id_hito]}"
begin
# Obtener ID del nodo si se especifica
nodo_id = nil
if hito[:nodo_nombre]
resultado = @conn.exec("SELECT id FROM bitacoras.nodos WHERE nombre = $1", [hito[:nodo_nombre]])
nodo_id = resultado[0]['id'] if resultado.ntuples > 0
end
# Insertar hito
@conn.exec(
"INSERT INTO bitacoras.hitos (fase_id, id_hito, titulo, descripcion, estado, horas_presencial, horas_remoto, nodo_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
[
fase_id,
hito[:id_hito],
hito[:titulo],
hito[:descripcion],
hito[:estado],
hito[:horas_presencial],
hito[:horas_remoto],
nodo_id
]
)
puts "#{hito[:id_hito]} agregado exitosamente"
true
rescue PG::Error => e
puts " ❌ Error al agregar #{hito[:id_hito]}: #{e.message}"
false
end
end
def mostrar_resultados(hitos_agregados, total_intentos)
puts "\n📊 RESULTADOS DE LA OPERACIÓN"
puts "=" * 60
if hitos_agregados.empty?
puts "️ No se agregaron hitos nuevos"
else
puts "✅ Hitos agregados exitosamente: #{hitos_agregados.size}/#{total_intentos}"
hitos_agregados.each do |id_hito|
puts "#{id_hito}"
end
end
# Mostrar hitos de fase 5
puts "\n🎯 HITOS DE FASE 5 COMPLETOS:"
HITOS_FASE_5.each do |hito|
existe = hitos_agregados.include?(hito[:id_hito]) ? '✅' : '📋'
puts " #{existe} #{hito[:id_hito]}: #{hito[:titulo]}"
end
puts "\n🔗 Dashboard actualizado: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
end
def ver_estado_actual
puts "🔍 ESTADO ACTUAL DEL DASHBOARD P2601"
puts "=" * 60
unless conectar_db
puts "❌ No se pudo conectar a la base de datos"
return false
end
begin
# Obtener proyecto P2601
proyecto = obtener_proyecto_p2601
return false unless proyecto
# Obtener todas las fases
resultado = @conn.exec("SELECT id, numero, nombre FROM bitacoras.fases WHERE proyecto_id = $1 ORDER BY numero", [proyecto['id']])
puts "📋 PROYECTO: #{proyecto['codigo']} - #{proyecto['nombre']}"
puts "📊 FASES: #{resultado.ntuples}"
resultado.each do |fase|
puts "\n📁 Fase #{fase['numero']}: #{fase['nombre']}"
# Obtener hitos de esta fase
hitos = @conn.exec("SELECT id_hito, titulo, estado, horas_presencial, horas_remoto FROM bitacoras.hitos WHERE fase_id = $1 ORDER BY id_hito", [fase['id']])
if hitos.ntuples.zero?
puts " ️ Sin hitos"
else
puts " 📝 Hitos: #{hitos.ntuples}"
hitos.each do |hito|
estado_emoji = hito['estado']
horas = " (P:#{hito['horas_presencial']}h, R:#{hito['horas_remoto']}h)"
puts "#{estado_emoji} #{hito['id_hito']}: #{hito['titulo']}#{horas}"
end
end
end
# Métricas generales
puts "\n📈 MÉTRICAS GENERALES:"
metricas = @conn.exec("
SELECT
COUNT(*) as total_hitos,
COUNT(*) FILTER (WHERE h.estado = '✅') as completados,
COUNT(*) FILTER (WHERE h.estado != '✅') as pendientes,
COALESCE(SUM(h.horas_presencial), 0) as total_presencial,
COALESCE(SUM(h.horas_remoto), 0) as total_remoto
FROM bitacoras.hitos h
JOIN bitacoras.fases f ON h.fase_id = f.id
WHERE f.proyecto_id = $1
", [proyecto['id']])
if metricas.ntuples > 0
m = metricas[0]
puts " • Total hitos: #{m['total_hitos']}"
puts " • Completados: #{m['completados']}"
puts " • Pendientes: #{m['pendientes']}"
puts " • Horas presenciales: #{m['total_presencial']}h"
puts " • Horas remotas: #{m['total_remoto']}h"
puts " • Total horas: #{m['total_presencial'].to_i + m['total_remoto'].to_i}h"
end
true
ensure
desconectar_db
end
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
manager = HitosDBManager.new
# Parsear argumentos
command = ARGV[0] || 'agregar'
case command
when 'agregar', 'add'
success = manager.agregar_hitos_fase_5
when 'estado', 'status'
success = manager.ver_estado_actual
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [comando]"
puts
puts "COMANDOS:"
puts " agregar, add - Agregar hitos de fase 5 a la base de datos"
puts " estado, status - Ver estado actual del dashboard P2601"
puts " help - Mostrar esta ayuda"
puts
puts "DESCRIPCIÓN:"
puts " Esta herramienta agrega hitos de fase 5 al dashboard P2601"
puts " que se encuentra en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts
puts "REQUISITOS:"
puts " 1. Contenedor PostgreSQL de dtic-BITACORAs ejecutándose"
puts " 2. Puerto 5433 accesible localmente"
puts " 3. Credenciales de base de datos configuradas"
puts
puts "HITOS DE FASE 5:"
HitosDBManager::HITOS_FASE_5.each do |hito|
puts "#{hito[:id_hito]}: #{hito[:titulo]}"
end
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Verificar dashboard: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 2. Los cambios deberían reflejarse automáticamente"
puts " 3. Actualizar estados según progreso real"
puts
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-208
View File
@@ -1,208 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/limpiar.rb
# ======================================
# Herramienta simple para limpiar hitos duplicados del dashboard P2601
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
class DashboardCleaner
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
@backup_dir = File.join(File.dirname(@dashboard_path), 'backups')
end
def default_dashboard_path
File.expand_path('../../../../dashboard/index.html', __FILE__)
end
def run
puts "🧹 LIMPIANDO DASHBOARD P2601"
puts "=" * 50
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
# Crear backup
create_backup
# Leer contenido
puts "📖 Leyendo dashboard..."
content = File.read(@dashboard_path)
original_size = content.length
original_lines = content.lines.count
# Limpiar hitos duplicados
puts "🔍 Buscando hitos duplicados..."
cleaned_content = remove_duplicate_hitos(content)
# Verificar cambios
if cleaned_content == content
puts "✅ No se encontraron hitos duplicados"
return true
end
# Guardar cambios
puts "💾 Guardando dashboard limpio..."
File.write(@dashboard_path, cleaned_content)
# Mostrar resultados
show_results(content, cleaned_content)
true
end
private
def create_backup
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_clean_#{timestamp}.html")
puts "💾 Creando backup en: #{backup_file}"
FileUtils.cp(@dashboard_path, backup_file)
puts "✅ Backup creado"
end
def remove_duplicate_hitos(content)
# Patrones para identificar hitos duplicados
# Buscamos hitos que comienzan con el comentario de "Hitos adicionales agregados automáticamente"
duplicate_pattern = /<!-- Hitos adicionales agregados automáticamente - .*?-->[\s\S]*?(?=<div class="tl-item"|<\/div>\s*<div class="footer">|$)/m
# Contar ocurrencias
matches = content.scan(duplicate_pattern)
puts "📊 Encontrados #{matches.size} bloques de hitos duplicados"
# Eliminar todos los bloques duplicados
cleaned = content.gsub(duplicate_pattern, '')
# También limpiar hitos insertados en lugares incorrectos
# Buscar hitos que no están dentro de la sección .timeline
timeline_section = extract_timeline_section(cleaned)
if timeline_section
# Mantener solo los hitos dentro de la sección timeline
cleaned = restore_proper_structure(cleaned, timeline_section)
end
cleaned
end
def extract_timeline_section(content)
# Buscar la sección de timeline
timeline_start = content.index('<div class="timeline">')
return nil unless timeline_start
# Encontrar el cierre de la sección timeline
# Buscar el cierre del div que contiene la timeline
depth = 0
timeline_end = timeline_start
content[timeline_start..-1].chars.each_with_index do |char, i|
if content[timeline_start + i, 4] == '<div'
depth += 1
elsif content[timeline_start + i, 5] == '</div'
depth -= 1
if depth == 0
timeline_end = timeline_start + i + 6 # Incluir '</div>'
break
end
end
end
content[timeline_start...timeline_end]
end
def restore_proper_structure(content, timeline_section)
# Reconstruir el contenido manteniendo solo la timeline correcta
# Primero, eliminar todas las ocurrencias de hitos fuera de timeline
tl_item_pattern = /<div class="tl-item">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m
# Extraer todos los hitos únicos
all_hitos = content.scan(tl_item_pattern).uniq
if all_hitos.any?
puts "📋 Encontrados #{all_hitos.size} hitos únicos"
# Reemplazar la sección timeline completa con una versión limpia
new_timeline = <<~HTML
<div class="timeline">
#{all_hitos.join("\n\n")}
</div>
HTML
# Reemplazar la timeline antigua con la nueva
content = content.gsub(/<div class="timeline">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m, new_timeline)
end
content
end
def show_results(original, cleaned)
puts "\n📊 RESULTADOS DE LA LIMPIEZA:"
puts " • Líneas originales: #{original.lines.count}"
puts " • Líneas limpias: #{cleaned.lines.count}"
puts " • Líneas eliminadas: #{original.lines.count - cleaned.lines.count}"
puts " • Bytes originales: #{original.length}"
puts " • Bytes limpios: #{cleaned.length}"
puts " • Bytes ahorrados: #{original.length - cleaned.length}"
# Contar hitos únicos
hitos_count = cleaned.scan(/<div class="tl-item">/).size
puts " • Hitos únicos encontrados: #{hitos_count}"
puts "\n✅ Dashboard limpiado exitosamente"
end
end
# Ejecución principal
if __FILE__ == $0
begin
# Parsear argumentos
dashboard_path = nil
ARGV.each_with_index do |arg, i|
if arg == '--dashboard' && ARGV[i + 1]
dashboard_path = ARGV[i + 1]
end
end
cleaner = DashboardCleaner.new(dashboard_path)
success = cleaner.run
if success
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Verificar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 2. Usar herramientas de orquestador para agregar hitos correctamente"
puts " 3. Revisar backups en: dashboard/backups/"
puts "\n📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 50
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
# Mostrar ayuda si se solicita
if ARGV.include?('--help') || ARGV.include?('-h')
puts "📖 USO:"
puts " #{$0} [--dashboard RUTA]"
puts
puts "OPCIONES:"
puts " --dashboard RUTA - Ruta personalizada al archivo dashboard"
puts " --help, -h - Mostrar esta ayuda"
puts
puts "EJEMPLO:"
puts " #{$0} --dashboard /ruta/al/dashboard/index.html"
exit 0
end
-419
View File
@@ -1,419 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/orquestador.rb
# ==========================================
# Script maestro para orquestar todas las herramientas del dashboard P2601
# Filosofía: "Menos es más" - orquestación de herramientas pequeñas y específicas
require 'fileutils'
class DashboardOrchestrator
VERSION = '1.0.0'
# Mapeo de comandos a herramientas
HERRAMIENTAS = {
'limpiar' => {
archivo: 'limpiar.rb',
descripcion: 'Limpiar hitos duplicados del dashboard',
uso: './orquestador.rb limpiar [--dashboard RUTA]'
},
'agregar-hitos' => {
archivo: 'agregar_hitos.rb',
descripcion: 'Agregar hitos al dashboard',
uso: './orquestador.rb agregar-hitos [fase] [modo] [--dashboard RUTA]'
},
'actualizar-estados' => {
archivo: 'actualizar_estados.rb',
descripcion: 'Actualizar estados de hitos',
uso: './orquestador.rb actualizar-estados [comando] [argumentos...]'
},
'verificar' => {
archivo: 'verificar.rb',
descripcion: 'Verificar estructura del dashboard',
uso: './orquestador.rb verificar [comando] [--dashboard RUTA]'
},
'vista-previa' => {
archivo: 'vista_previa.rb',
descripcion: 'Vista previa del dashboard',
uso: './orquestador.rb vista-previa [modo] [--dashboard RUTA]'
},
'sincronizar' => {
archivo: 'sincronizar.rb',
descripcion: 'Sincronizar dashboard local con producción',
uso: './orquestador.rb sincronizar [--dry-run] [--no-backup]'
},
'agregar-hitos-db' => {
archivo: 'agregar_hitos_db.rb',
descripcion: 'Agregar hitos a la base de datos PostgreSQL',
uso: './orquestador.rb agregar-hitos-db [comando]'
}
}.freeze
# Flujos de trabajo predefinidos
FLUJOS_TRABAJO = {
'inicializar' => {
descripcion: 'Inicializar dashboard limpio con hitos de fase 5',
pasos: [
{ herramienta: 'limpiar', args: [] },
{ herramienta: 'agregar-hitos', args: ['fase5', 'append'] },
{ herramienta: 'verificar', args: ['verificar'] }
]
},
'seguimiento-diario' => {
descripcion: 'Flujo para seguimiento diario del proyecto',
pasos: [
{ herramienta: 'vista-previa', args: ['rapido'] },
{ herramienta: 'actualizar-estados', args: ['listar'] }
]
},
'revision-semanal' => {
descripcion: 'Flujo para revisión semanal completa',
pasos: [
{ herramienta: 'limpiar', args: [] },
{ herramienta: 'verificar', args: ['verificar'] },
{ herramienta: 'vista-previa', args: ['resumen'] },
{ herramienta: 'actualizar-estados', args: ['listar'] }
]
},
'agregar-fase5' => {
descripcion: 'Agregar hitos de fase 5 y verificar',
pasos: [
{ herramienta: 'agregar-hitos', args: ['fase5', 'append'] },
{ herramienta: 'verificar', args: ['analizar'] }
]
},
'desplegar-produccion' => {
descripcion: 'Sincronizar cambios a producción y verificar',
pasos: [
{ herramienta: 'sincronizar', args: [] },
{ herramienta: 'verificar', args: ['verificar'] },
{ herramienta: 'vista-previa', args: ['rapido'] }
]
},
'ciclo-completo' => {
descripcion: 'Ciclo completo: limpiar, agregar hitos, sincronizar',
pasos: [
{ herramienta: 'limpiar', args: [] },
{ herramienta: 'agregar-hitos', args: ['fase5', 'append'] },
{ herramienta: 'sincronizar', args: [] },
{ herramienta: 'verificar', args: ['verificar'] }
]
},
'agregar-hitos-produccion' => {
descripcion: 'Agregar hitos a la base de datos de producción',
pasos: [
{ herramienta: 'agregar-hitos-db', args: ['agregar'] },
{ herramienta: 'agregar-hitos-db', args: ['estado'] }
]
},
'dashboard-completo' => {
descripcion: 'Flujo completo para dashboard en producción',
pasos: [
{ herramienta: 'agregar-hitos-db', args: ['agregar'] },
{ herramienta: 'vista-previa', args: ['rapido'] },
{ herramienta: 'verificar', args: ['verificar'] }
]
}
}.freeze
def initialize
@directorio_herramientas = File.dirname(__FILE__)
end
def ejecutar_herramienta(nombre_herramienta, args = [])
puts "🛠️ EJECUTANDO: #{nombre_herramienta}"
puts "=" * 60
herramienta = HERRAMIENTAS[nombre_herramienta]
unless herramienta
puts "❌ Error: Herramienta '#{nombre_herramienta}' no encontrada"
puts " Herramientas disponibles: #{HERRAMIENTAS.keys.join(', ')}"
return false
end
ruta_herramienta = File.join(@directorio_herramientas, herramienta[:archivo])
unless File.exist?(ruta_herramienta)
puts "❌ Error: Archivo de herramienta no encontrado: #{ruta_herramienta}"
return false
end
# Construir comando
comando = "ruby #{ruta_herramienta} #{args.join(' ')}"
puts "📝 Comando: #{comando}"
puts "-" * 60
# Ejecutar herramienta
system(comando)
exit_status = $?.exitstatus
puts "-" * 60
if exit_status == 0
puts "✅ Herramienta ejecutada exitosamente"
true
else
puts "❌ Herramienta falló con código #{exit_status}"
false
end
end
def ejecutar_flujo_trabajo(nombre_flujo)
puts "🔄 EJECUTANDO FLUJO DE TRABAJO: #{nombre_flujo}"
puts "=" * 60
flujo = FLUJOS_TRABAJO[nombre_flujo]
unless flujo
puts "❌ Error: Flujo de trabajo '#{nombre_flujo}' no encontrado"
puts " Flujos disponibles: #{FLUJOS_TRABAJO.keys.join(', ')}"
return false
end
puts "📋 Descripción: #{flujo[:descripcion]}"
puts "📊 Pasos: #{flujo[:pasos].size}"
puts
resultados = []
flujo[:pasos].each_with_index do |paso, index|
puts "📝 PASO #{index + 1}/#{flujo[:pasos].size}: #{paso[:herramienta]}"
puts " Argumentos: #{paso[:args].join(' ')}" if paso[:args].any?
puts
exito = ejecutar_herramienta(paso[:herramienta], paso[:args])
resultados << { paso: paso[:herramienta], exito: exito }
unless exito
puts "⚠️ Flujo interrumpido por fallo en paso #{index + 1}"
break
end
puts "\n" unless index == flujo[:pasos].size - 1
end
# Mostrar resumen
mostrar_resumen_flujo(resultados, nombre_flujo)
resultados.all? { |r| r[:exito] }
end
def listar_herramientas
puts "🛠️ HERRAMIENTAS DISPONIBLES"
puts "=" * 60
HERRAMIENTAS.each do |nombre, info|
puts "#{nombre}"
puts " 📝 #{info[:descripcion]}"
puts " 📋 Uso: #{info[:uso]}"
puts
end
true
end
def listar_flujos
puts "🔄 FLUJOS DE TRABAJO PREDEFINIDOS"
puts "=" * 60
FLUJOS_TRABAJO.each do |nombre, flujo|
puts "#{nombre}"
puts " 📝 #{flujo[:descripcion]}"
puts " 📊 Pasos: #{flujo[:pasos].size}"
flujo[:pasos].each_with_index do |paso, i|
puts " #{i + 1}. #{paso[:herramienta]} #{paso[:args].join(' ')}"
end
puts
end
true
end
def mostrar_estado_sistema
puts "📊 ESTADO DEL SISTEMA DE ORQUESTACIÓN"
puts "=" * 60
puts "🏗️ INFORMACIÓN GENERAL:"
puts " • Versión: #{VERSION}"
puts " • Directorio herramientas: #{@directorio_herramientas}"
puts " • Herramientas disponibles: #{HERRAMIENTAS.size}"
puts "📊 Flujos predefinidos: #{FLUJOS_TRABAJO.size}"
puts "\n🎯 DASHBOARDS DISPONIBLES:"
puts " • Producción: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " • Estático: https://ns8.frlr.utn.edu.ar/P2601/"
puts "\n🔍 VERIFICACIÓN DE HERRAMIENTAS:"
herramientas_ok = 0
HERRAMIENTAS.each do |nombre, info|
ruta = File.join(@directorio_herramientas, info[:archivo])
existe = File.exist?(ruta)
ejecutable = File.executable?(ruta)
emoji = existe && ejecutable ? '✅' : '❌'
puts " #{emoji} #{nombre}: #{info[:archivo]}"
puts " - Existe: #{existe ? 'Sí' : 'No'}"
puts " - Ejecutable: #{ejecutable ? 'Sí' : 'No'}"
puts " - Tipo: #{nombre.include?('db') ? 'Base de datos' : 'Archivo'}"
herramientas_ok += 1 if existe && ejecutable
end
puts "\n📈 RESUMEN:"
puts " • Herramientas listas: #{herramientas_ok}/#{HERRAMIENTAS.size}"
if herramientas_ok == HERRAMIENTAS.size
puts " ✅ Todas las herramientas están listas para usar"
else
puts " ⚠️ Algunas herramientas requieren atención"
end
true
end
private
def mostrar_resumen_flujo(resultados, nombre_flujo)
puts "📊 RESUMEN DEL FLUJO: #{nombre_flujo}"
puts "=" * 60
exitosos = resultados.count { |r| r[:exito] }
total = resultados.size
resultados.each_with_index do |resultado, index|
emoji = resultado[:exito] ? '✅' : '❌'
puts " #{emoji} Paso #{index + 1}: #{resultado[:paso]}"
end
puts "\n📈 ESTADÍSTICAS:"
puts " • Pasos ejecutados: #{total}"
puts " • Pasos exitosos: #{exitosos}"
puts " • Porcentaje de éxito: #{(exitosos * 100.0 / total).round(1)}%"
if exitosos == total
puts "\n🎉 ¡Flujo de trabajo completado exitosamente!"
else
puts "\n⚠️ Flujo de trabajo completado con errores"
end
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
orquestador = DashboardOrchestrator.new
# Parsear argumentos
comando = ARGV[0] || 'help'
case comando
when 'herramienta', 'tool'
if ARGV.length < 2
puts "❌ Error: Se requiere nombre de herramienta"
puts " Uso: #{$0} herramienta NOMBRE_HERRAMIENTA [argumentos...]"
exit 1
end
nombre_herramienta = ARGV[1]
args = ARGV[2..-1] || []
success = orquestador.ejecutar_herramienta(nombre_herramienta, args)
when 'flujo', 'workflow'
if ARGV.length < 2
puts "❌ Error: Se requiere nombre de flujo"
puts " Uso: #{$0} flujo NOMBRE_FLUJO"
exit 1
end
nombre_flujo = ARGV[1]
success = orquestador.ejecutar_flujo_trabajo(nombre_flujo)
when 'listar-herramientas', 'list-tools'
success = orquestador.listar_herramientas
when 'listar-flujos', 'list-workflows'
success = orquestador.listar_flujos
when 'estado', 'status'
success = orquestador.mostrar_estado_sistema
when 'version', '--version', '-v'
puts "Dashboard Orchestrator v#{DashboardOrchestrator::VERSION}"
success = true
when 'help', '--help', '-h'
puts "📖 ORQUESTADOR DE HERRAMIENTAS - DASHBOARD P2601"
puts "=" * 60
puts
puts "DESCRIPCIÓN:"
puts " Script maestro para orquestar herramientas pequeñas y específicas"
puts " del dashboard P2601, siguiendo la filosofía 'Menos es más'."
puts
puts "USO:"
puts " #{$0} [comando] [argumentos...]"
puts
puts "COMANDOS PRINCIPALES:"
puts " herramienta, tool NOMBRE [args...] Ejecutar una herramienta específica"
puts " flujo, workflow NOMBRE Ejecutar flujo de trabajo predefinido"
puts " listar-herramientas, list-tools Listar todas las herramientas"
puts " listar-flujos, list-workflows Listar flujos de trabajo"
puts " estado, status Mostrar estado del sistema"
puts " version, -v Mostrar versión"
puts " help, -h Mostrar esta ayuda"
puts
puts "HERRAMIENTAS DISPONIBLES:"
DashboardOrchestrator::HERRAMIENTAS.each do |nombre, info|
puts "#{nombre}: #{info[:descripcion]}"
end
puts
puts "FLUJOS PREDEFINIDOS:"
DashboardOrchestrator::FLUJOS_TRABAJO.each do |nombre, flujo|
puts "#{nombre}: #{flujo[:descripcion]}"
end
puts
puts "EJEMPLOS:"
puts " #{$0} herramienta limpiar"
puts " #{$0} herramienta agregar-hitos fase5 append"
puts " #{$0} flujo inicializar"
puts " #{$0} flujo seguimiento-diario"
puts " #{$0} estado"
puts
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
success = true
else
puts "❌ Comando '#{comando}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
# Verificar si es un comando de base de datos
if comando == 'herramienta' && nombre_herramienta == 'agregar-hitos-db'
puts "\n⚠️ NOTA: Esta herramienta modifica la base de datos PostgreSQL"
puts " • Asegúrese de que el contenedor esté ejecutándose"
puts " • Los cambios se reflejan en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
end
if success
puts "\n🚀 RECOMENDACIONES:"
puts " 1. Usar 'flujo inicializar' para dashboard limpio"
puts " 1. Usar 'flujo agregar-hitos-produccion' para hitos en base de datos"
puts " 2. Usar 'flujo seguimiento-diario' para monitoreo regular"
puts " 3. Verificar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts " 4. Dashboard estático en: https://ns8.frlr.utn.edu.ar/P2601/"
puts
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-540
View File
@@ -1,540 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/sincronizar.rb
# ==========================================
# Herramienta para sincronizar el dashboard local con producción
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
require 'time'
class DashboardSynchronizer
# Configuración de rutas
CONFIG = {
local: {
dashboard_dir: File.expand_path('../../../../dashboard', __FILE__),
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'
}
}.freeze
def initialize(options = {})
@dry_run = options[:dry_run] || false
@verbose = options[:verbose] || false
@backup = options[:backup] || true
@timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
end
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[: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 verificar_estado
puts "🔍 VERIFICANDO ESTADO DE SINCRONIZACIÓN"
puts "=" * 60
estado = {
local: verificar_estado_local,
production: verificar_estado_produccion,
diferencias: comparar_archivos
}
mostrar_estado_verificacion(estado)
estado[:local][:valido] && estado[:production][:valido]
end
def listar_backups
puts "💾 LISTADO DE BACKUPS EN PRODUCCIÓN"
puts "=" * 60
backups_dir = CONFIG[:production][:backups_dir]
unless Dir.exist?(backups_dir)
puts "️ No existe el directorio de backups: #{backups_dir}"
return false
end
backups = Dir.glob(File.join(backups_dir, '*.html')).sort.reverse
if backups.empty?
puts "️ No se encontraron backups"
return true
end
puts "📂 Directorio: #{backups_dir}"
puts "📊 Total de backups: #{backups.size}"
puts
backups.each_with_index do |backup, i|
nombre = File.basename(backup)
tamano = File.size(backup) / 1024
modificacion = File.mtime(backup).strftime('%Y-%m-%d %H:%M:%S')
puts " #{i + 1}. #{nombre}"
puts " • Tamaño: #{tamano} KB"
puts " • Modificado: #{modificacion}"
puts
end
true
end
def restaurar_backup(nombre_backup = nil)
puts "🔄 RESTAURANDO BACKUP DESDE PRODUCCIÓN"
puts "=" * 60
backups_dir = CONFIG[:production][:backups_dir]
unless Dir.exist?(backups_dir)
puts "❌ Error: No existe el directorio de backups: #{backups_dir}"
return false
end
# Si no se especifica backup, listar disponibles
unless nombre_backup
backups = Dir.glob(File.join(backups_dir, '*.html')).sort.reverse
if backups.empty?
puts "❌ Error: No hay backups disponibles para restaurar"
return false
end
puts "📋 Backups disponibles:"
backups.each_with_index do |backup, i|
puts " #{i + 1}. #{File.basename(backup)}"
end
puts "\n️ Use: #{$0} restaurar NOMBRE_BACKUP"
return false
end
# Verificar que el backup existe
ruta_backup = File.join(backups_dir, nombre_backup)
unless File.exist?(ruta_backup)
puts "❌ Error: Backup no encontrado: #{nombre_backup}"
puts " Ruta: #{ruta_backup}"
return false
end
# Crear backup del estado actual local
if @backup && !@dry_run
puts "💾 Creando backup del estado local actual..."
crear_backup_local
end
# Restaurar backup
puts "📥 Restaurando backup: #{nombre_backup}"
archivo_local = File.join(CONFIG[:local][:dashboard_dir], CONFIG[:local][:index_file])
if @dry_run
puts " [DRY RUN] Se restauraría #{ruta_backup} a #{archivo_local}"
puts " [DRY RUN] Tamaño: #{File.size(ruta_backup)} bytes"
else
begin
FileUtils.cp(ruta_backup, archivo_local)
puts "✅ Backup restaurado exitosamente"
puts " • Origen: #{ruta_backup}"
puts " • Destino: #{archivo_local}"
puts " • Tamaño: #{File.size(archivo_local)} bytes"
rescue => e
puts "❌ Error al restaurar backup: #{e.message}"
return false
end
end
true
end
private
def verificar_ruta_local
dir = CONFIG[:local][:dashboard_dir]
archivo = File.join(dir, CONFIG[:local][:index_file])
unless Dir.exist?(dir)
puts "❌ Directorio local no existe: #{dir}"
return false
end
unless File.exist?(archivo)
puts "❌ Archivo local no existe: #{archivo}"
return false
end
true
end
def verificar_ruta_produccion
dir = CONFIG[:production][:dashboard_dir]
unless Dir.exist?(dir)
puts "❌ Directorio de producción no existe: #{dir}"
return false
end
# Verificar permisos de escritura
unless File.writable?(dir)
puts "❌ Sin permisos de escritura en: #{dir}"
return false
end
true
end
def crear_backup_produccion
puts "💾 Creando backup en producción..."
backups_dir = CONFIG[:production][:backups_dir]
archivo_produccion = File.join(CONFIG[:production][:dashboard_dir], CONFIG[:production][:index_file])
# Crear directorio de backups si no existe
unless Dir.exist?(backups_dir)
puts " Creando directorio de backups: #{backups_dir}"
FileUtils.mkdir_p(backups_dir)
end
# Nombre del backup
nombre_backup = "dashboard_prod_#{@timestamp}.html"
ruta_backup = File.join(backups_dir, nombre_backup)
# Crear backup
begin
FileUtils.cp(archivo_produccion, ruta_backup)
puts "✅ Backup creado: #{nombre_backup}"
puts " • Ruta: #{ruta_backup}"
puts " • Tamaño: #{File.size(ruta_backup)} bytes"
true
rescue => e
puts "❌ Error al crear backup: #{e.message}"
false
end
end
def crear_backup_local
backups_dir = File.join(CONFIG[:local][:dashboard_dir], CONFIG[:local][:backups_dir])
archivo_local = File.join(CONFIG[:local][:dashboard_dir], CONFIG[:local][:index_file])
# Crear directorio de backups si no existe
unless Dir.exist?(backups_dir)
FileUtils.mkdir_p(backups_dir)
end
# Nombre del backup
nombre_backup = "dashboard_local_#{@timestamp}.html"
ruta_backup = File.join(backups_dir, nombre_backup)
# Crear backup
FileUtils.cp(archivo_local, ruta_backup)
end
def sincronizar_archivos
archivos_sincronizados = []
# Archivo principal (index.html)
archivo_local = File.join(CONFIG[:local][:dashboard_dir], CONFIG[:local][:index_file])
archivo_produccion = File.join(CONFIG[:production][:dashboard_dir], CONFIG[:production][:index_file])
puts "📄 Sincronizando: #{CONFIG[:local][:index_file]}"
puts " • Local: #{archivo_local}"
puts " • Producción: #{archivo_produccion}"
# Verificar si hay diferencias
if archivos_iguales?(archivo_local, archivo_produccion)
puts " ️ Los archivos son idénticos, no se requiere sincronización"
else
if @dry_run
puts " [DRY RUN] Se copiaría #{archivo_local} a #{archivo_produccion}"
puts " [DRY RUN] Diferencia de tamaño: #{File.size(archivo_local) - File.size(archivo_produccion)} bytes"
else
begin
FileUtils.cp(archivo_local, archivo_produccion)
puts " ✅ Sincronizado exitosamente"
archivos_sincronizados << CONFIG[:local][:index_file]
rescue => e
puts " ❌ Error al sincronizar: #{e.message}"
end
end
end
# Buscar otros archivos en el directorio local
Dir.glob(File.join(CONFIG[:local][:dashboard_dir], '*')).each do |archivo_local|
next if File.directory?(archivo_local)
next if File.basename(archivo_local) == CONFIG[:local][:index_file]
archivo_produccion = File.join(CONFIG[:production][:dashboard_dir], File.basename(archivo_local))
puts "📄 Sincronizando: #{File.basename(archivo_local)}"
if @dry_run
puts " [DRY RUN] Se copiaría #{archivo_local} a #{archivo_produccion}"
else
begin
FileUtils.cp(archivo_local, archivo_produccion)
puts " ✅ Sincronizado exitosamente"
archivos_sincronizados << File.basename(archivo_local)
rescue => e
puts " ❌ Error al sincronizar: #{e.message}"
end
end
end
archivos_sincronizados
end
def archivos_iguales?(archivo1, archivo2)
return false unless File.exist?(archivo1) && File.exist?(archivo2)
return false unless File.size(archivo1) == File.size(archivo2)
# Comparación simple por tamaño y contenido
File.read(archivo1) == File.read(archivo2)
end
def verificar_estado_local
dir = CONFIG[:local][:dashboard_dir]
archivo = File.join(dir, CONFIG[:local][:index_file])
{
valido: File.exist?(archivo),
ruta: dir,
archivo: CONFIG[:local][:index_file],
existe: File.exist?(archivo),
tamano: File.exist?(archivo) ? File.size(archivo) : 0,
modificacion: File.exist?(archivo) ? File.mtime(archivo).strftime('%Y-%m-%d %H:%M:%S') : 'N/A'
}
end
def verificar_estado_produccion
dir = CONFIG[:production][:dashboard_dir]
archivo = File.join(dir, CONFIG[:production][:index_file])
{
valido: File.exist?(archivo),
ruta: dir,
archivo: CONFIG[:production][:index_file],
existe: File.exist?(archivo),
tamano: File.exist?(archivo) ? File.size(archivo) : 0,
modificacion: File.exist?(archivo) ? File.mtime(archivo).strftime('%Y-%m-%d %H:%M:%S') : 'N/A',
escribible: File.writable?(dir)
}
end
def comparar_archivos
archivo_local = File.join(CONFIG[:local][:dashboard_dir], CONFIG[:local][:index_file])
archivo_produccion = File.join(CONFIG[:production][:dashboard_dir], CONFIG[:production][:index_file])
return {} unless File.exist?(archivo_local) && File.exist?(archivo_produccion)
{
iguales: archivos_iguales?(archivo_local, archivo_produccion),
tamano_local: File.size(archivo_local),
tamano_produccion: File.size(archivo_produccion),
diferencia_tamano: File.size(archivo_local) - File.size(archivo_produccion),
modificacion_local: File.mtime(archivo_local),
modificacion_produccion: File.mtime(archivo_produccion),
mas_reciente: File.mtime(archivo_local) > File.mtime(archivo_produccion) ? 'local' : 'produccion'
}
end
def mostrar_resumen_sincronizacion(archivos_sincronizados)
puts "\n📊 RESUMEN DE SINCRONIZACIÓN"
puts "=" * 60
if @dry_run
puts "🔍 MODO SIMULACIÓN (DRY RUN)"
puts " No se realizaron cambios reales"
end
if archivos_sincronizados.empty?
puts "️ No se sincronizaron archivos"
puts " • Los archivos ya están sincronizados"
puts " • O hubo errores en la sincronización"
else
puts "✅ Archivos sincronizados: #{archivos_sincronizados.size}"
archivos_sincronizados.each do |archivo|
puts "#{archivo}"
end
end
# Mostrar URLs
puts "\n🔗 URLs DEL DASHBOARD:"
puts " • Local: file://#{CONFIG[:local][:dashboard_dir]}/#{CONFIG[:local][:index_file]}"
puts " • Producción: https://ns8.frlr.utn.edu.ar/P2601/"
puts " • Bitácoras: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts "\n💡 RECOMENDACIONES:"
puts " 1. Verificar cambios en producción: https://ns8.frlr.utn.edu.ar/P2601/"
puts " 2. Usar 'verificar' para validar la sincronización"
puts " 3. Revisar backups si es necesario restaurar"
end
def mostrar_estado_verificacion(estado)
puts "🏠 ESTADO LOCAL:"
if estado[:local][:valido]
puts " ✅ Dashboard local válido"
puts " • Ruta: #{estado[:local][:ruta]}"
puts " • Archivo: #{estado[:local][:archivo]}"
puts " • Tamaño: #{estado[:local][:tamano]} bytes"
puts " • Modificación: #{estado[:local][:modificacion]}"
else
puts " ❌ Dashboard local no válido"
end
puts "\n🏭 ESTADO PRODUCCIÓN:"
if estado[:production][:valido]
puts " ✅ Dashboard de producción válido"
puts " • Ruta: #{estado[:production][:ruta]}"
puts " • Archivo: #{estado[:production][:archivo]}"
puts " • Tamaño: #{estado[:production][:tamano]} bytes"
puts " • Modificación: #{estado[:production][:modificacion]}"
puts " • Escribible: #{estado[:production][:escribible] ? '✅ Sí' : '❌ No'}"
else
puts " ❌ Dashboard de producción no válido"
end
if estado[:diferencias].any?
puts "\n🔍 COMPARACIÓN DE ARCHIVOS:"
if estado[:diferencias][:iguales]
puts " ✅ Los archivos son idénticos"
else
puts " ⚠️ Los archivos son diferentes"
puts " • Tamaño local: #{estado[:diferencias][:tamano_local]} bytes"
puts " • Tamaño producción: #{estado[:diferencias][:tamano_produccion]} bytes"
puts " • Diferencia: #{estado[:diferencias][:diferencia_tamano]} bytes"
puts " • Más reciente: #{estado[:diferencias][:mas_reciente]}"
end
end
puts "\n💡 RECOMENDACIONES:"
if estado[:local][:valido] && estado[:production][:valido]
if estado[:diferencias][:iguales]
puts " ✅ Los dashboards están sincronizados"
puts " • No se requiere acción"
else
puts " ⚠️ Los dashboards están desincronizados"
puts " • Use 'sincronizar' para actualizar producción"
puts " • Use 'sincronizar --dry-run' para simular"
end
else
puts " ❌ Hay problemas con los dashboards"
puts " • Verifique las rutas y permisos"
end
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
synchronizer = DashboardSynchronizer.new
# Parsear argumentos
command = ARGV[0] || 'verificar'
options = {
dry_run: ARGV.include?('--dry-run') || ARGV.include?('-n'),
verbose: ARGV.include?('--verbose') || ARGV.include?('-v'),
backup: !ARGV.include?('--no-backup')
}
# Extraer argumentos adicionales
extra_args = (ARGV[1..-1] || []).reject { |arg| arg.start_with?('--') || arg == '-n' || arg == '-v' }
case command
when 'sincronizar', 'sync'
synchronizer = DashboardSynchronizer.new(options)
success = synchronizer.sincronizar_a_produccion
when 'verificar', 'check'
success = synchronizer.verificar_estado
when 'backups', 'list-backups'
success = synchronizer.listar_backups
when 'restaurar', 'restore'
nombre_backup = extra_args[0]
synchronizer = DashboardSynchronizer.new(options)
success = synchronizer.restaurar_backup(nombre_backup)
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [comando] [argumentos...]"
puts
puts "COMANDOS:"
puts " sincronizar, sync - Sincronizar dashboard local a producción"
puts " verificar, check - Verificar estado de sincronización"
puts " backups, list-backups - Listar backups en producción"
puts " restaurar, restore [NOMBRE] - Restaurar backup desde producción"
puts " help - Mostrar esta ayuda"
puts
puts "OPCIONES:"
puts " --dry-run, -n - Simular sin hacer cambios"
puts " --verbose, -v - Mostrar detalles adicionales"
puts " --no-backup - No crear backups automáticos"
puts
puts "EJEMPLOS:"
puts " #{$0} sincronizar"
puts " #{$0} sincronizar --dry-run"
puts " #{$0} verificar"
puts " #{$0} backups"
puts " #{$0} restaurar dashboard_prod_20240306_114500.html"
puts
puts "🔗 URLs IMPORTANTES:"
puts " • Producción: https://ns8.frlr.utn.edu.ar/P2601/"
puts " • Bitácoras: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Verificar cambios en: https://ns8.frlr.utn.edu.ar/P2601/"
puts " 2. Usar herramientas de orquestador para gestión continua"
puts " 3. Revisar backups periódicamente"
puts
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-368
View File
@@ -1,368 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/verificar.rb
# ========================================
# Herramienta para verificar el estado y estructura del dashboard P2601
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
class DashboardVerifier
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
end
def default_dashboard_path
File.expand_path('../../../../dashboard/index.html', __FILE__)
end
def verificar_estructura
puts "🔍 VERIFICANDO ESTRUCTURA DEL DASHBOARD P2601"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
# Leer contenido
puts "📖 Analizando dashboard..."
content = File.read(@dashboard_path)
# Realizar verificaciones
resultados = {
archivo: verificar_archivo(content),
estructura: verificar_estructura_basica(content),
hitos: verificar_hitos(content),
timeline: verificar_timeline(content),
estadisticas: verificar_estadisticas(content)
}
# Mostrar resumen
mostrar_resumen(resultados)
# Retornar éxito si todas las verificaciones críticas pasan
resultados[:archivo][:exito] && resultados[:estructura][:exito]
end
def analizar_hitos
puts "📋 ANALIZANDO HITOS DEL DASHBOARD"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard"
return false
end
content = File.read(@dashboard_path)
# Extraer información de hitos
hitos_info = extraer_info_hitos(content)
# Mostrar análisis
mostrar_analisis_hitos(hitos_info)
true
end
private
def verificar_archivo(content)
puts "📄 Verificando archivo..."
{
exito: true,
detalles: {
tamaño_bytes: content.length,
lineas: content.lines.count,
encoding: content.encoding.name,
valido_html: content.include?('<!DOCTYPE html>') && content.include?('</html>')
}
}
end
def verificar_estructura_basica(content)
puts "🏗️ Verificando estructura básica..."
elementos_requeridos = {
'DOCTYPE html' => content.include?('<!DOCTYPE html>'),
'head' => content.include?('<head>') && content.include?('</head>'),
'body' => content.include?('<body>') && content.include?('</body>'),
'title P2601' => content.include?('P2601'),
'container' => content.include?('class="container"'),
'header' => content.include?('class="header"'),
'footer' => content.include?('class="footer"')
}
exito = elementos_requeridos.values.all?
{
exito: exito,
detalles: elementos_requeridos
}
end
def verificar_hitos(content)
puts "🎯 Verificando hitos..."
# Buscar hitos
hitos_count = content.scan(/<div class="tl-item">/).size
hitos_duplicados = buscar_hitos_duplicados(content)
# Verificar estructura de hitos
hitos_validos = content.scan(/<div class="tl-item">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m).size
{
exito: hitos_count > 0 && hitos_count == hitos_validos,
detalles: {
total_hitos: hitos_count,
hitos_validos: hitos_validos,
hitos_duplicados: hitos_duplicados.size,
porcentaje_validos: hitos_count > 0 ? (hitos_validos * 100 / hitos_count).round(1) : 0
}
}
end
def verificar_timeline(content)
puts "📅 Verificando timeline..."
timeline_presente = content.include?('class="timeline"')
if timeline_presente
# Extraer sección timeline
timeline_match = content.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
{
exito: timeline_presente,
detalles: {
presente: timeline_presente,
hitos_en_timeline: hitos_en_timeline
}
}
end
def verificar_estadisticas(content)
puts "📊 Verificando estadísticas..."
# Buscar métricas comunes
metricas = {
progreso: content.include?('Progreso General') || content.include?('progressPct'),
hitos_completados: content.include?('Hitos Completados'),
hitos_pendientes: content.include?('Hitos Pendientes'),
esfuerzo_total: content.include?('Esfuerzo Total')
}
{
exito: metricas.values.any?,
detalles: metricas
}
end
def buscar_hitos_duplicados(content)
# Extraer todos los hitos
hitos = content.scan(/<div class="tl-item">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m)
# Buscar duplicados por ID
ids_encontrados = {}
duplicados = []
hitos.each do |hito_html|
# Buscar ID en el hito
id_match = hito_html[0].match(/<span class="tl-id">([^<]+)<\/span>/)
next unless id_match
id = id_match[1]
if ids_encontrados[id]
duplicados << id
else
ids_encontrados[id] = true
end
end
duplicados
end
def extraer_info_hitos(content)
# Extraer todos los hitos
hitos_matches = content.scan(/<div class="tl-item">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m)
hitos_info = []
hitos_matches.each 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>/)
hitos_info << {
id: id_match ? id_match[1] : 'Desconocido',
titulo: titulo_match ? titulo_match[1] : 'Sin título',
estado: estado_match ? estado_match[1] : 'unknown',
fecha: fecha_match ? fecha_match[1] : 'Sin fecha',
tags: hito.scan(/<span class="[^"]*tag[^"]*">([^<]+)<\/span>/).flatten
}
end
hitos_info.uniq { |h| h[:id] } # Eliminar duplicados por ID
end
def mostrar_resumen(resultados)
puts "\n📋 RESUMEN DE VERIFICACIÓN"
puts "=" * 60
resultados.each do |nombre, resultado|
emoji = resultado[:exito] ? '✅' : '❌'
puts "#{emoji} #{nombre.to_s.capitalize}: #{resultado[:exito] ? 'OK' : 'PROBLEMA'}"
# Mostrar detalles importantes
if resultado[:detalles].is_a?(Hash)
resultado[:detalles].each do |detalle, valor|
puts "#{detalle}: #{valor}"
end
end
puts
end
# Resumen general
verificaciones_exitosas = resultados.values.count { |r| r[:exito] }
total_verificaciones = resultados.size
puts "📊 RESUMEN GENERAL:"
puts " • Verificaciones exitosas: #{verificaciones_exitosas}/#{total_verificaciones}"
puts " • Porcentaje de éxito: #{(verificaciones_exitosas * 100 / total_verificaciones).round(1)}%"
if verificaciones_exitosas == total_verificaciones
puts "\n🎉 ¡Todas las verificaciones pasaron exitosamente!"
else
puts "\n⚠️ Se encontraron problemas que requieren atención"
end
end
def mostrar_analisis_hitos(hitos_info)
puts "📈 ANÁLISIS DE HITOS"
puts "=" * 60
if hitos_info.empty?
puts "❌ No se encontraron hitos en el dashboard"
return
end
# Estadísticas generales
puts "📊 ESTADÍSTICAS:"
puts " • Total de hitos únicos: #{hitos_info.size}"
# Agrupar por estado
estados = Hash.new(0)
hitos_info.each { |h| estados[h[:estado]] += 1 }
puts " • Distribución por estado:"
estados.each do |estado, count|
porcentaje = (count * 100.0 / hitos_info.size).round(1)
puts " - #{estado}: #{count} (#{porcentaje}%)"
end
# Tags más comunes
todos_tags = hitos_info.flat_map { |h| h[:tags] }
tags_frecuentes = Hash.new(0)
todos_tags.each { |tag| tags_frecuentes[tag] += 1 }
if tags_frecuentes.any?
puts " • Tags más comunes:"
tags_frecuentes.sort_by { |_, count| -count }.first(5).each do |tag, count|
puts " - #{tag}: #{count} hitos"
end
end
# Lista de hitos
puts "\n📋 LISTA DE HITOS:"
hitos_info.each_with_index do |hito, i|
puts " #{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
puts " Estado: #{hito[:estado]} | Fecha: #{hito[:fecha]}"
puts " Tags: #{hito[:tags].join(', ')}" if hito[:tags].any?
puts
end
# Recomendaciones
puts "💡 RECOMENDACIONES:"
if estados['pending'] && estados['pending'] > hitos_info.size * 0.5
puts " • Muchos hitos pendientes (#{estados['pending']}/#{hitos_info.size})"
puts " • Considerar priorizar y actualizar estados"
end
if hitos_info.size < 5
puts " • Pocos hitos definidos (#{hitos_info.size})"
puts " • Considerar agregar más hitos para mejor seguimiento"
end
puts "\n✅ Análisis completado"
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
# Parsear argumentos
dashboard_path = nil
command = 'verificar'
ARGV.each_with_index do |arg, i|
if arg == '--dashboard' && ARGV[i + 1]
dashboard_path = ARGV[i + 1]
elsif i == 0 && !arg.start_with?('--')
command = arg
end
end
verifier = DashboardVerifier.new(dashboard_path)
case command
when 'verificar', 'check'
success = verifier.verificar_estructura
when 'analizar', 'analyze'
success = verifier.analizar_hitos
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [comando] [--dashboard RUTA]"
puts
puts "COMANDOS:"
puts " verificar, check - Verificar estructura del dashboard (predeterminado)"
puts " analizar, analyze - Analizar hitos en detalle"
puts " help - Mostrar esta ayuda"
puts
puts "OPCIONES:"
puts " --dashboard RUTA - Ruta personalizada al archivo dashboard"
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS SUGERIDOS:"
puts " 1. Usar 'limpiar.rb' si hay hitos duplicados"
puts " 2. Usar 'agregar_hitos.rb' para agregar hitos de fase 5"
puts " 3. Revisar dashboard en: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts "\n📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-534
View File
@@ -1,534 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/dashboard/vista_previa.rb
# ===========================================
# Herramienta para vista previa rápida del dashboard P2601
# Filosofía: "Menos es más" - herramienta pequeña, específica y reutilizable
require 'fileutils'
class DashboardPreview
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
end
def default_dashboard_path
File.expand_path('../../../../dashboard/index.html', __FILE__)
end
def mostrar_vista_previa(modo = 'resumen')
puts "👁️ VISTA PREVIA DEL DASHBOARD P2601"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
puts "📖 Analizando dashboard..."
content = File.read(@dashboard_path)
case modo
when 'resumen'
mostrar_resumen(content)
when 'hitos'
mostrar_hitos_detallados(content)
when 'estructura'
mostrar_estructura(content)
when 'estadisticas'
mostrar_estadisticas(content)
else
puts "❌ Modo '#{modo}' no válido"
puts " Modos disponibles: resumen, hitos, estructura, estadisticas"
return false
end
true
end
def mostrar_resumen_rapido
puts "⚡ RESUMEN RÁPIDO DASHBOARD"
puts "=" * 40
unless File.exist?(@dashboard_path)
puts "❌ Dashboard no encontrado"
return false
end
content = File.read(@dashboard_path)
# Información básica
puts "📄 Archivo: #{File.basename(@dashboard_path)}"
puts " • Tamaño: #{content.bytesize / 1024} KB"
puts " • Líneas: #{content.lines.count}"
puts " • Última modificación: #{File.mtime(@dashboard_path).strftime('%Y-%m-%d %H:%M')}"
# Hitos
hitos_count = content.scan(/<div class="tl-item">/).size
hitos_unicos = extraer_hitos_unicos(content).size
puts "🎯 Hitos: #{hitos_count} totales, #{hitos_unicos} únicos"
# Estados
estados = contar_estados_hitos(content)
if estados.any?
puts "📊 Estados:"
estados.each do |estado, count|
puts "#{estado}: #{count}"
end
end
# Timeline
timeline_presente = content.include?('class="timeline"')
puts "📅 Timeline: #{timeline_presente ? '✅ Presente' : '❌ Ausente'}"
# Métricas clave
metricas = extraer_metricas_clave(content)
if metricas.any?
puts "📈 Métricas clave:"
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
end
puts "\n🔗 URL: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 40
true
end
private
def mostrar_resumen(content)
puts "📋 RESUMEN COMPLETO"
puts "=" * 60
# Información del archivo
puts "📄 INFORMACIÓN DEL ARCHIVO:"
puts " • Ruta: #{@dashboard_path}"
puts " • Tamaño: #{content.bytesize} bytes (#{content.bytesize / 1024} KB)"
puts " • Líneas: #{content.lines.count}"
puts " • Encoding: #{content.encoding.name}"
# Estructura HTML
puts "\n🏗️ ESTRUCTURA HTML:"
elementos = {
'DOCTYPE html' => content.include?('<!DOCTYPE html>'),
'Elemento head' => content.include?('<head>') && content.include?('</head>'),
'Elemento body' => content.include?('<body>') && content.include?('</body>'),
'Título P2601' => content.include?('P2601'),
'Container principal' => content.include?('class="container"'),
'Header' => content.include?('class="header"'),
'Footer' => content.include?('class="footer"')
}
elementos.each do |elemento, presente|
puts "#{elemento}: #{presente ? '✅' : '❌'}"
end
# Hitos
puts "\n🎯 INFORMACIÓN DE HITOS:"
hitos_count = content.scan(/<div class="tl-item">/).size
hitos_unicos = extraer_hitos_unicos(content)
puts " • Total de hitos: #{hitos_count}"
puts " • Hitos únicos: #{hitos_unicos.size}"
if hitos_unicos.any?
puts " • Distribución por estado:"
estados = Hash.new(0)
hitos_unicos.each { |h| estados[h[:estado]] += 1 }
estados.each do |estado, count|
porcentaje = (count * 100.0 / hitos_unicos.size).round(1)
puts " - #{estado}: #{count} (#{porcentaje}%)"
end
end
# Timeline
puts "\n📅 TIMELINE:"
timeline_presente = content.include?('class="timeline"')
puts " • Presente: #{timeline_presente ? '✅ Sí' : '❌ No'}"
if timeline_presente
timeline_match = content.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 Y ESTADÍSTICAS:"
metricas = {
'Progreso General' => content.include?('Progreso General'),
'Hitos Completados' => content.include?('Hitos Completados'),
'Hitos Pendientes' => content.include?('Hitos Pendientes'),
'Esfuerzo Total' => content.include?('Esfuerzo Total')
}
metricas.each do |metrica, presente|
puts "#{metrica}: #{presente ? '✅ Presente' : '❌ Ausente'}"
end
# Extraer valores de métricas si existen
valores_metricas = extraer_valores_metricas(content)
if valores_metricas.any?
puts " • Valores encontrados:"
valores_metricas.each do |nombre, valor|
puts " - #{nombre}: #{valor}"
end
end
puts "\n💡 RECOMENDACIONES:"
if hitos_count > hitos_unicos.size * 2
puts " • Posibles hitos duplicados detectados"
puts " • Considerar usar 'limpiar.rb'"
end
if hitos_unicos.size < 3
puts " • Pocos hitos definidos"
puts " • Considerar agregar más hitos con 'agregar_hitos.rb'"
end
puts "\n✅ Resumen completado"
end
def mostrar_hitos_detallados(content)
puts "📋 HITOS DETALLADOS"
puts "=" * 60
hitos = extraer_hitos_unicos(content)
if hitos.empty?
puts "️ No se encontraron hitos en el dashboard"
return
end
puts "🎯 Total de hitos únicos: #{hitos.size}"
puts
hitos.each_with_index do |hito, i|
puts " #{i + 1}. [#{hito[:id]}] #{hito[:titulo]}"
puts " 📊 Estado: #{hito[:estado]}"
puts " 📅 Fecha: #{hito[:fecha]}"
puts " 🏷️ Tags: #{hito[:tags].join(', ')}" if hito[:tags].any?
# Mostrar descripción truncada
if hito[:descripcion] && hito[:descripcion].length > 100
puts " 📝 Descripción: #{hito[:descripcion][0..100]}..."
elsif hito[:descripcion]
puts " 📝 Descripción: #{hito[:descripcion]}"
end
puts
end
# Resumen de estados
puts "📊 RESUMEN DE ESTADOS:"
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
estados.each do |estado, count|
porcentaje = (count * 100.0 / hitos.size).round(1)
puts "#{estado}: #{count} hitos (#{porcentaje}%)"
end
end
def mostrar_estructura(content)
puts "🏗️ ESTRUCTURA DEL DASHBOARD"
puts "=" * 60
# Mostrar 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 = content.include?(seccion[:inicio])
puts "#{seccion[:nombre]}: #{presente ? '✅ Presente' : '❌ Ausente'}"
if presente && seccion[:fin] && content.include?(seccion[:fin])
# Calcular tamaño aproximado de la sección
inicio_idx = content.index(seccion[:inicio])
fin_idx = content.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
# Contar elementos HTML
puts "\n🔢 CONTEO DE ELEMENTOS HTML:"
elementos = {
'Divs' => content.scan(/<div/).size,
'Spans' => content.scan(/<span/).size,
'Headers (h1-h6)' => content.scan(/<h[1-6]/).size,
'Párrafos (p)' => content.scan(/<p>/).size,
'Enlaces (a)' => content.scan(/<a /).size
}
elementos.each do |elemento, count|
puts "#{elemento}: #{count}"
end
# Clases CSS más comunes
puts "\n🎨 CLASES CSS MÁS COMUNES:"
clases = content.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
end
def mostrar_estadisticas(content)
puts "📈 ESTADÍSTICAS DETALLADAS"
puts "=" * 60
# Métricas extraídas del contenido
metricas = extraer_valores_metricas(content)
if metricas.any?
puts "📊 MÉTRICAS ENCONTRADAS:"
metricas.each do |nombre, valor|
puts "#{nombre}: #{valor}"
end
else
puts "️ No se encontraron métricas específicas en el contenido"
end
# Estadísticas de hitos
puts "\n🎯 ESTADÍSTICAS DE HITOS:"
hitos = extraer_hitos_unicos(content)
if hitos.any?
puts " • Total de hitos únicos: #{hitos.size}"
# Por estado
estados = Hash.new(0)
hitos.each { |h| estados[h[:estado]] += 1 }
if estados.any?
puts " • Distribución por estado:"
estados.each do |estado, count|
porcentaje = (count * 100.0 / hitos.size).round(1)
puts " - #{estado}: #{count} (#{porcentaje}%)"
end
end
# Por tags
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|
puts " - #{tag}: #{count} hitos"
end
end
else
puts " • No se encontraron hitos"
end
# Estadísticas de texto
puts "\n📝 ESTADÍSTICAS DE TEXTO:"
texto_sin_html = content.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ')
puts " • Caracteres totales: #{content.length}"
puts " • Caracteres sin HTML: #{texto_sin_html.length}"
puts " • Palabras aproximadas: #{texto_sin_html.split.size}"
puts " • Líneas de código: #{content.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
end
def extraer_hitos_unicos(content)
hitos = []
content.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
}
end
# Eliminar duplicados por ID
hitos.uniq { |h| h[:id] }
end
def contar_estados_hitos(content)
estados = Hash.new(0)
content.scan(/<div class="tl-dot ([^"]+)"><\/div>/) do |estado|
estados[estado[0]] += 1
end
estados
end
def extraer_metricas_clave(content)
metricas = {}
# Buscar porcentaje de progreso
if match = content.match(/id="progressPct">([^<]+)</)
metricas['Progreso'] = match[1]
end
# Buscar hitos completados
if match = content.match(/Hitos Completados[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Completados'] = match[1]
end
# Buscar hitos pendientes
if match = content.match(/Hitos Pendientes[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Pendientes'] = match[1]
end
metricas
end
def extraer_valores_metricas(content)
metricas = {}
# Buscar porcentaje de progreso
if match = content.match(/id="progressPct">([^<]+)</)
metricas['Progreso'] = match[1]
end
# Buscar hitos completados
if match = content.match(/Hitos Completados[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Completados'] = match[1]
end
# Buscar hitos pendientes
if match = content.match(/Hitos Pendientes[^<]*<[^>]*>([^<]+)</)
metricas['Hitos Pendientes'] = match[1]
end
# Buscar esfuerzo total
if match = content.match(/Esfuerzo Total[^<]*<[^>]*>([^<]+)</)
metricas['Esfuerzo Total'] = match[1]
end
# Buscar horas físicas
if match = content.match(/Horas Físicas[^<]*<[^>]*>([^<]+)</)
metricas['Horas Físicas'] = match[1]
end
metricas
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
preview = DashboardPreview.new
# Parsear argumentos
modo = 'resumen'
dashboard_path = nil
ARGV.each_with_index do |arg, i|
if arg == '--dashboard' && ARGV[i + 1]
dashboard_path = ARGV[i + 1]
elsif i == 0 && !arg.start_with?('--')
modo = arg
end
end
preview = DashboardPreview.new(dashboard_path)
case modo
when 'resumen', 'summary'
success = preview.mostrar_vista_previa('resumen')
when 'hitos', 'milestones'
success = preview.mostrar_vista_previa('hitos')
when 'estructura', 'structure'
success = preview.mostrar_vista_previa('estructura')
when 'estadisticas', 'stats', 'statistics'
success = preview.mostrar_vista_previa('estadisticas')
when 'rapido', 'quick', 'fast'
success = preview.mostrar_resumen_rapido
when 'help', '--help', '-h'
puts "📖 USO:"
puts " #{$0} [modo] [--dashboard RUTA]"
puts
puts "MODOS:"
puts " resumen, summary - Resumen completo (predeterminado)"
puts " hitos, milestones - Hitos detallados"
puts " estructura, structure - Estructura del dashboard"
puts " estadisticas, stats - Estadísticas detalladas"
puts " rapido, quick - Resumen rápido"
puts " help - Mostrar esta ayuda"
puts
puts "OPCIONES:"
puts " --dashboard RUTA - Ruta personalizada al archivo dashboard"
puts
puts "EJEMPLOS:"
puts " #{$0} resumen"
puts " #{$0} hitos"
puts " #{$0} rapido"
puts " #{$0} --dashboard /ruta/al/dashboard.html estructura"
puts
success = true
else
puts "❌ Modo '#{modo}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 HERRAMIENTAS RELACIONADAS:"
puts " 1. limpiar.rb - Limpiar hitos duplicados"
puts " 2. agregar_hitos.rb - Agregar nuevos hitos"
puts " 3. actualizar_estados.rb - Actualizar estados de hitos"
puts " 4. verificar.rb - Verificar estructura del dashboard"
puts
puts "🔗 Dashboard: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
@@ -1,406 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/ejemplo_modulos_optimizados.rb
# ================================================
# Ejemplo de uso de los nuevos módulos optimizados del orquestador
# Demuestra cómo usar las funcionalidades centralizadas y reutilizables
require_relative 'lib/dashboard_core'
# ============================================================================
# EJEMPLO 1: GESTIÓN COMPLETA DE HITOS CON HitosManager
# ============================================================================
def ejemplo_gestion_hitos
puts "🎯 EJEMPLO 1: GESTIÓN COMPLETA DE HITOS"
puts "=" * 60
# 1. Crear instancia del manager
hitos_manager = DashboardCore::HitosManager.new
puts "✅ HitosManager creado"
# 2. Cargar hitos del dashboard
hitos = hitos_manager.cargar_hitos
puts "📊 Hitos cargados: #{hitos.size} hitos encontrados"
# 3. Buscar hitos específicos
puts "\n🔍 BUSQUEDA DE HITOS:"
# Buscar por ID
hito_f05001 = hitos_manager.buscar_hito_por_id('f05001')
if hito_f05001
puts " • Por ID 'f05001': #{hito_f05001[:titulo]}"
end
# Buscar por fase
hitos_fase5 = hitos_manager.buscar_hitos_por_fase(5)
puts " • Por fase 5: #{hitos_fase5.size} hitos"
# Buscar por estado
hitos_pendientes = hitos_manager.buscar_hitos_por_estado('pending')
puts " • Pendientes: #{hitos_pendientes.size} hitos"
# 4. Estadísticas
puts "\n📈 ESTADÍSTICAS:"
stats = hitos_manager.estadisticas
stats[:por_estado].each do |estado, cantidad|
estado_info = DashboardCore::ESTADOS_VALIDOS[estado] || DashboardCore::ESTADOS_VALIDOS['pending']
puts "#{estado_info[:nombre]}: #{cantidad}"
end
# 5. Reporte detallado
puts "\n📋 REPORTE DETALLADO:"
reporte = hitos_manager.reporte_detallado
puts reporte.lines.first(10).join # Mostrar primeras 10 líneas
# 6. Verificar duplicados
puts "\n🔍 VERIFICACIÓN DE DUPLICADOS:"
resultado = hitos_manager.verificar_duplicados
puts resultado
true
end
# ============================================================================
# EJEMPLO 2: GESTIÓN DE ESTADOS CON EstadosManager
# ============================================================================
def ejemplo_gestion_estados
puts "\n📊 EJEMPLO 2: GESTIÓN DE ESTADOS DE HITOS"
puts "=" * 60
# 1. Crear instancia del manager
estados_manager = DashboardCore::EstadosManager.new
puts "✅ EstadosManager creado"
# 2. Listar estados disponibles
puts "\n🎨 ESTADOS DISPONIBLES:"
estados_disponibles = estados_manager.listar_estados_disponibles
estados_disponibles.each do |estado|
puts "#{estado[:codigo]}: #{estado[:nombre]} (#{estado[:color]})"
end
# 3. Cargar y analizar estados actuales
puts "\n📈 ANÁLISIS DE ESTADOS ACTUALES:"
estados_manager.cargar_estados
# 4. Obtener hitos por estado
hitos_pendientes = estados_manager.hitos_pendientes
hitos_en_progreso = estados_manager.hitos_en_progreso
hitos_completados = estados_manager.hitos_completados
puts " • Pendientes: #{hitos_pendientes.size} hitos"
puts " • En progreso: #{hitos_en_progreso.size} hitos"
puts " • Completados: #{hitos_completados.size} hitos"
# 5. Reporte de estados
puts "\n📋 REPORTE DE ESTADOS:"
reporte = estados_manager.reporte_estados
puts reporte.lines.first(15).join # Mostrar primeras 15 líneas
# 6. Progreso por fase
puts "\n📁 PROGRESO POR FASE:"
progreso_fases = estados_manager.progreso_por_fase
progreso_fases.each do |fase, datos|
puts " • Fase #{fase}: #{datos[:progreso]}% (#{datos[:completados]}/#{datos[:total]} completados)"
end
true
end
# ============================================================================
# EJEMPLO 3: VERIFICACIÓN CON VerificacionManager
# ============================================================================
def ejemplo_verificacion
puts "\n🔍 EJEMPLO 3: VERIFICACIÓN COMPLETA DEL DASHBOARD"
puts "=" * 60
# 1. Crear instancia del manager
verificacion_manager = DashboardCore::VerificacionManager.new
puts "✅ VerificacionManager creado"
# 2. Verificación completa
puts "\n🏗️ VERIFICANDO ESTRUCTURA:"
resultado_estructura = verificacion_manager.verificar_estructura
if resultado_estructura[:exito]
puts " ✅ Estructura correcta"
else
puts " ❌ Problemas en estructura:"
resultado_estructura[:problemas].each do |problema|
puts "#{problema}"
end
end
# 3. Verificación de hitos
puts "\n🎯 VERIFICANDO HITOS:"
resultado_hitos = verificacion_manager.verificar_hitos
puts " • Total hitos: #{resultado_hitos[:detalles][:total_hitos]}"
puts " • Hitos válidos: #{resultado_hitos[:detalles][:hitos_validos]}"
puts " • Duplicados: #{resultado_hitos[:detalles][:duplicados]}"
# 4. Análisis detallado de hitos
puts "\n📋 ANÁLISIS DETALLADO DE HITOS:"
analisis = verificacion_manager.analizar_hitos_detallado
if analisis
puts " • Total: #{analisis[:total]} hitos"
puts " • Por estado: #{analisis[:por_estado].size} estados diferentes"
puts " • Por fase: #{analisis[:por_fase].size} fases identificadas"
end
# 5. Generar reporte de verificación
puts "\n📄 GENERANDO REPORTE DE VERIFICACIÓN..."
reporte = verificacion_manager.generar_reporte_verificacion
puts " ✅ Reporte generado (#{reporte.lines.count} líneas)"
true
end
# ============================================================================
# EJEMPLO 4: VISTA PREVIA CON VistaPreviaManager
# ============================================================================
def ejemplo_vista_previa
puts "\n👁️ EJEMPLO 4: VISTA PREVIA MULTI-MODO"
puts "=" * 60
# 1. Crear instancia del manager
vista_manager = DashboardCore::VistaPreviaManager.new
puts "✅ VistaPreviaManager creado"
# 2. Mostrar modos disponibles
puts "\n🔄 MODOS DISPONIBLES:"
DashboardCore::VistaPreviaManager::MODOS_VISTA_PREVIA.each do |modo, info|
puts "#{modo}: #{info[:descripcion]}"
end
# 3. Ejemplos de diferentes modos
puts "\n📊 EJEMPLOS DE SALIDA:"
# Modo rápido
puts "\n⚡ MODO RÁPIDO:"
begin
vista_manager.mostrar_vista_previa('rapido')
rescue => e
puts " Error: #{e.message}"
end
# Modo hitos
puts "\n🎯 MODO HITOS (primeros 3 hitos):"
begin
# Solo mostramos parte del output para no saturar
puts " [Mostrando resumen de hitos...]"
rescue => e
puts " Error: #{e.message}"
end
# Modo métricas
puts "\n📈 MODO MÉTRICAS:"
begin
vista_manager.mostrar_vista_previa('metricas')
rescue => e
puts " Error: #{e.message}"
end
true
end
# ============================================================================
# EJEMPLO 5: USO DE MÓDULOS DE UTILIDAD
# ============================================================================
def ejemplo_modulos_utilidad
puts "\n🛠️ EJEMPLO 5: MÓDULOS DE UTILIDAD"
puts "=" * 60
# 1. Generador de IDs
puts "\n🔢 GENERADOR DE IDs:"
nuevo_id = DashboardCore::HitoIdGenerator.generar_id(5, 6)
puts " • Nuevo ID para fase 5, secuencia 6: #{nuevo_id}"
info_id = DashboardCore::HitoIdGenerator.parsear_id('f05001')
if info_id
puts " • Parsear 'f05001': Fase #{info_id[:fase]}, Secuencia #{info_id[:secuencia]}"
end
es_valido = DashboardCore::HitoIdGenerator.es_valido?('f05001')
puts " • 'f05001' válido?: #{es_valido ? '✅ Sí' : '❌ No'}"
es_valido_mal = DashboardCore::HitoIdGenerator.es_valido?('BKP02')
puts " • 'BKP02' válido?: #{es_valido_mal ? '✅ Sí' : '❌ No'}"
# 2. Validaciones
puts "\n✅ VALIDACIONES:"
estado_valido = DashboardCore::Validator.validar_estado('in-progress')
puts " • Estado 'in-progress' válido?: #{estado_valido ? '✅ Sí' : '❌ No'}"
estado_invalido = DashboardCore::Validator.validar_estado('invalid-state')
puts " • Estado 'invalid-state' válido?: #{estado_invalido ? '✅ Sí' : '❌ No'}"
id_valido = DashboardCore::Validator.validar_id_hito('f05001')
puts " • ID 'f05001' válido?: #{id_valido ? '✅ Sí' : '❌ No'}"
fase_valida = DashboardCore::Validator.validar_fase('fase5')
puts " • Fase 'fase5' válida?: #{fase_valida ? '✅ Sí' : '❌ No'}"
# 3. Gestión de fases
puts "\n📁 GESTIÓN DE FASES:"
fases_disponibles = DashboardCore::FaseManager.fases_disponibles
puts " • Fases disponibles: #{fases_disponibles.join(', ')}"
hitos_fase5 = DashboardCore::FaseManager.hitos_por_fase('fase5')
puts " • Hitos predefinidos para fase 5: #{hitos_fase5.size}"
# 4. Gestión de backups
puts "\n💾 GESTIÓN DE BACKUPS:"
backup_dir = DashboardCore::CONFIG[:backups_dir]
backups = DashboardCore::BackupManager.listar_backups(backup_dir)
puts " • Backups encontrados: #{backups.size}"
true
end
# ============================================================================
# EJEMPLO 6: FLUJO DE TRABAJO COMPLETO
# ============================================================================
def ejemplo_flujo_trabajo_completo
puts "\n🔄 EJEMPLO 6: FLUJO DE TRABAJO COMPLETO"
puts "=" * 60
puts "Este ejemplo muestra un flujo de trabajo típico:"
puts "1. 📋 Verificar estado actual"
puts "2. 🎯 Agregar nuevos hitos"
puts "3. 📊 Actualizar estados"
puts "4. 🔍 Verificar cambios"
puts "5. 👁️ Vista previa final"
puts "6. 💾 Crear backup"
puts "\n📋 PASO 1: Verificar estado actual"
verificacion = DashboardCore::VerificacionManager.new
resultado = verificacion.verificar_completo
puts " • Verificación completada: #{resultado ? '✅' : '❌'}"
puts "\n🎯 PASO 2: Agregar nuevos hitos"
hitos_manager = DashboardCore::HitosManager.new
# Nota: En producción, se usaría hitos_manager.agregar_hitos_fase('fase5')
puts " • Listo para agregar hitos de fase 5"
puts "\n📊 PASO 3: Actualizar estados"
estados_manager = DashboardCore::EstadosManager.new
# Nota: En producción, se usaría estados_manager.actualizar_estado_hito('f05001', 'in-progress')
puts " • Listo para actualizar estados"
puts "\n🔍 PASO 4: Verificar cambios"
puts " • Verificación post-cambios lista"
puts "\n👁️ PASO 5: Vista previa final"
vista_manager = DashboardCore::VistaPreviaManager.new
puts " • Vista previa lista para ejecutar"
puts "\n💾 PASO 6: Crear backup"
base = DashboardCore::Base.new
backup_file = base.crear_backup('flujo_completo')
puts " • Backup creado: #{File.basename(backup_file)}"
puts "\n✅ FLUJO DE TRABAJO COMPLETADO"
true
end
# ============================================================================
# MENÚ PRINCIPAL
# ============================================================================
def mostrar_menu
puts "\n" + "=" * 60
puts "🎯 EJEMPLOS DE MÓDULOS OPTIMIZADOS - ORQUESTADOR P2601"
puts "=" * 60
puts
puts "Seleccione un ejemplo para ejecutar:"
puts " 1. 🎯 Gestión completa de hitos (HitosManager)"
puts " 2. 📊 Gestión de estados (EstadosManager)"
puts " 3. 🔍 Verificación completa (VerificacionManager)"
puts " 4. 👁️ Vista previa multi-modo (VistaPreviaManager)"
puts " 5. 🛠️ Módulos de utilidad (Generadores, Validaciones, etc.)"
puts " 6. 🔄 Flujo de trabajo completo"
puts " 7. 🚀 Ejecutar todos los ejemplos"
puts " 0. ❌ Salir"
puts
print "Opción: "
end
def ejecutar_ejemplo(opcion)
case opcion
when '1'
ejemplo_gestion_hitos
when '2'
ejemplo_gestion_estados
when '3'
ejemplo_verificacion
when '4'
ejemplo_vista_previa
when '5'
ejemplo_modulos_utilidad
when '6'
ejemplo_flujo_trabajo_completo
when '7'
puts "\n🚀 EJECUTANDO TODOS LOS EJEMPLOS..."
puts "=" * 60
ejemplo_gestion_hitos
ejemplo_gestion_estados
ejemplo_verificacion
ejemplo_vista_previa
ejemplo_modulos_utilidad
ejemplo_flujo_trabajo_completo
puts "\n✅ TODOS LOS EJEMPLOS COMPLETADOS"
when '0'
puts "👋 Saliendo..."
return false
else
puts "❌ Opción no válida"
end
true
end
# ============================================================================
# PROGRAMA PRINCIPAL
# ============================================================================
if __FILE__ == $0
begin
puts "🔧 CARGANDO MÓDULOS OPTIMIZADOS..."
puts "=" * 60
# Verificar que los módulos estén disponibles
require_relative 'lib/dashboard_core'
puts "✅ Módulo DashboardCore cargado"
# Verificar acceso al dashboard
base = DashboardCore::Base.new
begin
contenido = base.leer_dashboard
hitos = base.extraer_hitos(contenido)
puts "✅ Dashboard cargado: #{hitos.size} hitos encontrados"
rescue => e
puts "⚠️ Advertencia: #{e.message}"
puts " Algunos ejemplos pueden no funcionar correctamente"
end
# Menú interactivo
continuar = true
while continuar
mostrar_menu
opcion = gets.chomp
continuar = ejecutar_ejemplo(opcion)
end
puts "\n" + "=" * 60
puts "📚 RECURSOS ADICIONALES:"
puts "=" * 60
puts "• Documentación completa: tools/orquestador/README_OPTIMIZADO.md"
puts "• Script de migración: tools/orquestador/migrar_ids_hitos.rb"
puts "• Dashboard en producción: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts "• Dashboard estático: https://ns8.frlr.utn.edu.ar/P2601/"
end
end
-540
View File
@@ -1,540 +0,0 @@
#!/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
@@ -1,440 +0,0 @@
#!/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
@@ -1,315 +0,0 @@
#!/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
@@ -1,173 +0,0 @@
#!/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
@@ -1,582 +0,0 @@
#!/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
@@ -1,653 +0,0 @@
#!/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
-428
View File
@@ -1,428 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/migrar_ids_hitos.rb
# ======================================
# Script de migración para actualizar IDs de hitos al nuevo formato fNNNNN
# Migra IDs antiguos como BKP02, DB02, etc. a f05001, f05002, etc.
require 'fileutils'
class MigradorIDsHitos
# Mapeo de IDs antiguos a nuevos (Fase 5)
MAPEO_IDS = {
# Fase 5
'BKP02' => 'f05001',
'DB02' => 'f05002',
'MON01' => 'f05003',
'DOC01' => 'f05004',
'PERF01' => 'f05005',
# IDs genéricos para migración automática
'BKP' => 'f05', # Patrón para backups
'DB' => 'f05', # Patrón para bases de datos
'MON' => 'f05', # Patrón para monitoreo
'DOC' => 'f05', # Patrón para documentación
'PERF' => 'f05', # Patrón para performance
}.freeze
def initialize(dashboard_path = nil)
@dashboard_path = dashboard_path || default_dashboard_path
@backup_dir = File.join(File.dirname(@dashboard_path), 'backups')
@contador_migrados = 0
@contador_errores = 0
@ids_migrados = []
end
def default_dashboard_path
File.expand_path('../../../dashboard/index.html', __FILE__)
end
def migrar_ids
puts "🔄 MIGRANDO IDs DE HITOS AL NUEVO FORMATO"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard en #{@dashboard_path}"
return false
end
# Crear backup
crear_backup_migracion
# Leer contenido
puts "📖 Leyendo dashboard..."
contenido = File.read(@dashboard_path)
contenido_original = contenido.dup
# Extraer todos los hitos para análisis
hitos = extraer_hitos_con_ids(contenido)
puts "🔍 Encontrados #{hitos.size} hitos para analizar"
# Mostrar IDs actuales
mostrar_ids_actuales(hitos)
# Preguntar confirmación
unless confirmar_migracion(hitos.size)
puts "❌ Migración cancelada por el usuario"
return false
end
# Realizar migración
puts "\n🔄 Realizando migración..."
contenido_migrado = migrar_contenido(contenido)
# Verificar cambios
if contenido_migrado == contenido_original
puts "️ No se realizaron cambios (posiblemente ya están migrados)"
return true
end
# Guardar cambios
puts "💾 Guardando dashboard migrado..."
File.write(@dashboard_path, contenido_migrado)
# Mostrar resultados
mostrar_resultados_migracion
# Verificar integridad
verificar_integridad_migracion(contenido_migrado)
true
end
def simular_migracion
puts "🔍 SIMULACIÓN DE MIGRACIÓN (MODO DRY RUN)"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard"
return false
end
contenido = File.read(@dashboard_path)
hitos = extraer_hitos_con_ids(contenido)
puts "📊 ANÁLISIS DE MIGRACIÓN:"
puts " • Total de hitos encontrados: #{hitos.size}"
puts " • IDs que requieren migración: #{hitos.count { |h| requiere_migracion?(h[:id]) }}"
puts " • IDs ya en formato nuevo: #{hitos.count { |h| !requiere_migracion?(h[:id]) }}"
puts "\n📋 DETALLE DE MIGRACIÓN:"
hitos.each do |hito|
if requiere_migracion?(hito[:id])
nuevo_id = generar_nuevo_id(hito[:id])
puts "#{hito[:id]}#{nuevo_id} (#{hito[:titulo][0..50]}...)"
else
puts "#{hito[:id]} (ya migrado)"
end
end
puts "\n💡 RECOMENDACIONES:"
if hitos.any? { |h| requiere_migracion?(h[:id]) }
puts " • Ejecutar migración completa: ./migrar_ids_hitos.rb migrar"
puts " • Revisar mapeo de IDs antes de proceder"
else
puts " • Todos los IDs ya están en formato nuevo"
puts " • No se requiere migración"
end
true
end
def verificar_estado_migracion
puts "🔍 VERIFICANDO ESTADO DE MIGRACIÓN"
puts "=" * 60
unless File.exist?(@dashboard_path)
puts "❌ Error: No se encuentra el dashboard"
return false
end
contenido = File.read(@dashboard_path)
hitos = extraer_hitos_con_ids(contenido)
puts "📊 ESTADO ACTUAL:"
# Contar por tipo de ID
ids_antiguos = hitos.select { |h| requiere_migracion?(h[:id]) }
ids_nuevos = hitos.select { |h| !requiere_migracion?(h[:id]) }
puts " • IDs en formato antiguo: #{ids_antiguos.size}"
puts " • IDs en formato nuevo: #{ids_nuevos.size}"
puts " • Total de hitos: #{hitos.size}"
if ids_antiguos.any?
puts "\n⚠️ IDs QUE REQUIEREN MIGRACIÓN:"
ids_antiguos.each do |hito|
nuevo_id = generar_nuevo_id(hito[:id])
puts "#{hito[:id]}#{nuevo_id}"
end
end
if ids_nuevos.any?
puts "\n✅ IDs YA MIGRADOS:"
ids_nuevos.each do |hito|
puts "#{hito[:id]}"
end
end
# Verificar formato de IDs nuevos
puts "\n🔍 VALIDACIÓN DE FORMATOS:"
ids_nuevos.each do |hito|
if formato_valido_nuevo?(hito[:id])
puts "#{hito[:id]}: ✅ Formato válido"
else
puts "#{hito[:id]}: ❌ Formato inválido"
end
end
puts "\n💡 RECOMENDACIONES:"
if ids_antiguos.any?
puts " • Ejecutar migración para estandarizar IDs"
puts " • Usar: ./migrar_ids_hitos.rb migrar"
else
puts " • Todos los IDs están en formato nuevo"
puts " • Sistema listo para usar módulos optimizados"
end
true
end
private
def crear_backup_migracion
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_pre_migracion_#{timestamp}.html")
puts "💾 Creando backup pre-migración: #{backup_file}"
FileUtils.cp(@dashboard_path, backup_file)
puts "✅ Backup creado"
end
def extraer_hitos_con_ids(contenido)
hitos = []
contenido.scan(/<div class="tl-item">([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/m) do |hito_html|
hito = hito_html[0]
# Extraer ID
id_match = hito.match(/<span class="tl-id">([^<]+)<\/span>/)
titulo_match = hito.match(/<div class="tl-title">([^<]+)<\/div>/)
if id_match && titulo_match
hitos << {
id: id_match[1],
titulo: titulo_match[1],
html_completo: hito_html[0]
}
end
end
hitos.uniq { |h| h[:id] }
end
def mostrar_ids_actuales(hitos)
puts "\n📋 IDs ACTUALES ENCONTRADOS:"
hitos.each_with_index do |hito, i|
requiere = requiere_migracion?(hito[:id]) ? "⚠️ Requiere migración" : "✅ Ya migrado"
puts " #{i + 1}. #{hito[:id]}: #{hito[:titulo][0..40]}... (#{requiere})"
end
end
def confirmar_migracion(total_hitos)
puts "\n⚠️ ADVERTENCIA: Esta operación modificará el dashboard"
puts " • Total de hitos a analizar: #{total_hitos}"
puts " • Se creará un backup automático"
puts " • Los cambios no se pueden deshacer automáticamente"
# Para ejecución automática, siempre confirmar
puts "\n✅ Confirmación automática para migración"
true
end
def requiere_migracion?(id)
# Un ID requiere migración si no sigue el formato fNNNNN
!id.match(/^f\d{5}$/)
end
def formato_valido_nuevo?(id)
id.match(/^f\d{5}$/)
end
def generar_nuevo_id(id_antiguo)
# Buscar mapeo directo
return MAPEO_IDS[id_antiguo] if MAPEO_IDS.key?(id_antiguo)
# Buscar por prefijo
MAPEO_IDS.each do |prefijo, nuevo_base|
if id_antiguo.start_with?(prefijo)
# Extraer número del ID antiguo
numero_match = id_antiguo.match(/\d+/)
numero = numero_match ? numero_match[0].to_i : 1
# Generar nuevo ID con secuencia
return "#{nuevo_base}#{numero.to_s.rjust(3, '0')}"
end
end
# ID genérico para fase 5
"f05#{(@contador_migrados + 1).to_s.rjust(3, '0')}"
end
def migrar_contenido(contenido)
nuevo_contenido = contenido.dup
# Extraer todos los hitos
hitos = extraer_hitos_con_ids(contenido)
hitos.each do |hito|
if requiere_migracion?(hito[:id])
nuevo_id = generar_nuevo_id(hito[:id])
# Reemplazar en el contenido
nuevo_contenido = nuevo_contenido.gsub(
/<span class="tl-id">#{Regexp.escape(hito[:id])}<\/span>/,
"<span class=\"tl-id\">#{nuevo_id}</span>"
)
@contador_migrados += 1
@ids_migrados << { antiguo: hito[:id], nuevo: nuevo_id }
puts "#{hito[:id]}#{nuevo_id}"
end
end
nuevo_contenido
end
def mostrar_resultados_migracion
puts "\n📊 RESULTADOS DE LA MIGRACIÓN:"
puts "=" * 60
puts "✅ Migración completada exitosamente"
puts " • IDs migrados: #{@contador_migrados}"
puts " • Errores: #{@contador_errores}"
if @ids_migrados.any?
puts "\n📋 DETALLE DE MIGRACIONES:"
@ids_migrados.each do |migracion|
puts "#{migracion[:antiguo]}#{migracion[:nuevo]}"
end
end
puts "\n💡 RECOMENDACIONES POST-MIGRACIÓN:"
puts " 1. Verificar dashboard en producción"
puts " 2. Actualizar referencias en documentación"
puts " 3. Usar nuevos módulos optimizados"
puts " 4. Backup disponible en: #{@backup_dir}"
end
def verificar_integridad_migracion(contenido_migrado)
puts "\n🔍 VERIFICANDO INTEGRIDAD POST-MIGRACIÓN"
hitos = extraer_hitos_con_ids(contenido_migrado)
ids_invalidos = hitos.select { |h| requiere_migracion?(h[:id]) }
if ids_invalidos.empty?
puts "✅ Todos los IDs están en formato nuevo"
else
puts "⚠️ Se encontraron IDs aún en formato antiguo:"
ids_invalidos.each do |hito|
puts "#{hito[:id]}"
end
end
# Verificar duplicados
ids = hitos.map { |h| h[:id] }
duplicados = ids.select { |id| ids.count(id) > 1 }.uniq
if duplicados.empty?
puts "✅ No hay IDs duplicados"
else
puts "❌ IDs duplicados encontrados:"
duplicados.each do |id|
puts "#{id}"
end
end
end
end
# Interfaz de línea de comandos
if __FILE__ == $0
begin
migrador = MigradorIDsHitos.new
# Parsear argumentos
command = ARGV[0] || 'verificar'
case command
when 'migrar', 'migrate'
success = migrador.migrar_ids
when 'simular', 'simulate', 'dry-run'
success = migrador.simular_migracion
when 'verificar', 'check', 'status'
success = migrador.verificar_estado_migracion
when 'help', '--help', '-h'
puts "📖 MIGRADOR DE IDs DE HITOS - Dashboard P2601"
puts "=" * 60
puts
puts "DESCRIPCIÓN:"
puts " Migra IDs de hitos del formato antiguo (BKP02, DB02, etc.)"
puts " al nuevo formato estandarizado (f05001, f05002, etc.)"
puts
puts "USO:"
puts " #{$0} [comando]"
puts
puts "COMANDOS:"
puts " migrar, migrate - Realizar migración completa (con backup)"
puts " simular, simulate - Simular migración (modo dry-run)"
puts " verificar, check - Verificar estado actual de migración"
puts " help - Mostrar esta ayuda"
puts
puts "MAFEO DE IDs (Fase 5):"
puts " • BKP02 → f05001 (Backup Automático de VMs)"
puts " • DB02 → f05002 (Backup Automático de Base de Datos)"
puts " • MON01 → f05003 (Monitoreo de Recursos)"
puts " • DOC01 → f05004 (Documentación Operativa)"
puts " • PERF01 → f05005 (Pruebas de Rendimiento)"
puts
puts "CARACTERÍSTICAS:"
puts " • Backup automático pre-migración"
puts " • Confirmación interactiva"
puts " • Verificación de integridad post-migración"
puts " • Modo dry-run para pruebas"
puts
puts "EJEMPLOS:"
puts " #{$0} verificar # Verificar estado actual"
puts " #{$0} simular # Simular migración"
puts " #{$0} migrar # Realizar migración completa"
puts
success = true
else
puts "❌ Comando '#{command}' no reconocido"
puts " Use '#{$0} help' para ver la ayuda"
success = false
end
if success
puts "\n🚀 PRÓXIMOS PASOS:"
puts " 1. Usar nuevos módulos optimizados en tools/orquestador/lib/"
puts " 2. Consultar README_OPTIMIZADO.md para documentación"
puts " 3. Verificar dashboard: https://ns8.frlr.utn.edu.ar/bitacoras/p2601"
puts
puts "📅 #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts "=" * 60
else
exit 1
end
rescue => e
puts "❌ Error: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-489
View File
@@ -1,489 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/probar_buscador.rb — Script de prueba para sistema de búsqueda
# ================================================================================
# Este script prueba la funcionalidad del sistema de búsqueda y filtrado
# implementado en tools/adn/db/core/buscador_bd.rb
#
# Uso:
# ruby tools/probar_buscador.rb [OPCIONES]
#
# Opciones:
# --test-all Ejecutar todas las pruebas
# --test-entradas Probar búsqueda en entradas
# --test-nodos Probar búsqueda en nodos
# --test-temas Probar búsqueda en temas
# --test-bitacoras Probar búsqueda en bitácoras
# --test-avanzado Probar búsqueda avanzada
# --test-indices Probar creación de índices
# --test-metricas Probar métricas de rendimiento
# --verbose Mostrar detalles de ejecución
# --help Mostrar ayuda
#
# Ejemplos:
# ruby tools/probar_buscador.rb --test-all
# ruby tools/probar_buscador.rb --test-entradas --test-nodos
# ruby tools/probar_buscador.rb --test-indices --verbose
require 'pg'
require 'yaml'
require 'optparse'
require 'json'
# Cargar módulos locales
$LOAD_PATH.unshift(File.join(__dir__, 'adn', 'db', 'core'))
require 'buscador_bd'
class ProbadorBuscador
def initialize(opciones)
@opciones = opciones
@config = cargar_configuracion
@conn = nil
@buscador = nil
@resultados_pruebas = []
@tiempo_inicio = Time.now
end
def ejecutar
puts "🔍 PROBANDO SISTEMA DE BÚSQUEDA ADN BITACORAS"
puts "=" * 70
puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts ""
conectar_bd
inicializar_buscador
# Ejecutar pruebas según opciones
if @opciones[:test_all] || @opciones[:test_entradas]
probar_busqueda_entradas
end
if @opciones[:test_all] || @opciones[:test_nodos]
probar_busqueda_nodos
end
if @opciones[:test_all] || @opciones[:test_temas]
probar_busqueda_temas
end
if @opciones[:test_all] || @opciones[:test_bitacoras]
probar_busqueda_bitacoras
end
if @opciones[:test_all] || @opciones[:test_avanzado]
probar_busqueda_avanzada
end
if @opciones[:test_all] || @opciones[:test_indices]
probar_creacion_indices
end
if @opciones[:test_all] || @opciones[:test_metricas]
probar_metricas_rendimiento
end
mostrar_resumen
ensure
desconectar_bd
end
private
def cargar_configuracion
config_path = File.join(__dir__, 'config', 'database.yml')
unless File.exist?(config_path)
puts "❌ Archivo de configuración no encontrado: #{config_path}"
exit 1
end
config = YAML.load_file(config_path)
config['development'] || config['production'] || config['test']
end
def conectar_bd
puts "🔌 Conectando a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}"
@conn = PG.connect(
host: @config['host'],
port: @config['port'],
dbname: @config['database'],
user: @config['username'],
password: @config['password']
)
puts "✅ Conexión establecida\n\n"
rescue PG::Error => e
puts "❌ Error de conexión: #{e.message}"
puts " Asegúrate de que el contenedor Docker esté en ejecución:"
puts " cd dtic-BITACORAs && docker-compose up -d"
exit 1
end
def desconectar_bd
@conn&.close
puts "\n🔌 Conexión cerrada"
end
def inicializar_buscador
puts "🚀 Inicializando BuscadorBD..."
@buscador = BitacorasDB::BuscadorBD.new(@conn)
puts "✅ BuscadorBD inicializado\n\n"
end
def probar_busqueda_entradas
registrar_prueba("Búsqueda en entradas") do
puts "📋 Probando búsqueda en entradas..."
puts "-" * 50
# 1. Búsqueda simple por texto
puts "1. Búsqueda simple por texto 'dashboard':"
resultados = @buscador.buscar_entradas(texto: "dashboard", limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
# 2. Filtrado por estado
puts "\n2. Filtrado por estado '✅' (completado):"
resultados = @buscador.buscar_entradas(estado: "", limite: 3, formato: :texto)
mostrar_resultados(resultados, 3)
# 3. Filtrado por modo
puts "\n3. Filtrado por modo 'P' (presencial):"
resultados = @buscador.buscar_entradas(modo: "P", limite: 3, formato: :texto)
mostrar_resultados(resultados, 3)
# 4. Búsqueda combinada
puts "\n4. Búsqueda combinada (texto + estado):"
resultados = @buscador.buscar_entradas(
texto: "sistema",
estado: "",
limite: 3,
formato: :texto
)
mostrar_resultados(resultados, 3)
# 5. Formato JSON
puts "\n5. Búsqueda en formato JSON (primeros 2 resultados):"
resultados_json = @buscador.buscar_entradas(limite: 2, formato: :json)
if resultados_json && !resultados_json.empty?
parsed = JSON.parse(resultados_json)
puts " • Total resultados: #{parsed.size}"
puts " • Primer resultado: #{parsed.first['descripcion'][0..50]}..." if parsed.any?
end
true
end
end
def probar_busqueda_nodos
registrar_prueba("Búsqueda en nodos") do
puts "📋 Probando búsqueda en nodos..."
puts "-" * 50
# 1. Búsqueda por nombre
puts "1. Búsqueda por nombre 'ns8':"
resultados = @buscador.buscar_nodos(texto: "ns8", limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
# 2. Filtrado por tipo
puts "\n2. Filtrado por tipo 'servidor':"
resultados = @buscador.buscar_nodos(tipo: "servidor", limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
# 3. Nodos activos
puts "\n3. Nodos activos:"
resultados = @buscador.buscar_nodos(activo: true, limite: 10, formato: :texto)
mostrar_resultados(resultados, 10)
true
end
end
def probar_busqueda_temas
registrar_prueba("Búsqueda en temas") do
puts "📋 Probando búsqueda en temas..."
puts "-" * 50
# 1. Búsqueda por texto
puts "1. Búsqueda por texto 'implementación':"
resultados = @buscador.buscar_temas(texto: "implementación", limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
# 2. Filtrado por estado
puts "\n2. Temas con estado '✅':"
resultados = @buscador.buscar_temas(estado: "", limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
true
end
end
def probar_busqueda_bitacoras
registrar_prueba("Búsqueda en bitácoras") do
puts "📋 Probando búsqueda en bitácoras..."
puts "-" * 50
# 1. Últimas bitácoras
puts "1. Últimas 5 bitácoras:"
resultados = @buscador.buscar_bitacoras(limite: 5, formato: :texto)
mostrar_resultados(resultados, 5)
# 2. Búsqueda en resumen
puts "\n2. Búsqueda en resumen 'trabajo':"
resultados = @buscador.buscar_bitacoras(texto: "trabajo", limite: 3, formato: :texto)
mostrar_resultados(resultados, 3)
true
end
end
def probar_busqueda_avanzada
registrar_prueba("Búsqueda avanzada") do
puts "📋 Probando búsqueda avanzada..."
puts "-" * 50
puts "Búsqueda en todas las tablas por 'sistema':"
resultados = @buscador.buscar_avanzado(
texto: "sistema",
tipos: [:entradas, :temas, :nodos, :bitacoras],
limite_por_tipo: 3,
formato: :hash
)
if resultados[:estadisticas]
stats = resultados[:estadisticas]
puts " • Total resultados: #{stats[:total_resultados]}"
puts " • Tipos buscados: #{stats[:tipos_buscados].join(', ')}"
puts " • Texto buscado: #{stats[:texto_buscado]}"
end
[:entradas, :temas, :nodos, :bitacoras].each do |tipo|
if resultados[tipo] && resultados[tipo].any?
puts "#{tipo}: #{resultados[tipo].size} resultados"
end
end
true
end
end
def probar_creacion_indices
registrar_prueba("Creación de índices") do
puts "📋 Probando creación de índices recomendados..."
puts "-" * 50
puts "Creando índices para optimización..."
resultado = @buscador.crear_indices_recomendados
if resultado
puts " • Total índices: #{resultado[:total_indices]}"
puts " • Índices creados: #{resultado[:creados]}"
puts " • Errores: #{resultado[:errores]}"
if @opciones[:verbose] && resultado[:detalles]
puts "\n Detalles:"
resultado[:detalles].each do |detalle|
estado = detalle[:estado] == 'creado' ? '✅' : '❌'
puts " #{estado} #{detalle[:indice].split(' ON ').first}"
end
end
end
true
end
end
def probar_metricas_rendimiento
registrar_prueba("Métricas de rendimiento") do
puts "📋 Probando métricas de rendimiento..."
puts "-" * 50
metricas = @buscador.metricas_rendimiento
if metricas[:tamanos_tablas]
puts "Tamaños de tablas:"
metricas[:tamanos_tablas].each do |tabla|
puts "#{tabla['table_name']}: #{tabla['tamaño_total']} (#{tabla['registros']} registros)"
end
end
if metricas[:indices_uso] && metricas[:indices_uso].any?
puts "\nÍndices más usados:"
metricas[:indices_uso].first(3).each do |indice|
puts "#{indice['nombre_indice']}: #{indice['escaneos']} escaneos"
end
end
if metricas[:metricas_internas]
puts "\nMétricas internas:"
metricas[:metricas_internas].each do |key, value|
puts "#{key}: #{value}"
end
end
true
end
end
def registrar_prueba(nombre, &block)
puts "\n🧪 PRUEBA: #{nombre}"
inicio = Time.now
begin
resultado = block.call
duracion = Time.now - inicio
@resultados_pruebas << {
nombre: nombre,
estado: resultado ? '✅' : '❌',
duracion: duracion.round(3),
timestamp: Time.now.strftime('%H:%M:%S')
}
puts " Duración: #{duracion.round(3)}s"
puts " Estado: #{resultado ? '✅ PASÓ' : '❌ FALLÓ'}"
puts ""
resultado
rescue => e
duracion = Time.now - inicio
@resultados_pruebas << {
nombre: nombre,
estado: '💥',
duracion: duracion.round(3),
error: e.message,
timestamp: Time.now.strftime('%H:%M:%S')
}
puts " ❌ ERROR: #{e.message}"
puts " Backtrace: #{e.backtrace.first}" if @opciones[:verbose]
puts " Duración: #{duracion.round(3)}s"
puts ""
false
end
end
def mostrar_resultados(resultados, max_mostrar = 3)
if resultados.is_a?(String)
# Formato texto
lineas = resultados.split("\n")
lineas.first(max_mostrar * 5).each do |linea|
puts " #{linea}" unless linea.strip.empty?
end
puts " ..." if lineas.size > max_mostrar * 5
elsif resultados.is_a?(Array)
# Formato hash/array
puts " • Total resultados: #{resultados.size}"
resultados.first(max_mostrar).each_with_index do |resultado, idx|
if resultado.is_a?(Hash)
desc = resultado['descripcion'] || resultado['nombre'] || resultado['titulo'] || 'Sin título'
puts " #{idx + 1}. #{desc[0..60]}..."
end
end
puts " ..." if resultados.size > max_mostrar
else
puts " • Resultados: #{resultados.inspect[0..100]}..."
end
end
def mostrar_resumen
tiempo_total = Time.now - @tiempo_inicio
puts "\n" + "=" * 70
puts "📊 RESUMEN DE PRUEBAS"
puts "=" * 70
@resultados_pruebas.each do |prueba|
puts " #{prueba[:estado]} #{prueba[:nombre]} (#{prueba[:duracion]}s)"
if prueba[:error]
puts " Error: #{prueba[:error][0..100]}..."
end
end
puts "\n📈 ESTADÍSTICAS:"
puts " • Total pruebas: #{@resultados_pruebas.size}"
puts " • Pruebas exitosas: #{@resultados_pruebas.count { |p| p[:estado] == '✅' }}"
puts " • Pruebas fallidas: #{@resultados_pruebas.count { |p| p[:estado] == '❌' }}"
puts " • Errores: #{@resultados_pruebas.count { |p| p[:estado] == '💥' }}"
puts " • Tiempo total: #{tiempo_total.round(2)}s"
exit_code = @resultados_pruebas.any? { |p| p[:estado] != '✅' } ? 1 : 0
puts "\n🚀 Resultado: #{exit_code == 0 ? '✅ TODAS LAS PRUEBAS PASARON' : '❌ ALGUNAS PRUEBAS FALLARON'}"
puts "=" * 70
exit exit_code
end
end
# Parsear opciones de línea de comandos
opciones = {
test_all: false,
test_entradas: false,
test_nodos: false,
test_temas: false,
test_bitacoras: false,
test_avanzado: false,
test_indices: false,
test_metricas: false,
verbose: false
}
OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/probar_buscador.rb [OPCIONES]"
opts.on("--test-all", "Ejecutar todas las pruebas") do
opciones[:test_all] = true
end
opts.on("--test-entradas", "Probar búsqueda en entradas") do
opciones[:test_entradas] = true
end
opts.on("--test-nodos", "Probar búsqueda en nodos") do
opciones[:test_nodos] = true
end
opts.on("--test-temas", "Probar búsqueda en temas") do
opciones[:test_temas] = true
end
opts.on("--test-bitacoras", "Probar búsqueda en bitácoras") do
opciones[:test_bitacoras] = true
end
opts.on("--test-avanzado", "Probar búsqueda avanzada") do
opciones[:test_avanzado] = true
end
opts.on("--test-indices", "Probar creación de índices") do
opciones[:test_indices] = true
end
opts.on("--test-metricas", "Probar métricas de rendimiento") do
opciones[:test_metricas] = true
end
opts.on("--verbose", "Mostrar detalles de ejecución") do
opciones[:verbose] = true
end
opts.on("-h", "--help", "Mostrar ayuda") do
puts opts
exit
end
end.parse!
# Si no se especificó ninguna prueba, ejecutar todas
if opciones.values.none? { |v| v == true }
opciones[:test_all] = true
end
# Ejecutar probador
begin
probador = ProbadorBuscador.new(opciones)
probador.ejecutar
rescue => e
puts "❌ Error fatal: #{e.message}"
puts e.backtrace if opciones[:verbose]
exit 1
end
-325
View File
@@ -1,325 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/probar_buscador_simple.rb — Prueba de la versión simplificada del sistema de búsqueda
# ================================================================================
# Prueba funcional del sistema de búsqueda simplificado para dtic-BITACORAs
# Esta prueba demuestra la funcionalidad básica del sistema de búsqueda
require 'pg'
require 'yaml'
require 'json'
# Cargar módulos locales
$LOAD_PATH.unshift(File.join(__dir__, 'adn', 'db', 'core'))
require 'buscador_bd_simple'
class ProbadorBuscadorSimple
def initialize
@config = cargar_configuracion
@conn = nil
@buscador = nil
end
def ejecutar
puts "🔍 PROBANDO SISTEMA DE BÚSQUEDA SIMPLIFICADO"
puts "=" * 70
puts "Fecha: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
puts ""
conectar_bd
inicializar_buscador
# Ejecutar pruebas
probar_busqueda_entradas
probar_busqueda_nodos
probar_busqueda_temas
probar_busqueda_bitacoras
probar_busqueda_avanzada
probar_estadisticas
probar_creacion_indices
mostrar_resumen
ensure
desconectar_bd
end
private
def cargar_configuracion
config_path = File.join(__dir__, 'config', 'database.yml')
unless File.exist?(config_path)
puts "❌ Archivo de configuración no encontrado: #{config_path}"
exit 1
end
config = YAML.load_file(config_path)
config['development'] || config['production'] || config['test']
end
def conectar_bd
puts "🔌 Conectando a PostgreSQL: #{@config['host']}:#{@config['port']}/#{@config['database']}"
@conn = PG.connect(
host: @config['host'],
port: @config['port'],
dbname: @config['database'],
user: @config['username'],
password: @config['password']
)
puts "✅ Conexión establecida\n\n"
rescue PG::Error => e
puts "❌ Error de conexión: #{e.message}"
puts " Asegúrate de que el contenedor Docker esté en ejecución:"
puts " cd dtic-BITACORAs && docker-compose up -d"
exit 1
end
def desconectar_bd
@conn&.close
puts "\n🔌 Conexión cerrada"
end
def inicializar_buscador
puts "🚀 Inicializando BuscadorBDSimple..."
@buscador = BitacorasDB::BuscadorBDSimple.new(@conn)
puts "✅ BuscadorBDSimple inicializado\n\n"
end
def probar_busqueda_entradas
puts "📋 PRUEBA 1: Búsqueda en entradas"
puts "-" * 50
# 1. Búsqueda simple por texto
puts "1. Búsqueda por texto 'dashboard':"
resultados = @buscador.buscar_entradas_simple(texto: "dashboard", limite: 3, formato: :texto)
puts resultados
puts ""
# 2. Filtrado por estado
puts "2. Entradas con estado '✅' (completado):"
resultados = @buscador.buscar_entradas_simple(estado: "", limite: 2, formato: :texto)
puts resultados
puts ""
# 3. Filtrado por modo
puts "3. Entradas con modo 'P' (presencial):"
resultados = @buscador.buscar_entradas_simple(modo: "P", limite: 2, formato: :texto)
puts resultados
puts ""
# 4. Búsqueda combinada
puts "4. Búsqueda combinada (texto 'sistema' + estado '⏳'):"
resultados = @buscador.buscar_entradas_simple(
texto: "sistema",
estado: "",
limite: 2,
formato: :texto
)
puts resultados
puts ""
# 5. Formato JSON
puts "5. Búsqueda en formato JSON (primeras 2 entradas):"
resultados_json = @buscador.buscar_entradas_simple(limite: 2, formato: :json)
if resultados_json && !resultados_json.empty?
parsed = JSON.parse(resultados_json)
puts " • Total resultados: #{parsed.size}"
if parsed.any?
primera = parsed.first
puts " • Primera entrada: #{primera['descripcion'][0..50]}..."
puts " • Estado: #{primera['estado']}"
puts " • Nodo: #{primera['nodo_nombre'] || 'Sin nodo'}"
end
end
puts ""
end
def probar_busqueda_nodos
puts "📋 PRUEBA 2: Búsqueda en nodos"
puts "-" * 50
# 1. Búsqueda por nombre
puts "1. Búsqueda por nombre 'ns8':"
resultados = @buscador.buscar_nodos_simple(texto: "ns8", limite: 5, formato: :texto)
puts resultados
puts ""
# 2. Filtrado por tipo
puts "2. Nodos de tipo 'servidor':"
resultados = @buscador.buscar_nodos_simple(tipo: "servidor", limite: 5, formato: :texto)
puts resultados
puts ""
# 3. Nodos activos
puts "3. Nodos activos:"
resultados = @buscador.buscar_nodos_simple(activo: true, limite: 10, formato: :texto)
puts resultados
puts ""
end
def probar_busqueda_temas
puts "📋 PRUEBA 3: Búsqueda en temas"
puts "-" * 50
# 1. Búsqueda por texto
puts "1. Búsqueda por texto 'implementación':"
resultados = @buscador.buscar_temas_simple(texto: "implementación", limite: 5, formato: :texto)
puts resultados
puts ""
# 2. Filtrado por estado
puts "2. Temas con estado '✅':"
resultados = @buscador.buscar_temas_simple(estado: "", limite: 5, formato: :texto)
puts resultados
puts ""
end
def probar_busqueda_bitacoras
puts "📋 PRUEBA 4: Búsqueda en bitácoras"
puts "-" * 50
# 1. Últimas bitácoras
puts "1. Últimas 5 bitácoras:"
resultados = @buscador.buscar_bitacoras_simple(limite: 5, formato: :texto)
puts resultados
puts ""
# 2. Búsqueda en resumen
puts "2. Búsqueda en resumen 'trabajo':"
resultados = @buscador.buscar_bitacoras_simple(texto: "trabajo", limite: 3, formato: :texto)
puts resultados
puts ""
end
def probar_busqueda_avanzada
puts "📋 PRUEBA 5: Búsqueda avanzada"
puts "-" * 50
puts "Búsqueda en todas las tablas por 'sistema':"
resultados = @buscador.buscar_avanzado_simple(texto: "sistema", limite_por_tipo: 3)
if resultados[:estadisticas]
stats = resultados[:estadisticas]
puts " • Total resultados: #{stats[:total_resultados]}"
puts " • Texto buscado: #{stats[:texto_buscado]}"
puts " • Timestamp: #{stats[:timestamp]}"
end
[:entradas, :nodos, :temas, :bitacoras].each do |tipo|
if resultados[tipo] && resultados[tipo].is_a?(Array)
puts "#{tipo}: #{resultados[tipo].size} resultados"
elsif resultados["#{tipo}_error".to_sym]
puts "#{tipo}: ERROR - #{resultados["#{tipo}_error".to_sym]}"
end
end
puts ""
end
def probar_estadisticas
puts "📋 PRUEBA 6: Estadísticas del sistema"
puts "-" * 50
estadisticas = @buscador.estadisticas_basicas
puts "📊 Conteo por tabla:"
[:nodos, :bitacoras, :temas, :entradas].each do |tabla|
if estadisticas[tabla]
puts "#{tabla}: #{estadisticas[tabla]} registros"
end
end
if estadisticas[:entradas_por_estado] && estadisticas[:entradas_por_estado].any?
puts "\n📈 Entradas por estado:"
estadisticas[:entradas_por_estado].each do |estado|
emoji = case estado['estado']
when '⏳' then '⏳'
when '✅' then '✅'
when '❌' then '❌'
when '⚠️' then '⚠️'
when '⏸️' then '⏸️'
else estado['estado']
end
puts "#{emoji}: #{estado['total']} entradas"
end
end
if estadisticas[:entradas_ia]
puts "\n🤖 Entradas generadas por IA:"
puts " • Total: #{estadisticas[:entradas_ia]}"
if estadisticas[:porcentaje_ia]
puts " • Porcentaje: #{estadisticas[:porcentaje_ia]}%"
end
end
puts "\n🕐 Última actualización: #{estadisticas[:timestamp]}"
puts ""
end
def probar_creacion_indices
puts "📋 PRUEBA 7: Creación de índices"
puts "-" * 50
puts "Creando índices básicos para optimización..."
resultado = @buscador.crear_indices_basicos
if resultado
puts " • Total índices: #{resultado[:total_indices]}"
puts " • Índices creados: #{resultado[:creados]}"
puts " • Errores: #{resultado[:errores]}"
if resultado[:detalles]
puts "\n Detalles:"
resultado[:detalles].each do |detalle|
estado = detalle[:estado] == 'creado' ? '✅' : '❌'
puts " #{estado} #{detalle[:indice]}"
end
end
end
puts ""
end
def mostrar_resumen
puts "=" * 70
puts "🎉 PRUEBAS COMPLETADAS EXITOSAMENTE"
puts "=" * 70
puts ""
puts "✅ Sistema de búsqueda simplificado funcionando correctamente"
puts "✅ Todas las operaciones básicas implementadas:"
puts " • Búsqueda en entradas, nodos, temas y bitácoras"
puts " • Filtrado por múltiples criterios"
puts " • Formatos de salida: texto, JSON, hash"
puts " • Búsqueda avanzada en múltiples tablas"
puts " • Estadísticas del sistema"
puts " • Creación de índices para optimización"
puts ""
puts "🚀 El sistema está listo para integrarse con la CLI ADN"
puts "📊 Total de datos en sistema:"
puts " • Nodos: 14 servidores/VMs/PCs"
puts " • Bitácoras: 15 días registrados"
puts " • Temas: 49 agrupadores de actividades"
puts " • Entradas: 112 actividades cronológicas"
puts ""
puts "💡 Próximos pasos:"
puts " 1. Integrar BuscadorBDSimple con subcomando_db.rb"
puts " 2. Crear comandos CLI: adn db buscar:entradas, adn db buscar:nodos, etc."
puts " 3. Agregar autocompletado y ayuda contextual"
puts " 4. Implementar sistema de logging para búsquedas"
puts ""
puts "📅 " + Time.now.strftime('%Y-%m-%d %H:%M:%S')
puts "=" * 70
end
end
# Ejecutar pruebas
if __FILE__ == $0
begin
probador = ProbadorBuscadorSimple.new
probador.ejecutar
rescue => e
puts "❌ Error fatal durante las pruebas: #{e.message}"
puts e.backtrace if ENV['DEBUG']
exit 1
end
end
-101
View File
@@ -1,101 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/prueba_simple_buscador.rb — Prueba simplificada del sistema de búsqueda
# ================================================================================
# Prueba básica del sistema de búsqueda para identificar problemas
require 'pg'
require 'yaml'
# Cargar configuración
config_path = File.join(__dir__, 'config', 'database.yml')
config = YAML.load_file(config_path)['development']
# Conectar a PostgreSQL
puts "🔌 Conectando a PostgreSQL..."
conn = PG.connect(
host: config['host'],
port: config['port'],
dbname: config['database'],
user: config['username'],
password: config['password']
)
puts "✅ Conexión establecida\n\n"
# Prueba 1: Consulta SQL simple
puts "1. Consulta SQL simple (entradas):"
resultados = conn.exec("SELECT id, descripcion, estado FROM bitacoras.entradas LIMIT 3")
resultados.each do |row|
puts "#{row['id']}: #{row['descripcion'][0..50]}... (#{row['estado']})"
end
# Prueba 2: Búsqueda con LIKE
puts "\n2. Búsqueda con LIKE 'dashboard':"
resultados = conn.exec_params(
"SELECT id, descripcion, estado FROM bitacoras.entradas WHERE descripcion ILIKE $1 LIMIT 3",
['%dashboard%']
)
if resultados.ntuples > 0
resultados.each do |row|
puts "#{row['id']}: #{row['descripcion'][0..50]}... (#{row['estado']})"
end
else
puts " • No se encontraron resultados"
end
# Prueba 3: Consulta con JOIN
puts "\n3. Consulta con JOIN (entradas + nodos):"
sql = <<-SQL
SELECT e.id, e.descripcion, e.estado, n.nombre as nodo_nombre
FROM bitacoras.entradas e
LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id
WHERE e.estado = '✅'
LIMIT 3
SQL
resultados = conn.exec(sql)
if resultados.ntuples > 0
resultados.each do |row|
puts "#{row['id']}: #{row['descripcion'][0..50]}... (#{row['estado']}) - Nodo: #{row['nodo_nombre']}"
end
else
puts " • No se encontraron entradas completadas"
end
# Prueba 4: Full-text search (si existe el índice)
puts "\n4. Probando full-text search:"
begin
# Primero verificar si existe la extensión
conn.exec("CREATE EXTENSION IF NOT EXISTS pg_trgm")
# Búsqueda simple con trigram
resultados = conn.exec_params(
"SELECT id, descripcion, estado FROM bitacoras.entradas WHERE descripcion % $1 LIMIT 3",
['sistema']
)
if resultados.ntuples > 0
puts " • Búsqueda trigram encontrada #{resultados.ntuples} resultados"
resultados.each do |row|
puts " - #{row['id']}: #{row['descripcion'][0..50]}..."
end
else
puts " • No se encontraron resultados con trigram"
end
rescue => e
puts " • Error en full-text search: #{e.message}"
end
# Prueba 5: Estadísticas
puts "\n5. Estadísticas de la base de datos:"
tablas = ['nodos', 'bitacoras', 'temas', 'entradas']
tablas.each do |tabla|
count = conn.exec("SELECT COUNT(*) as total FROM bitacoras.#{tabla}")[0]['total']
puts "#{tabla}: #{count} registros"
end
conn.close
puts "\n🔌 Conexión cerrada"
puts "\n✅ Prueba completada"