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

This commit is contained in:
Ricardo Monla
2026-03-06 18:03:07 -03:00
parent 15725e0b3c
commit ac9bdf516b
286 changed files with 36187 additions and 112775 deletions
+365
View File
@@ -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
+354
View File
@@ -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
View File
@@ -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
+208
View File
@@ -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
+419
View File
@@ -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
+540
View File
@@ -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
+368
View File
@@ -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
+534
View File
@@ -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