355 lines
11 KiB
Ruby
Executable File
355 lines
11 KiB
Ruby
Executable File
#!/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
|