[ADN / Bitácora] Evolución Progresiva: Sistema de Iconografía Dual (Estado + Semántica)

This commit is contained in:
Ricardo Monla
2026-02-20 22:52:05 -03:00
parent ca715aee69
commit 2f4dcb62ea
18 changed files with 1666 additions and 15 deletions
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/ruby
# frozen_string_literal: true
# ==========================================================
# dtic-BKPs - Automatic Backup Processor (Unified Script)
# ----------------------------------------------------------
# APP: dtic-BKPs
# VERSION: v5.5.1
# AUTHOR: Ricardo MONLA ( rmonla@ )
# LICENSE: MIT License
# ==========================================================
# --- REQUIRES GLOBALES ---
require 'fileutils'
require 'open3'
require 'time'
require 'io/console'
require 'shellwords'
require 'logger'
# --- CARGA INICIAL DE MÓDULOS ---
# (Se asume que los otros archivos están en el mismo directorio o accesibles)
# require_relative 'core/globals'
require_relative 'fxs/config_handler'
require_relative 'fxs/ui_handlers'
require_relative 'fxs/proc_syncPmox'
require_relative 'fxs/sync_rclone'
require_relative 'fxs/proc_syncXEN'
# ==========================================================
# CONSTANTES GLOBALES
# ==========================================================
# --- CONSTANTES GLOBALES DEL SCRIPT ---
APP_NOM_BASE = 'dtic-BKPs'
APP_NOM = "#{APP_NOM_BASE}_app.rb"
APP_VER = 'v5.5.1' # Actualizado
APP_AUTOR = "Ricardo MONLA ( rmonla@ )"
APP_DIR_SCRIPT = File.dirname(File.expand_path(__FILE__))
APP_CFG_NOM_ARCHIVO = "#{APP_NOM_BASE}_tasks.rb"
APP_LOG_NOM_ARCHIVO = "logs/#{APP_NOM_BASE}.log"
APP_CFG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_CFG_NOM_ARCHIVO)
APP_LOG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_LOG_NOM_ARCHIVO)
# --- COLORES E ICONOS GLOBALES ---
RESET = "\e[0m"; NEGRITA = "\e[1m"; SUBRAYADO="\e[4m"; INTERMITENTE = "\e[5m"
CIAN = "\e[96m"; AMARILLO = "\e[93m"; ROJO = "\e[91m"; VERDE = "\e[92m"
ICONO_OK = "#{VERDE}#{RESET}"; ICONO_ERROR = "#{ROJO}#{RESET}"; ICONO_WARN = "#{AMARILLO}#{RESET}"; ICONO_INFO = "#{CIAN}#{RESET}"
# --- CONFIGURACIÓN GLOBAL ---
# Bloque para configurar el logger directamente aquí
require 'logger' # Asegúrate de que Logger esté cargado
$logger = Logger.new(APP_LOG_RUTA_ARCHIVO, 'daily')
$logger.level = Logger::INFO # Puedes ajustar el nivel de log aquí
$logger.formatter = proc do |severity, datetime, progname, msg|
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{msg}\n"
end
$estadisticas_ejecucion = { tareas_completadas: {} }
# --- FUNCIÓN DE LOGGING GLOBAL ---
def log(nivel, mensaje)
# Usa esta expresión regular más robusta para eliminar códigos ANSI
msg_l = mensaje.gsub(/\e\[(?:\d{1,3}(?:;\d{1,3})*)?[m|K]/, '')
case nivel
when :info then $logger.info(msg_l)
when :error then $logger.error(msg_l)
when :warn then $logger.warn(msg_l)
else $logger.debug(msg_l)
end
end
# Función auxiliar para verificar si un comando existe
def which(cmd)
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
exts.each { |ext|
exe = File.join(path, "#{cmd}#{ext}")
return exe if File.executable?(exe) && !File.directory?(exe)
}
end
nil
end
# ==========================================================
# (Bucle Principal)
# ==========================================================
# Mapea los tipos de proceso a sus funciones correspondientes.
PROCESSOR_MAP = {
xva: ->(task_cfg) { procesar_backups_xva(task_cfg) },
rclone_sync: ->(task_cfg) { sincronizar_rclone(task_cfg) },
proxmox_log_notes: ->(task_cfg) { procesar_backups_tipo_proxmox(task_cfg) }
}
def ejecutar_tarea_individual(tarea_cfg)
processor = PROCESSOR_MAP[tarea_cfg[:tipo_proceso]]
if processor
processor.call(tarea_cfg)
else
puts "#{ICONO_ERROR} Tipo de proceso desconocido: #{tarea_cfg[:tipo_proceso]}"
log :error, "Tipo de proceso desconocido para tarea #{tarea_cfg[:id]}: #{tarea_cfg[:tipo_proceso]}"
end
end
def ejecutar_comando(comando_cfg, todas_las_tareas)
system 'clear' or system 'cls'
puts "\n#{NEGRITA}#{CIAN}--- Ejecutando Comando: #{comando_cfg[:menu_texto]} ---#{RESET}"
log :info, "Iniciando ejecución del comando: #{comando_cfg[:id]}"
comando_cfg[:tareas].each_with_index do |id_tarea, index|
tarea_actual = todas_las_tareas.find { |t| t[:id] == id_tarea }
if tarea_actual
puts "\n#{AMARILLO}--- Paso #{index + 1}/#{comando_cfg[:tareas].length}: #{tarea_actual[:menu_texto]} ---#{RESET}"
log :info, "Ejecutando tarea '#{id_tarea}' dentro del comando '#{comando_cfg[:id]}'."
ejecutar_tarea_individual(tarea_actual)
# Aquí puedes añadir lógica para pausar o continuar automáticamente
# if !AUTO_AVANZAR_TAREA_MENU # O una configuración específica para comandos
# puts "\nPresiona Enter para continuar con la siguiente tarea del comando..."
# $stdin.gets
# end
else
puts "#{ICONO_ERROR} Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'. Saltando."
log :error, "Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'."
end
end
puts "\n#{ICONO_OK} Comando '#{comando_cfg[:menu_texto]}' completado."
log :info, "Comando '#{comando_cfg[:id]}' completado."
sleep 2
end
def bucle_principal(tareas_config, comandos_config, auto_avanzar)
loop do
opciones_menu = []
# Añadir tareas individuales al menú
tareas_config.each_with_index do |tarea_cfg, index|
opciones_menu << {
id: "T#{index + 1}",
texto: tarea_cfg[:menu_texto],
tipo: :tarea,
config: tarea_cfg
}
end
# Añadir comandos al menú
comandos_config.each_with_index do |comando_cfg, index|
opciones_menu << {
id: "C#{index + 1}",
texto: comando_cfg[:menu_texto],
tipo: :comando,
config: comando_cfg
}
end
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
system 'clear' or system 'cls'
mostrar_encabezado_app
puts "\n#{CIAN}--- Menú Principal ---#{RESET}"
puts "\n#{NEGRITA}Tareas Individuales: #{RESET}"
opciones_menu.select { |op| op[:tipo] == :tarea }.each do |op|
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
end
puts "\n#{NEGRITA}Comandos (Secuencias de Tareas): #{RESET}"
opciones_menu.select { |op| op[:tipo] == :comando }.each do |op|
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
end
puts "\n#{CIAN}--------------------#{RESET}"
# Opciones especiales al final
opciones_menu.select { |op| op[:tipo] == :especial }.each do |op|
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
end
puts "#{CIAN}--------------------#{RESET}"
opcion_usuario = leer_entrada_simple("Selecciona una opción > ").strip.upcase
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
if opcion_encontrada
case opcion_encontrada[:tipo]
when :tarea
ejecutar_tarea_individual(opcion_encontrada[:config])
when :comando
ejecutar_comando(opcion_encontrada[:config], tareas_config)
when :especial
case opcion_encontrada[:id]
when 'E'
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
# Recargar configuración después de editar
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
when 'S'
puts "Saliendo..."; exit 0
end
end
# Pausar si no es una opción de edición/salir
unless ['E', 'S'].include?(opcion_encontrada[:id].to_s) || auto_avanzar
puts "\nPresiona Enter para continuar..."
$stdin.gets
end
else
puts "#{ICONO_ERROR} Opción no válida."
sleep 1
end
end
end
# --- Inicio del Script ---
Signal.trap("INT") { puts "\n#{ROJO}Ctrl+C detectado. Saliendo...#{RESET}"; log :warn, "Script interrumpido (Ctrl+C)."; exit 130; }
log :info, "===== Inicio script #{APP_NOM} #{APP_VER} ====="
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
opciones_menu = []
# Añadir tareas individuales al menú
tareas_config.each_with_index do |tarea_cfg, index|
opciones_menu << {
id: "T#{index + 1}",
texto: tarea_cfg[:menu_texto],
tipo: :tarea,
config: tarea_cfg
}
end
# Añadir comandos al menú
comandos_config.each_with_index do |comando_cfg, index|
opciones_menu << {
id: "C#{index + 1}",
texto: comando_cfg[:menu_texto],
tipo: :comando,
config: comando_cfg
}
end
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
if ARGV[0]
opcion_usuario = ARGV[0].strip.upcase
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
if opcion_encontrada
case opcion_encontrada[:tipo]
when :tarea
ejecutar_tarea_individual(opcion_encontrada[:config])
when :comando
ejecutar_comando(opcion_encontrada[:config], tareas_config)
when :especial
case opcion_encontrada[:id]
when 'E'
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
# Recargar configuración después de editar
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
when 'S'
puts "Saliendo..."; exit 0
end
end
log :info, "Ejecutado automáticamente desde línea de comandos: #{opcion_usuario}"
exit 0
else
puts "#{ICONO_ERROR} Opción no válida: #{opcion_usuario}"
log :error, "Opción no válida desde línea de comandos: #{opcion_usuario}"
exit 1
end
else
bucle_principal(tareas_config, comandos_config, auto_avanzar)
end