248 lines
11 KiB
Ruby
248 lines
11 KiB
Ruby
module ADN
|
|
module SubcomandoDBMisc
|
|
# Comando: salud:bd - Verificar conexión y estado de la base de datos
|
|
def comando_salud_bd
|
|
puts "#{Color::CYAN}🔍 Verificando salud de la base de datos dtic-BITACORAs...#{Color::RESET}"
|
|
puts ""
|
|
|
|
begin
|
|
BitacorasDB::BitacoraDB.with_connection(env: :development) do |db|
|
|
salud = db.health_check
|
|
|
|
case salud[:status]
|
|
when 'healthy'
|
|
puts "#{Color::GREEN}✅ Conexión a PostgreSQL establecida#{Color::RESET}"
|
|
puts ""
|
|
puts " #{Color::BOLD}Versión PostgreSQL:#{Color::RESET} #{salud[:postgres_version].split(',')[0]}"
|
|
puts " #{Color::BOLD}Bitácoras en BD:#{Color::RESET} #{salud[:bitacoras_count]}"
|
|
puts " #{Color::BOLD}Servidor:#{Color::RESET} #{salud[:connection][:host]}:#{salud[:connection][:port]}"
|
|
puts " #{Color::BOLD}Base de datos:#{Color::RESET} #{salud[:connection][:database]}"
|
|
puts " #{Color::BOLD}Timestamp:#{Color::RESET} #{salud[:timestamp]}"
|
|
when 'disconnected'
|
|
puts "#{Color::RED}❌ No hay conexión activa a la base de datos#{Color::RESET}"
|
|
puts " #{Color::DIM}#{salud[:message]}#{Color::RESET}"
|
|
when 'unhealthy'
|
|
puts "#{Color::YELLOW}⚠ Problema de salud en la base de datos#{Color::RESET}"
|
|
puts " #{Color::DIM}#{salud[:message]}#{Color::RESET}"
|
|
end
|
|
end
|
|
|
|
# Verificar contenedor Docker
|
|
puts ""
|
|
puts "#{Color::CYAN}🔍 Verificando contenedor Docker...#{Color::RESET}"
|
|
if system("docker ps --filter name=postgres --format '{{.Names}}' | grep -q postgres")
|
|
puts "#{Color::GREEN}✅ Contenedor PostgreSQL en ejecución#{Color::RESET}"
|
|
else
|
|
puts "#{Color::YELLOW}⚠ Contenedor PostgreSQL no encontrado#{Color::RESET}"
|
|
puts " Ejecuta: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
|
|
end
|
|
|
|
rescue PG::ConnectionBad => e
|
|
puts "#{Color::RED}❌ Error de conexión a PostgreSQL#{Color::RESET}"
|
|
puts " #{e.message}"
|
|
puts ""
|
|
puts "#{Color::YELLOW}Solución:#{Color::RESET}"
|
|
puts " 1. Verifica que el contenedor esté en ejecución:"
|
|
puts " #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
|
|
puts " 2. Espera 10-20 segundos para que PostgreSQL inicie"
|
|
puts " 3. Verifica credenciales en tools/config/database.yml"
|
|
rescue => e
|
|
puts "#{Color::RED}❌ Error inesperado#{Color::RESET}"
|
|
puts " #{e.message}"
|
|
end
|
|
end
|
|
|
|
def comando_estadisticas
|
|
puts "#{Color::CYAN}📊 Estadísticas del sistema dtic-BITACORAs#{Color::RESET}"
|
|
|
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
stats = db.statistics
|
|
|
|
puts ""
|
|
puts "#{Color::BOLD}📈 Conteos de tablas:#{Color::RESET}"
|
|
puts " #{Color::DIM}Nodos:#{Color::RESET} #{stats[:nodos] || 0}"
|
|
puts " #{Color::DIM}Bitácoras:#{Color::RESET} #{stats[:bitacoras] || 0}"
|
|
puts " #{Color::DIM}Temas:#{Color::RESET} #{stats[:temas] || 0}"
|
|
puts " #{Color::DIM}Entradas:#{Color::RESET} #{stats[:entradas] || 0}"
|
|
|
|
if stats[:nodos_con_entradas]
|
|
puts ""
|
|
puts "#{Color::BOLD}📊 Estadísticas de actividad:#{Color::RESET}"
|
|
puts " #{Color::DIM}Nodos con entradas:#{Color::RESET} #{stats[:nodos_con_entradas]}"
|
|
puts " #{Color::DIM}Días con actividad:#{Color::RESET} #{stats[:dias_con_actividad]}"
|
|
puts " #{Color::DIM}Entradas generadas por IA:#{Color::RESET} #{stats[:entradas_ia]}"
|
|
puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{stats[:entradas_completadas]}"
|
|
end
|
|
|
|
# Calcular porcentajes
|
|
total_entradas = stats[:entradas] || 0
|
|
if total_entradas > 0
|
|
porcentaje_ia = (stats[:entradas_ia] || 0) * 100 / total_entradas
|
|
porcentaje_completadas = (stats[:entradas_completadas] || 0) * 100 / total_entradas
|
|
|
|
puts ""
|
|
puts "#{Color::BOLD}📈 Porcentajes:#{Color::RESET}"
|
|
puts " #{Color::DIM}Entradas IA:#{Color::RESET} #{porcentaje_ia}%"
|
|
puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{porcentaje_completadas}%"
|
|
end
|
|
end
|
|
|
|
def comando_backup(args)
|
|
opts = { output: nil, compress: true, timestamp: true }
|
|
|
|
parser = OptionParser.new do |opt|
|
|
opt.banner = "Uso: adn db backup [OPCIONES]"
|
|
opt.on("--output FILE", "Archivo de salida (default: backup_YYYYMMDD_HHMMSS.sql)") do |file|
|
|
opts[:output] = file
|
|
end
|
|
opt.on("--no-compress", "No comprimir el archivo de backup") do
|
|
opts[:compress] = false
|
|
end
|
|
opt.on("--no-timestamp", "No agregar timestamp al nombre del archivo") do
|
|
opts[:timestamp] = false
|
|
end
|
|
opt.on("-h", "--help", "Mostrar ayuda") do
|
|
puts opt
|
|
exit
|
|
end
|
|
end
|
|
|
|
begin
|
|
parser.parse!(args)
|
|
rescue OptionParser::InvalidOption => e
|
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
puts parser
|
|
exit 1
|
|
end
|
|
|
|
puts "#{Color::CYAN}💾 Creando backup de la base de datos dtic-BITACORAs...#{Color::RESET}"
|
|
puts " #{Color::DIM}Compresión: #{opts[:compress]}, Timestamp: #{opts[:timestamp]}#{Color::RESET}"
|
|
|
|
# Cargar configuración de base de datos
|
|
config_path = File.join(__dir__, '..', '..', 'config', 'database.yml')
|
|
unless File.exist?(config_path)
|
|
puts "#{Color::RED}✗ Archivo de configuración no encontrado: #{config_path}#{Color::RESET}"
|
|
exit 1
|
|
end
|
|
|
|
begin
|
|
config = YAML.load_file(config_path)
|
|
db_config = config['development'] || config.first[1] # Tomar primer entorno si no hay development
|
|
|
|
host = db_config['host'] || 'localhost'
|
|
port = db_config['port'] || 5433
|
|
database = db_config['database']
|
|
username = db_config['username']
|
|
password = db_config['password']
|
|
|
|
unless database && username
|
|
puts "#{Color::RED}✗ Configuración de base de datos incompleta#{Color::RESET}"
|
|
exit 1
|
|
end
|
|
|
|
# Generar nombre de archivo
|
|
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
base_name = "dtic_bitacoras_backup_#{timestamp}"
|
|
sql_file = opts[:output] || "#{base_name}.sql"
|
|
final_file = opts[:compress] ? "#{sql_file}.gz" : sql_file
|
|
|
|
puts " #{Color::DIM}Archivo de salida: #{final_file}#{Color::RESET}"
|
|
|
|
# Construir comando pg_dump
|
|
dump_cmd = "PGPASSWORD=\"#{password}\" pg_dump"
|
|
dump_cmd += " -h #{host} -p #{port} -U #{username} -d #{database}"
|
|
dump_cmd += " --schema=bitacoras" # Solo esquema bitacoras
|
|
dump_cmd += " --clean --if-exists" # Incluir DROP statements
|
|
dump_cmd += " --no-owner --no-privileges" # No incluir permisos
|
|
dump_cmd += " --inserts" # Usar INSERT en lugar de COPY
|
|
|
|
if opts[:compress]
|
|
dump_cmd += " | gzip -c > \"#{final_file}\""
|
|
else
|
|
dump_cmd += " > \"#{final_file}\""
|
|
end
|
|
|
|
# Ejecutar backup
|
|
puts " #{Color::DIM}Ejecutando pg_dump...#{Color::RESET}"
|
|
system(dump_cmd)
|
|
|
|
if $?.success?
|
|
file_size = File.size(final_file) rescue 0
|
|
size_str = file_size > 0 ? " (#{file_size / 1024} KB)" : ""
|
|
puts "#{Color::GREEN}✅ Backup creado exitosamente: #{final_file}#{size_str}#{Color::RESET}"
|
|
puts " #{Color::DIM}Para restaurar:#{Color::RESET} gunzip -c #{final_file} | psql -h #{host} -p #{port} -U #{username} -d #{database}"
|
|
else
|
|
puts "#{Color::RED}✗ Error creando backup#{Color::RESET}"
|
|
# Limpiar archivo parcial si existe
|
|
File.delete(final_file) if File.exist?(final_file)
|
|
exit 1
|
|
end
|
|
|
|
rescue => e
|
|
puts "#{Color::RED}✗ Error durante backup: #{e.message}#{Color::RESET}"
|
|
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Comando: archivar:md - Mover bitácoras y proyectos MD importados/obsoletos a carpetas _hist
|
|
def comando_archivar_md
|
|
require 'fileutils'
|
|
puts "#{Color::CYAN}🧹 Iniciando limpieza y archivado de archivos Markdown...#{Color::RESET}"
|
|
|
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
bitacoras_dir = defined?(ADN::BITACORAS_DIR) ? ADN::BITACORAS_DIR : File.expand_path('../../../docs/bitacoras', __dir__)
|
|
proyectos_dir = File.expand_path('../../../docs/proyectos', __dir__)
|
|
|
|
hist_bitacoras = File.join(bitacoras_dir, '_hist')
|
|
hist_proyectos = File.join(proyectos_dir, '_hist')
|
|
|
|
FileUtils.mkdir_p(hist_bitacoras)
|
|
FileUtils.mkdir_p(hist_proyectos)
|
|
|
|
# 1. Archivar Bitácoras Diarias
|
|
puts "\\n#{Color::BOLD}>> Verificando Bitácoras Diarias...#{Color::RESET}"
|
|
bitacoras_db = db.send(:execute, "SELECT fecha FROM bitacoras.bitacoras").map { |row| row['fecha'] }
|
|
|
|
Dir.glob(File.join(bitacoras_dir, '*.md')).each do |md_path|
|
|
next if File.basename(md_path) == 'README.md'
|
|
fecha_md = File.basename(md_path, '.md')
|
|
|
|
if bitacoras_db.include?(fecha_md)
|
|
dest = File.join(hist_bitacoras, File.basename(md_path))
|
|
FileUtils.mv(md_path, dest)
|
|
puts " #{Color::GREEN}✓ Archivada:#{Color::RESET} #{fecha_md}.md"
|
|
else
|
|
puts " #{Color::YELLOW}⚠ No en DB (se omite):#{Color::RESET} #{fecha_md}.md"
|
|
end
|
|
end
|
|
|
|
# 2. Archivar Proyectos
|
|
puts "\\n#{Color::BOLD}>> Verificando Proyectos MD...#{Color::RESET}"
|
|
proyectos_db = db.send(:execute, "SELECT codigo FROM bitacoras.proyectos").map { |row| row['codigo'] }
|
|
|
|
Dir.glob(File.join(proyectos_dir, '*.md')).each do |md_path|
|
|
next if File.basename(md_path) == 'README.md'
|
|
# Extraer el código PXXXX del principio del archivo, asumiendo formato PXXXX_Titulo.md o PXXXX.md
|
|
codigo_match = File.basename(md_path).match(/^(P\d+)/)
|
|
|
|
if codigo_match && proyectos_db.include?(codigo_match[1])
|
|
dest = File.join(hist_proyectos, File.basename(md_path))
|
|
FileUtils.mv(md_path, dest)
|
|
puts " #{Color::GREEN}✓ Archivado Proyecto:#{Color::RESET} #{File.basename(md_path)}"
|
|
else
|
|
puts " #{Color::YELLOW}⚠ Proyecto no registrado en DB o formato no válido:#{Color::RESET} #{File.basename(md_path)}"
|
|
end
|
|
end
|
|
|
|
puts "\\n#{Color::GREEN}✅ Limpieza Markdown finalizada. Entorno 100% DB-First fortalecido.#{Color::RESET}"
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|