Reestructuración modular integral de servicios, reorganización de gestión en docs/ y limpieza profunda del repositorio
This commit is contained in:
+365
@@ -0,0 +1,365 @@
|
||||
#!/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
|
||||
Executable
+354
@@ -0,0 +1,354 @@
|
||||
#!/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
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
#!/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
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/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
|
||||
Executable
+419
@@ -0,0 +1,419 @@
|
||||
#!/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
|
||||
Executable
+540
@@ -0,0 +1,540 @@
|
||||
#!/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
|
||||
Executable
+368
@@ -0,0 +1,368 @@
|
||||
#!/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
|
||||
Executable
+534
@@ -0,0 +1,534 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user