#!/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