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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"*
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user