Refactor(ADN): Atomización del monolito db.rb moviendo utilidades a submódulos en la carpeta db/
This commit is contained in:
+10
-952
@@ -37,6 +37,16 @@ end
|
|||||||
|
|
||||||
module ADN
|
module ADN
|
||||||
class SubcomandoDB
|
class SubcomandoDB
|
||||||
|
require_relative 'db/bitacora'
|
||||||
|
require_relative 'db/evento'
|
||||||
|
require_relative 'db/nodo'
|
||||||
|
require_relative 'db/hito'
|
||||||
|
require_relative 'db/misc'
|
||||||
|
include SubcomandoDBBitacora
|
||||||
|
include SubcomandoDBEvento
|
||||||
|
include SubcomandoDBNodo
|
||||||
|
include SubcomandoDBHito
|
||||||
|
include SubcomandoDBMisc
|
||||||
# Inicializa el subcomando con argumentos y logger
|
# Inicializa el subcomando con argumentos y logger
|
||||||
#
|
#
|
||||||
# @param args [Array<String>] Argumentos de línea de comandos
|
# @param args [Array<String>] Argumentos de línea de comandos
|
||||||
@@ -111,958 +121,6 @@ module ADN
|
|||||||
|
|
||||||
# ─── COMANDOS PRINCIPALES ──────────────────────────────────────────────
|
# ─── COMANDOS PRINCIPALES ──────────────────────────────────────────────
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Comando: bitacora:crear [FECHA] - Crear nueva bitácora diaria
|
|
||||||
def comando_bitacora_crear(args)
|
|
||||||
opts = { fecha: Date.today, resumen: nil }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db bitacora:crear [FECHA] [OPCIONES]"
|
|
||||||
opt.on("--resumen TEXTO", "Resumen inicial de la bitácora") do |r|
|
|
||||||
opts[:resumen] = r
|
|
||||||
end
|
|
||||||
opt.on("-h", "--help", "Mostrar ayuda") do
|
|
||||||
puts opt
|
|
||||||
exit
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
parser.parse!(args)
|
|
||||||
# Si queda un argumento sin procesar, asumimos que es la fecha
|
|
||||||
opts[:fecha] = Date.parse(args[0]) if args[0]
|
|
||||||
rescue OptionParser::InvalidOption, Date::Error => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
puts parser
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}📝 Creando bitácora para #{opts[:fecha]}...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen])
|
|
||||||
puts "#{Color::GREEN}✅ Bitácora creada exitosamente#{Color::RESET}"
|
|
||||||
puts ""
|
|
||||||
puts " #{Color::BOLD}ID:#{Color::RESET} #{bitacora['id']}"
|
|
||||||
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{bitacora['fecha']}"
|
|
||||||
puts " #{Color::BOLD}Resumen:#{Color::RESET} #{bitacora['resumen'] || '(sin resumen)'}"
|
|
||||||
puts " #{Color::BOLD}Creada:#{Color::RESET} #{bitacora['created_at']}"
|
|
||||||
rescue PG::UniqueViolation
|
|
||||||
puts "#{Color::YELLOW}⚠ Ya existe una bitácora para la fecha #{opts[:fecha]}#{Color::RESET}"
|
|
||||||
puts " Usa #{Color::DIM}adn db bitacora:listar#{Color::RESET} para ver bitácoras existentes"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: bitacora:listar [OPCIONES] - Listar bitácoras existentes
|
|
||||||
def comando_bitacora_listar(args)
|
|
||||||
opts = { limit: 10, offset: 0, desde: nil, hasta: nil }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db bitacora:listar [OPCIONES]"
|
|
||||||
opt.on("--limit N", Integer, "Número máximo de resultados (default: 10)") do |n|
|
|
||||||
opts[:limit] = n
|
|
||||||
end
|
|
||||||
opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n|
|
|
||||||
opts[:offset] = n
|
|
||||||
end
|
|
||||||
opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d|
|
|
||||||
opts[:desde] = d
|
|
||||||
end
|
|
||||||
opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h|
|
|
||||||
opts[:hasta] = h
|
|
||||||
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}📋 Listando bitácoras...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
bitacoras = db.list_bitacoras(
|
|
||||||
limit: opts[:limit],
|
|
||||||
offset: opts[:offset],
|
|
||||||
desde: opts[:desde],
|
|
||||||
hasta: opts[:hasta]
|
|
||||||
)
|
|
||||||
|
|
||||||
if bitacoras.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se encontraron bitácoras#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Resumen".ljust(40)} #{"Creada".ljust(20)}#{Color::RESET}"
|
|
||||||
puts "#{"─" * 5} #{"─" * 12} #{"─" * 40} #{"─" * 20}"
|
|
||||||
|
|
||||||
bitacoras.each do |b|
|
|
||||||
fecha = b['fecha']
|
|
||||||
resumen = b['resumen'] || ''
|
|
||||||
resumen = resumen[0..37] + '...' if resumen.length > 40
|
|
||||||
creada = Time.parse(b['created_at']).strftime('%Y-%m-%d %H:%M')
|
|
||||||
|
|
||||||
puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} #{resumen.ljust(40)} #{creada.ljust(20)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "Total: #{bitacoras.length} bitácoras"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: evento:crear [OPCIONES] - Crear nueva entrada cronológica
|
|
||||||
def comando_evento_crear(args)
|
|
||||||
opts = {
|
|
||||||
inicio: nil,
|
|
||||||
descripcion: nil,
|
|
||||||
fin: nil,
|
|
||||||
estado: nil,
|
|
||||||
modo: nil,
|
|
||||||
es_ia: false,
|
|
||||||
nodo: nil,
|
|
||||||
tema: nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db evento:crear [OPCIONES]"
|
|
||||||
opt.on("--inicio HH:MM", "Hora de inicio (requerido)") do |i|
|
|
||||||
opts[:inicio] = i
|
|
||||||
end
|
|
||||||
opt.on("--descripcion TEXTO", "Descripción de la actividad (requerido)") do |d|
|
|
||||||
opts[:descripcion] = d
|
|
||||||
end
|
|
||||||
opt.on("--fin HH:MM", "Hora de fin (opcional)") do |f|
|
|
||||||
opts[:fin] = f
|
|
||||||
end
|
|
||||||
opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
|
|
||||||
opts[:estado] = e
|
|
||||||
end
|
|
||||||
opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") do |m|
|
|
||||||
opts[:modo] = m
|
|
||||||
end
|
|
||||||
opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n|
|
|
||||||
opts[:nodo] = n
|
|
||||||
end
|
|
||||||
opt.on("--ia", "Marcar como generado por IA") do
|
|
||||||
opts[:es_ia] = true
|
|
||||||
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
|
|
||||||
|
|
||||||
# Validaciones básicas
|
|
||||||
unless opts[:inicio] && opts[:descripcion]
|
|
||||||
puts "#{Color::RED}✗ Error: --inicio y --descripcion son requeridos#{Color::RESET}"
|
|
||||||
puts parser
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}📝 Creando nueva entrada...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
# Buscar nodo por nombre si se especificó
|
|
||||||
nodo_id = nil
|
|
||||||
if opts[:nodo]
|
|
||||||
nodo = db.find_nodo_by_nombre(opts[:nodo])
|
|
||||||
if nodo
|
|
||||||
nodo_id = nodo['id']
|
|
||||||
puts " #{Color::DIM}Nodo encontrado: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}"
|
|
||||||
else
|
|
||||||
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}"
|
|
||||||
puts " Creando entrada sin nodo asociado"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Buscar bitácora para hoy (o crearla)
|
|
||||||
fecha_hoy = Date.today.to_s
|
|
||||||
bitacora = db.find_bitacora_by_fecha(fecha_hoy)
|
|
||||||
|
|
||||||
if bitacora.nil?
|
|
||||||
puts " #{Color::DIM}Creando bitácora para hoy...#{Color::RESET}"
|
|
||||||
bitacora = db.create_bitacora(fecha: fecha_hoy)
|
|
||||||
end
|
|
||||||
|
|
||||||
bitacora_id = bitacora['id']
|
|
||||||
|
|
||||||
# Crear la entrada
|
|
||||||
entrada = db.create_entrada(
|
|
||||||
inicio: opts[:inicio],
|
|
||||||
descripcion: opts[:descripcion],
|
|
||||||
fin: opts[:fin],
|
|
||||||
estado: opts[:estado],
|
|
||||||
modo: opts[:modo],
|
|
||||||
es_ia: opts[:es_ia],
|
|
||||||
bitacora_id: bitacora_id,
|
|
||||||
nodo_id: nodo_id,
|
|
||||||
tema_id: nil # Por ahora sin tema
|
|
||||||
)
|
|
||||||
|
|
||||||
puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}"
|
|
||||||
puts ""
|
|
||||||
puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}"
|
|
||||||
puts " #{Color::BOLD}Inicio:#{Color::RESET} #{entrada['inicio']}"
|
|
||||||
puts " #{Color::BOLD}Fin:#{Color::RESET} #{entrada['fin'] || '(sin fin)'}"
|
|
||||||
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{entrada['descripcion']}"
|
|
||||||
puts " #{Color::BOLD}Estado:#{Color::RESET} #{entrada['estado']}"
|
|
||||||
puts " #{Color::BOLD}Modo:#{Color::RESET} #{entrada['modo'] == 'P' ? 'Presencial' : 'Remoto'}"
|
|
||||||
puts " #{Color::BOLD}IA:#{Color::RESET} #{entrada['es_ia'] ? 'Sí' : 'No'}"
|
|
||||||
puts " #{Color::BOLD}Bitácora:#{Color::RESET} #{fecha_hoy} (ID: #{bitacora_id})"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: evento:actualizar <ID> [OPCIONES] - Actualizar una entrada existente
|
|
||||||
def comando_evento_actualizar(args)
|
|
||||||
if args.length < 1 || args[0].start_with?('-')
|
|
||||||
puts "#{Color::YELLOW}Uso: adn db evento:actualizar <ID> [OPCIONES]#{Color::RESET}"
|
|
||||||
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"✅\", --descripcion \"Texto\""
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
id_evento = args.shift.to_i
|
|
||||||
opts = {}
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Opciones:"
|
|
||||||
opt.on("--inicio HH:MM", "Nueva hora de inicio") { |i| opts[:inicio] = i }
|
|
||||||
opt.on("--fin HH:MM", "Nueva hora de fin") { |f| opts[:fin] = f }
|
|
||||||
opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d }
|
|
||||||
opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e }
|
|
||||||
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}"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion o --estado#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}🔄 Actualizando evento #{id_evento}...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
res = db.update_entrada(id_evento, opts)
|
|
||||||
if res
|
|
||||||
puts "#{Color::GREEN}✅ Evento #{id_evento} actualizado exitosamente.#{Color::RESET}"
|
|
||||||
puts " #{Color::BOLD}Inicio:#{Color::RESET} #{res['inicio']}"
|
|
||||||
puts " #{Color::BOLD}Fin:#{Color::RESET} #{res['fin'] || '(sin fin)'}"
|
|
||||||
else
|
|
||||||
puts "#{Color::YELLOW}⚠ No se pudo actualizar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}"
|
|
||||||
end
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: evento:listar [OPCIONES] - Listar entradas con filtros
|
|
||||||
def comando_evento_listar(args)
|
|
||||||
opts = { limit: 20, offset: 0, nodo: nil, estado: nil, buscar: nil, desde: nil, hasta: nil }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db evento:listar [OPCIONES]"
|
|
||||||
opt.on("--limit N", Integer, "Número máximo de resultados (default: 20)") do |n|
|
|
||||||
opts[:limit] = n
|
|
||||||
end
|
|
||||||
opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n|
|
|
||||||
opts[:offset] = n
|
|
||||||
end
|
|
||||||
opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n|
|
|
||||||
opts[:nodo] = n
|
|
||||||
end
|
|
||||||
opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
|
|
||||||
opts[:estado] = e
|
|
||||||
end
|
|
||||||
opt.on("--buscar TEXTO", "Buscar texto en descripción") do |b|
|
|
||||||
opts[:buscar] = b
|
|
||||||
end
|
|
||||||
opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d|
|
|
||||||
opts[:desde] = d
|
|
||||||
end
|
|
||||||
opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h|
|
|
||||||
opts[:hasta] = h
|
|
||||||
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}📋 Listando entradas...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
# Obtener ID de nodo si se especificó nombre
|
|
||||||
nodo_id = nil
|
|
||||||
if opts[:nodo]
|
|
||||||
nodo = db.find_nodo_by_nombre(opts[:nodo])
|
|
||||||
if nodo
|
|
||||||
nodo_id = nodo['id']
|
|
||||||
puts " #{Color::DIM}Filtrando por nodo: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}"
|
|
||||||
else
|
|
||||||
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
entradas = db.list_entradas(
|
|
||||||
nodo_id: nodo_id,
|
|
||||||
estado: opts[:estado],
|
|
||||||
buscar: opts[:buscar],
|
|
||||||
desde: opts[:desde],
|
|
||||||
hasta: opts[:hasta],
|
|
||||||
limit: opts[:limit],
|
|
||||||
offset: opts[:offset]
|
|
||||||
)
|
|
||||||
|
|
||||||
if entradas.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se encontraron entradas#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Fin".ljust(8)} #{"Nodo".ljust(15)} #{"Estado".ljust(3)} #{"Descripción".ljust(40)}#{Color::RESET}"
|
|
||||||
puts "#{"─" * 5} #{"─" * 8} #{"─" * 8} #{"─" * 15} #{"─" * 3} #{"─" * 40}"
|
|
||||||
|
|
||||||
entradas.each do |e|
|
|
||||||
id = e['id'].to_s
|
|
||||||
inicio = e['inicio'][0..7] # HH:MM:SS -> HH:MM
|
|
||||||
fin = e['fin'] ? e['fin'][0..7] : '--:--'
|
|
||||||
nodo = e['nodo_nombre'] || '(sin nodo)'
|
|
||||||
nodo = nodo[0..13] + '...' if nodo.length > 15
|
|
||||||
estado = e['estado']
|
|
||||||
desc = e['descripcion']
|
|
||||||
desc = desc[0..37] + '...' if desc.length > 40
|
|
||||||
|
|
||||||
puts "#{id.ljust(5)} #{inicio.ljust(8)} #{fin.ljust(8)} #{nodo.ljust(15)} #{estado.ljust(3)} #{desc.ljust(40)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "Total: #{entradas.length} entradas"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: nodo:crear [OPCIONES] - Crear nuevo nodo
|
|
||||||
def comando_nodo_crear(args)
|
|
||||||
opts = { nombre: nil, ip: nil, tipo: 'servidor', descripcion: nil, activo: true }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db nodo:crear [OPCIONES]"
|
|
||||||
opt.on("--nombre NOMBRE", "Nombre único del nodo (requerido)") do |n|
|
|
||||||
opts[:nombre] = n
|
|
||||||
end
|
|
||||||
opt.on("--ip DIRECCION", "Dirección IP (opcional)") do |i|
|
|
||||||
opts[:ip] = i
|
|
||||||
end
|
|
||||||
opt.on("--tipo TIPO", "Tipo (servidor, hipervisor, vm, pc)") do |t|
|
|
||||||
opts[:tipo] = t
|
|
||||||
end
|
|
||||||
opt.on("--descripcion TEXTO", "Descripción del nodo") do |d|
|
|
||||||
opts[:descripcion] = d
|
|
||||||
end
|
|
||||||
opt.on("--inactivo", "Crear nodo como inactivo") do
|
|
||||||
opts[:activo] = 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
|
|
||||||
|
|
||||||
unless opts[:nombre]
|
|
||||||
puts "#{Color::RED}✗ Error: --nombre es requerido#{Color::RESET}"
|
|
||||||
puts parser
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}🖥️ Creando nodo '#{opts[:nombre]}'...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
nodo = db.create_nodo(
|
|
||||||
nombre: opts[:nombre],
|
|
||||||
ip: opts[:ip],
|
|
||||||
tipo: opts[:tipo],
|
|
||||||
descripcion: opts[:descripcion],
|
|
||||||
activo: opts[:activo]
|
|
||||||
)
|
|
||||||
|
|
||||||
puts "#{Color::GREEN}✅ Nodo creado exitosamente#{Color::RESET}"
|
|
||||||
puts ""
|
|
||||||
puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}"
|
|
||||||
puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}"
|
|
||||||
puts " #{Color::BOLD}IP:#{Color::RESET} #{nodo['ip'] || '(sin IP)'}"
|
|
||||||
puts " #{Color::BOLD}Tipo:#{Color::RESET} #{nodo['tipo']}"
|
|
||||||
puts " #{Color::BOLD}Activo:#{Color::RESET} #{nodo['activo'] ? 'Sí' : 'No'}"
|
|
||||||
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{nodo['descripcion'] || '(sin descripción)'}"
|
|
||||||
puts " #{Color::BOLD}Creado:#{Color::RESET} #{nodo['created_at']}"
|
|
||||||
rescue PG::UniqueViolation
|
|
||||||
puts "#{Color::YELLOW}⚠ Ya existe un nodo con el nombre '#{opts[:nombre]}'#{Color::RESET}"
|
|
||||||
puts " Usa #{Color::DIM}adn db nodo:listar#{Color::RESET} para ver nodos existentes"
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error creando nodo: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: nodo:listar [OPCIONES] - Listar nodos del sistema
|
|
||||||
def comando_nodo_listar(args)
|
|
||||||
opts = { activo: nil, tipo: nil }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Uso: adn db nodo:listar [OPCIONES]"
|
|
||||||
opt.on("--activo", "Mostrar solo nodos activos") do
|
|
||||||
opts[:activo] = true
|
|
||||||
end
|
|
||||||
opt.on("--inactivo", "Mostrar solo nodos inactivos") do
|
|
||||||
opts[:activo] = false
|
|
||||||
end
|
|
||||||
opt.on("--tipo TIPO", "Filtrar por tipo (servidor, hipervisor, vm, pc)") do |t|
|
|
||||||
opts[:tipo] = t
|
|
||||||
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}🖥️ Listando nodos...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], limit: 100)
|
|
||||||
|
|
||||||
if nodos.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}"
|
|
||||||
puts "#{"─" * 5} #{"─" * 20} #{"─" * 15} #{"─" * 12} #{"─" * 6} #{"─" * 30}"
|
|
||||||
|
|
||||||
nodos.each do |n|
|
|
||||||
id = n['id'].to_s
|
|
||||||
nombre = n['nombre']
|
|
||||||
ip = n['ip'] || '(sin IP)'
|
|
||||||
tipo = n['tipo']
|
|
||||||
activo = n['activo'] ? '✅' : '❌'
|
|
||||||
desc = n['descripcion'] || ''
|
|
||||||
desc = desc[0..27] + '...' if desc.length > 30
|
|
||||||
|
|
||||||
puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "Total: #{nodos.length} nodos"
|
|
||||||
|
|
||||||
# Estadísticas por tipo
|
|
||||||
tipos = nodos.group_by { |n| n['tipo'] }
|
|
||||||
puts "#{Color::DIM}Tipos: #{tipos.map { |t, ns| "#{t}: #{ns.length}" }.join(', ')}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: hito:listar [PROYECTO]
|
|
||||||
def comando_hito_listar(args)
|
|
||||||
if args.empty?
|
|
||||||
puts "#{Color::YELLOW}Uso: adn db hito:listar <CODIGO_PROYECTO> (ej: P2605)#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
codigo = args[0]
|
|
||||||
puts "#{Color::CYAN}📋 Listando hitos del proyecto #{codigo.upcase}...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
hitos = db.list_hitos(codigo)
|
|
||||||
if hitos.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se encontraron hitos o el proyecto no existe.#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts ""
|
|
||||||
puts "#{Color::BOLD}#{"Hito".ljust(8)} #{"Fase".ljust(6)} #{"Estado".ljust(8)} #{"Hrs P/R".ljust(10)} #{"Título"}#{Color::RESET}"
|
|
||||||
puts "#{"─" * 8} #{"─" * 6} #{"─" * 8} #{"─" * 10} #{"─" * 40}"
|
|
||||||
|
|
||||||
hitos.each do |h|
|
|
||||||
hrs = "#{h['horas_presencial']}/#{h['horas_remoto']}"
|
|
||||||
puts "#{h['id_hito'].ljust(8)} #{h['fase_num'].to_s.ljust(6)} #{h['estado'].ljust(8)} #{hrs.ljust(10)} #{h['titulo']}"
|
|
||||||
end
|
|
||||||
puts "\nTotal: #{hitos.size} hitos"
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: hito:ordenar <PROYECTO> [--aplicar FILE]
|
|
||||||
def comando_hito_ordenar(args)
|
|
||||||
codigo = args.shift
|
|
||||||
if !codigo
|
|
||||||
puts "#{Color::YELLOW}Uso: adn db hito:ordenar <PROYECTO> [--aplicar /ruta/al/archivo.txt]#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
aplicar_path = nil
|
|
||||||
if args[0] == '--aplicar' && args[1]
|
|
||||||
aplicar_path = args[1]
|
|
||||||
end
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
hitos = db.list_hitos(codigo)
|
|
||||||
if hitos.empty?
|
|
||||||
puts "#{Color::YELLOW}⚠ No se encontraron hitos para #{codigo.upcase}.#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if aplicar_path
|
|
||||||
unless File.exist?(aplicar_path)
|
|
||||||
puts "#{Color::RED}✗ No se encontró el archivo: #{aplicar_path}#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Procesar el archivo y aplicarrenombramientos lógicos "Fase.Grupo.Paso"
|
|
||||||
puts "#{Color::CYAN}📋 Aplicando ordenamiento desde #{aplicar_path}...#{Color::RESET}"
|
|
||||||
lineas = File.readlines(aplicar_path).map(&:strip).reject { |l| l.empty? || l.start_with?('#') }
|
|
||||||
|
|
||||||
# Agrupar por fase según el orden de las líneas
|
|
||||||
hitos_por_fase = Hash.new { |h, k| h[k] = [] }
|
|
||||||
lineas.each do |linea|
|
|
||||||
if linea =~ /^(\d+):\s*\[.+?\]\s*(.+)$/
|
|
||||||
id_db = $1.to_i
|
|
||||||
hito = hitos.find { |h| h['id'] == id_db }
|
|
||||||
hitos_por_fase[hito['fase_num']] << hito if hito
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Iniciar renombrado secuencial
|
|
||||||
hitos_por_fase.each do |fase, hitos_fase|
|
|
||||||
grupo = 1
|
|
||||||
letra = 65 # 'A'
|
|
||||||
|
|
||||||
hitos_fase.each do |h|
|
|
||||||
nuevo_id = "#{fase}.#{grupo}.#{letra.chr}"
|
|
||||||
|
|
||||||
if h['id_hito'] != nuevo_id
|
|
||||||
db.rename_hito_id(codigo, h['id_hito'], nuevo_id)
|
|
||||||
puts " #{Color::GREEN}✔ #{h['id_hito'].ljust(8)} → #{nuevo_id.ljust(8)}#{Color::RESET} | #{h['titulo']}"
|
|
||||||
else
|
|
||||||
puts " #{Color::DIM}• #{nuevo_id.ljust(8)} (Sin cambios) | #{h['titulo']}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
|
|
||||||
letra += 1
|
|
||||||
if letra > 67 # 'C'
|
|
||||||
letra = 65
|
|
||||||
grupo += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
puts "#{Color::GREEN}✅ Ordenamiento aplicado exitosamente a #{codigo.upcase}.#{Color::RESET}"
|
|
||||||
|
|
||||||
else
|
|
||||||
# Generar archivo temporal para reordenamiento (Swarm-Ready)
|
|
||||||
tmp_file = "/tmp/adn_ordenar_#{codigo.downcase}.txt"
|
|
||||||
File.open(tmp_file, 'w') do |f|
|
|
||||||
f.puts "# ==========================================================================="
|
|
||||||
f.puts "# MODO ORDENAMIENTO DE HITOS - PROYECTO #{codigo.upcase}"
|
|
||||||
f.puts "# ==========================================================================="
|
|
||||||
f.puts "# Cambia el orden de las líneas (Cortar y Pegar) dentro de la misma FASE"
|
|
||||||
f.puts "# para definir la secuencia lógica de ejecución."
|
|
||||||
f.puts "# NO MODIFIQUES el número inicial ni los corchetes (ej: '42: [F3]')."
|
|
||||||
f.puts "# Cuando termines de editar, guarda el archivo y ejecuta en la terminal:"
|
|
||||||
f.puts "# adn db hito:ordenar #{codigo.upcase} --aplicar #{tmp_file}"
|
|
||||||
f.puts "# ===========================================================================\n\n"
|
|
||||||
|
|
||||||
fases = hitos.group_by { |h| h['fase_num'] }.sort_by { |k, v| k }
|
|
||||||
fases.each do |num_fase, hitos_fase|
|
|
||||||
f.puts "# --- FASE #{num_fase} ---"
|
|
||||||
hitos_fase.each do |h|
|
|
||||||
f.puts "#{h['id']}: [F#{num_fase}] #{h['titulo']}"
|
|
||||||
end
|
|
||||||
f.puts "\n"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}✅ Archivo de configuración generado en: #{tmp_file}#{Color::RESET}"
|
|
||||||
puts "1. Abre el archivo y reordena las líneas como necesites."
|
|
||||||
puts "2. Ejecuta: #{Color::BOLD}adn db hito:ordenar #{codigo.upcase} --aplicar #{tmp_file}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: hito:renombrar <PROYECTO> <ID_HITO_ACTUAL> <NUEVO_ID_HITO>
|
|
||||||
def comando_hito_renombrar(args)
|
|
||||||
if args.length < 3
|
|
||||||
puts "#{Color::YELLOW}Uso: adn db hito:renombrar <PROYECTO> <ID_HITO_ACTUAL> <NUEVO_ID_HITO>#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
proyecto = args[0]
|
|
||||||
id_actual = args[1]
|
|
||||||
id_nuevo = args[2]
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}🔄 Renombrando hito #{id_actual} → #{id_nuevo} (Proyecto #{proyecto.upcase})...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
res = db.rename_hito_id(proyecto, id_actual, id_nuevo)
|
|
||||||
puts "#{Color::GREEN}✅ Hito renombrado exitosamente. Título: #{res['titulo']}#{Color::RESET}"
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comando: hito:actualizar <PROYECTO> <ID_HITO> [OPCIONES]
|
|
||||||
def comando_hito_actualizar(args)
|
|
||||||
if args.length < 2
|
|
||||||
puts "#{Color::YELLOW}Uso: adn db hito:actualizar <PROYECTO> <ID_HITO> [OPCIONES]#{Color::RESET}"
|
|
||||||
puts "Opciones soportadas: --estado=EJ, --horas-p=1.5, --horas-r=0, --desc=\"Texto\""
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
proyecto = args.shift
|
|
||||||
id_hito = args.shift
|
|
||||||
opts = { estado: nil, horas_presencial: nil, horas_remoto: nil, descripcion: nil }
|
|
||||||
|
|
||||||
parser = OptionParser.new do |opt|
|
|
||||||
opt.banner = "Opciones:"
|
|
||||||
opt.on("--estado ESTADO", "Nuevo estado (ej: ✅, ⏳, 📋)") { |e| opts[:estado] = e }
|
|
||||||
opt.on("--horas-p HORAS", "Horas presenciales") { |h| opts[:horas_presencial] = h.to_f }
|
|
||||||
opt.on("--horas-r HORAS", "Horas remotas") { |h| opts[:horas_remoto] = h.to_f }
|
|
||||||
opt.on("--desc DESC", "Descripción o detalle para el hito") { |d| opts[:descripcion] = d }
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
parser.parse!(args)
|
|
||||||
rescue OptionParser::InvalidOption => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts.values.all?(&:nil?)
|
|
||||||
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --estado, --horas-p, --horas-r o --desc#{Color::RESET}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{Color::CYAN}🔄 Actualizando hito #{id_hito} del proyecto #{proyecto.upcase}...#{Color::RESET}"
|
|
||||||
|
|
||||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
||||||
begin
|
|
||||||
res = db.update_hito(
|
|
||||||
proyecto, id_hito,
|
|
||||||
estado: opts[:estado],
|
|
||||||
horas_presencial: opts[:horas_presencial],
|
|
||||||
horas_remoto: opts[:horas_remoto],
|
|
||||||
descripcion: opts[:descripcion]
|
|
||||||
)
|
|
||||||
puts "#{Color::GREEN}✅ Hito actualizado exitosamente.#{Color::RESET}"
|
|
||||||
hrs = "#{res['horas_presencial'].to_f}P / #{res['horas_remoto'].to_f}R"
|
|
||||||
puts " Estado actual : #{res['estado']}"
|
|
||||||
puts " Horas imputadas : #{hrs}"
|
|
||||||
rescue => e
|
|
||||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
|
||||||
end
|
|
||||||
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
|
|
||||||
|
|
||||||
# ─── MÉTODOS AUXILIARES ──────────────────────────────────────────────
|
# ─── MÉTODOS AUXILIARES ──────────────────────────────────────────────
|
||||||
|
|
||||||
# Carga la configuración desde config.yml
|
# Carga la configuración desde config.yml
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
module ADN
|
||||||
|
module SubcomandoDBBitacora
|
||||||
|
# Comando: bitacora:crear [FECHA] - Crear nueva bitácora diaria
|
||||||
|
def comando_bitacora_crear(args)
|
||||||
|
opts = { fecha: Date.today, resumen: nil }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db bitacora:crear [FECHA] [OPCIONES]"
|
||||||
|
opt.on("--resumen TEXTO", "Resumen inicial de la bitácora") do |r|
|
||||||
|
opts[:resumen] = r
|
||||||
|
end
|
||||||
|
opt.on("-h", "--help", "Mostrar ayuda") do
|
||||||
|
puts opt
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
parser.parse!(args)
|
||||||
|
# Si queda un argumento sin procesar, asumimos que es la fecha
|
||||||
|
opts[:fecha] = Date.parse(args[0]) if args[0]
|
||||||
|
rescue OptionParser::InvalidOption, Date::Error => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
puts parser
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}📝 Creando bitácora para #{opts[:fecha]}...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen])
|
||||||
|
puts "#{Color::GREEN}✅ Bitácora creada exitosamente#{Color::RESET}"
|
||||||
|
puts ""
|
||||||
|
puts " #{Color::BOLD}ID:#{Color::RESET} #{bitacora['id']}"
|
||||||
|
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{bitacora['fecha']}"
|
||||||
|
puts " #{Color::BOLD}Resumen:#{Color::RESET} #{bitacora['resumen'] || '(sin resumen)'}"
|
||||||
|
puts " #{Color::BOLD}Creada:#{Color::RESET} #{bitacora['created_at']}"
|
||||||
|
rescue PG::UniqueViolation
|
||||||
|
puts "#{Color::YELLOW}⚠ Ya existe una bitácora para la fecha #{opts[:fecha]}#{Color::RESET}"
|
||||||
|
puts " Usa #{Color::DIM}adn db bitacora:listar#{Color::RESET} para ver bitácoras existentes"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: bitacora:listar [OPCIONES] - Listar bitácoras existentes
|
||||||
|
def comando_bitacora_listar(args)
|
||||||
|
opts = { limit: 10, offset: 0, desde: nil, hasta: nil }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db bitacora:listar [OPCIONES]"
|
||||||
|
opt.on("--limit N", Integer, "Número máximo de resultados (default: 10)") do |n|
|
||||||
|
opts[:limit] = n
|
||||||
|
end
|
||||||
|
opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n|
|
||||||
|
opts[:offset] = n
|
||||||
|
end
|
||||||
|
opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d|
|
||||||
|
opts[:desde] = d
|
||||||
|
end
|
||||||
|
opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h|
|
||||||
|
opts[:hasta] = h
|
||||||
|
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}📋 Listando bitácoras...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
bitacoras = db.list_bitacoras(
|
||||||
|
limit: opts[:limit],
|
||||||
|
offset: opts[:offset],
|
||||||
|
desde: opts[:desde],
|
||||||
|
hasta: opts[:hasta]
|
||||||
|
)
|
||||||
|
|
||||||
|
if bitacoras.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se encontraron bitácoras#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Resumen".ljust(40)} #{"Creada".ljust(20)}#{Color::RESET}"
|
||||||
|
puts "#{"─" * 5} #{"─" * 12} #{"─" * 40} #{"─" * 20}"
|
||||||
|
|
||||||
|
bitacoras.each do |b|
|
||||||
|
fecha = b['fecha']
|
||||||
|
resumen = b['resumen'] || ''
|
||||||
|
resumen = resumen[0..37] + '...' if resumen.length > 40
|
||||||
|
creada = Time.parse(b['created_at']).strftime('%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} #{resumen.ljust(40)} #{creada.ljust(20)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "Total: #{bitacoras.length} bitácoras"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
module ADN
|
||||||
|
module SubcomandoDBEvento
|
||||||
|
# Comando: evento:crear [OPCIONES] - Crear nueva entrada cronológica
|
||||||
|
def comando_evento_crear(args)
|
||||||
|
opts = {
|
||||||
|
inicio: nil,
|
||||||
|
descripcion: nil,
|
||||||
|
fin: nil,
|
||||||
|
estado: nil,
|
||||||
|
modo: nil,
|
||||||
|
es_ia: false,
|
||||||
|
nodo: nil,
|
||||||
|
tema: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db evento:crear [OPCIONES]"
|
||||||
|
opt.on("--inicio HH:MM", "Hora de inicio (requerido)") do |i|
|
||||||
|
opts[:inicio] = i
|
||||||
|
end
|
||||||
|
opt.on("--descripcion TEXTO", "Descripción de la actividad (requerido)") do |d|
|
||||||
|
opts[:descripcion] = d
|
||||||
|
end
|
||||||
|
opt.on("--fin HH:MM", "Hora de fin (opcional)") do |f|
|
||||||
|
opts[:fin] = f
|
||||||
|
end
|
||||||
|
opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
|
||||||
|
opts[:estado] = e
|
||||||
|
end
|
||||||
|
opt.on("--modo MODO", "Modo (P=Presencial, R=Remoto)") do |m|
|
||||||
|
opts[:modo] = m
|
||||||
|
end
|
||||||
|
opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n|
|
||||||
|
opts[:nodo] = n
|
||||||
|
end
|
||||||
|
opt.on("--ia", "Marcar como generado por IA") do
|
||||||
|
opts[:es_ia] = true
|
||||||
|
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
|
||||||
|
|
||||||
|
# Validaciones básicas
|
||||||
|
unless opts[:inicio] && opts[:descripcion]
|
||||||
|
puts "#{Color::RED}✗ Error: --inicio y --descripcion son requeridos#{Color::RESET}"
|
||||||
|
puts parser
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}📝 Creando nueva entrada...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
# Buscar nodo por nombre si se especificó
|
||||||
|
nodo_id = nil
|
||||||
|
if opts[:nodo]
|
||||||
|
nodo = db.find_nodo_by_nombre(opts[:nodo])
|
||||||
|
if nodo
|
||||||
|
nodo_id = nodo['id']
|
||||||
|
puts " #{Color::DIM}Nodo encontrado: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}"
|
||||||
|
else
|
||||||
|
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}"
|
||||||
|
puts " Creando entrada sin nodo asociado"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Buscar bitácora para hoy (o crearla)
|
||||||
|
fecha_hoy = Date.today.to_s
|
||||||
|
bitacora = db.find_bitacora_by_fecha(fecha_hoy)
|
||||||
|
|
||||||
|
if bitacora.nil?
|
||||||
|
puts " #{Color::DIM}Creando bitácora para hoy...#{Color::RESET}"
|
||||||
|
bitacora = db.create_bitacora(fecha: fecha_hoy)
|
||||||
|
end
|
||||||
|
|
||||||
|
bitacora_id = bitacora['id']
|
||||||
|
|
||||||
|
# Crear la entrada
|
||||||
|
entrada = db.create_entrada(
|
||||||
|
inicio: opts[:inicio],
|
||||||
|
descripcion: opts[:descripcion],
|
||||||
|
fin: opts[:fin],
|
||||||
|
estado: opts[:estado],
|
||||||
|
modo: opts[:modo],
|
||||||
|
es_ia: opts[:es_ia],
|
||||||
|
bitacora_id: bitacora_id,
|
||||||
|
nodo_id: nodo_id,
|
||||||
|
tema_id: nil # Por ahora sin tema
|
||||||
|
)
|
||||||
|
|
||||||
|
puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}"
|
||||||
|
puts ""
|
||||||
|
puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}"
|
||||||
|
puts " #{Color::BOLD}Inicio:#{Color::RESET} #{entrada['inicio']}"
|
||||||
|
puts " #{Color::BOLD}Fin:#{Color::RESET} #{entrada['fin'] || '(sin fin)'}"
|
||||||
|
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{entrada['descripcion']}"
|
||||||
|
puts " #{Color::BOLD}Estado:#{Color::RESET} #{entrada['estado']}"
|
||||||
|
puts " #{Color::BOLD}Modo:#{Color::RESET} #{entrada['modo'] == 'P' ? 'Presencial' : 'Remoto'}"
|
||||||
|
puts " #{Color::BOLD}IA:#{Color::RESET} #{entrada['es_ia'] ? 'Sí' : 'No'}"
|
||||||
|
puts " #{Color::BOLD}Bitácora:#{Color::RESET} #{fecha_hoy} (ID: #{bitacora_id})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: evento:actualizar <ID> [OPCIONES] - Actualizar una entrada existente
|
||||||
|
def comando_evento_actualizar(args)
|
||||||
|
if args.length < 1 || args[0].start_with?('-')
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db evento:actualizar <ID> [OPCIONES]#{Color::RESET}"
|
||||||
|
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"✅\", --descripcion \"Texto\""
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
id_evento = args.shift.to_i
|
||||||
|
opts = {}
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Opciones:"
|
||||||
|
opt.on("--inicio HH:MM", "Nueva hora de inicio") { |i| opts[:inicio] = i }
|
||||||
|
opt.on("--fin HH:MM", "Nueva hora de fin") { |f| opts[:fin] = f }
|
||||||
|
opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d }
|
||||||
|
opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e }
|
||||||
|
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}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion o --estado#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}🔄 Actualizando evento #{id_evento}...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
res = db.update_entrada(id_evento, opts)
|
||||||
|
if res
|
||||||
|
puts "#{Color::GREEN}✅ Evento #{id_evento} actualizado exitosamente.#{Color::RESET}"
|
||||||
|
puts " #{Color::BOLD}Inicio:#{Color::RESET} #{res['inicio']}"
|
||||||
|
puts " #{Color::BOLD}Fin:#{Color::RESET} #{res['fin'] || '(sin fin)'}"
|
||||||
|
else
|
||||||
|
puts "#{Color::YELLOW}⚠ No se pudo actualizar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}"
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: evento:listar [OPCIONES] - Listar entradas con filtros
|
||||||
|
def comando_evento_listar(args)
|
||||||
|
opts = { limit: 20, offset: 0, nodo: nil, estado: nil, buscar: nil, desde: nil, hasta: nil }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db evento:listar [OPCIONES]"
|
||||||
|
opt.on("--limit N", Integer, "Número máximo de resultados (default: 20)") do |n|
|
||||||
|
opts[:limit] = n
|
||||||
|
end
|
||||||
|
opt.on("--offset N", Integer, "Desplazamiento para paginación (default: 0)") do |n|
|
||||||
|
opts[:offset] = n
|
||||||
|
end
|
||||||
|
opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n|
|
||||||
|
opts[:nodo] = n
|
||||||
|
end
|
||||||
|
opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
|
||||||
|
opts[:estado] = e
|
||||||
|
end
|
||||||
|
opt.on("--buscar TEXTO", "Buscar texto en descripción") do |b|
|
||||||
|
opts[:buscar] = b
|
||||||
|
end
|
||||||
|
opt.on("--desde FECHA", "Filtrar desde fecha (YYYY-MM-DD)") do |d|
|
||||||
|
opts[:desde] = d
|
||||||
|
end
|
||||||
|
opt.on("--hasta FECHA", "Filtrar hasta fecha (YYYY-MM-DD)") do |h|
|
||||||
|
opts[:hasta] = h
|
||||||
|
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}📋 Listando entradas...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
# Obtener ID de nodo si se especificó nombre
|
||||||
|
nodo_id = nil
|
||||||
|
if opts[:nodo]
|
||||||
|
nodo = db.find_nodo_by_nombre(opts[:nodo])
|
||||||
|
if nodo
|
||||||
|
nodo_id = nodo['id']
|
||||||
|
puts " #{Color::DIM}Filtrando por nodo: #{opts[:nodo]} (ID: #{nodo_id})#{Color::RESET}"
|
||||||
|
else
|
||||||
|
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo]}' no encontrado#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
entradas = db.list_entradas(
|
||||||
|
nodo_id: nodo_id,
|
||||||
|
estado: opts[:estado],
|
||||||
|
buscar: opts[:buscar],
|
||||||
|
desde: opts[:desde],
|
||||||
|
hasta: opts[:hasta],
|
||||||
|
limit: opts[:limit],
|
||||||
|
offset: opts[:offset]
|
||||||
|
)
|
||||||
|
|
||||||
|
if entradas.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se encontraron entradas#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Fin".ljust(8)} #{"Nodo".ljust(15)} #{"Estado".ljust(3)} #{"Descripción".ljust(40)}#{Color::RESET}"
|
||||||
|
puts "#{"─" * 5} #{"─" * 8} #{"─" * 8} #{"─" * 15} #{"─" * 3} #{"─" * 40}"
|
||||||
|
|
||||||
|
entradas.each do |e|
|
||||||
|
id = e['id'].to_s
|
||||||
|
inicio = e['inicio'][0..7] # HH:MM:SS -> HH:MM
|
||||||
|
fin = e['fin'] ? e['fin'][0..7] : '--:--'
|
||||||
|
nodo = e['nodo_nombre'] || '(sin nodo)'
|
||||||
|
nodo = nodo[0..13] + '...' if nodo.length > 15
|
||||||
|
estado = e['estado']
|
||||||
|
desc = e['descripcion']
|
||||||
|
desc = desc[0..37] + '...' if desc.length > 40
|
||||||
|
|
||||||
|
puts "#{id.ljust(5)} #{inicio.ljust(8)} #{fin.ljust(8)} #{nodo.ljust(15)} #{estado.ljust(3)} #{desc.ljust(40)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "Total: #{entradas.length} entradas"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
module ADN
|
||||||
|
module SubcomandoDBHito
|
||||||
|
# Comando: hito:listar [PROYECTO]
|
||||||
|
def comando_hito_listar(args)
|
||||||
|
if args.empty?
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db hito:listar <CODIGO_PROYECTO> (ej: P2605)#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
codigo = args[0]
|
||||||
|
puts "#{Color::CYAN}📋 Listando hitos del proyecto #{codigo.upcase}...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
hitos = db.list_hitos(codigo)
|
||||||
|
if hitos.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se encontraron hitos o el proyecto no existe.#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "#{Color::BOLD}#{"Hito".ljust(8)} #{"Fase".ljust(6)} #{"Estado".ljust(8)} #{"Hrs P/R".ljust(10)} #{"Título"}#{Color::RESET}"
|
||||||
|
puts "#{"─" * 8} #{"─" * 6} #{"─" * 8} #{"─" * 10} #{"─" * 40}"
|
||||||
|
|
||||||
|
hitos.each do |h|
|
||||||
|
hrs = "#{h['horas_presencial']}/#{h['horas_remoto']}"
|
||||||
|
puts "#{h['id_hito'].ljust(8)} #{h['fase_num'].to_s.ljust(6)} #{h['estado'].ljust(8)} #{hrs.ljust(10)} #{h['titulo']}"
|
||||||
|
end
|
||||||
|
puts "\nTotal: #{hitos.size} hitos"
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: hito:ordenar <PROYECTO> [--aplicar FILE]
|
||||||
|
def comando_hito_ordenar(args)
|
||||||
|
codigo = args.shift
|
||||||
|
if !codigo
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db hito:ordenar <PROYECTO> [--aplicar /ruta/al/archivo.txt]#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
aplicar_path = nil
|
||||||
|
if args[0] == '--aplicar' && args[1]
|
||||||
|
aplicar_path = args[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
hitos = db.list_hitos(codigo)
|
||||||
|
if hitos.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se encontraron hitos para #{codigo.upcase}.#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if aplicar_path
|
||||||
|
unless File.exist?(aplicar_path)
|
||||||
|
puts "#{Color::RED}✗ No se encontró el archivo: #{aplicar_path}#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Procesar el archivo y aplicarrenombramientos lógicos "Fase.Grupo.Paso"
|
||||||
|
puts "#{Color::CYAN}📋 Aplicando ordenamiento desde #{aplicar_path}...#{Color::RESET}"
|
||||||
|
lineas = File.readlines(aplicar_path).map(&:strip).reject { |l| l.empty? || l.start_with?('#') }
|
||||||
|
|
||||||
|
# Agrupar por fase según el orden de las líneas
|
||||||
|
hitos_por_fase = Hash.new { |h, k| h[k] = [] }
|
||||||
|
lineas.each do |linea|
|
||||||
|
if linea =~ /^(\d+):\s*\[.+?\]\s*(.+)$/
|
||||||
|
id_db = $1.to_i
|
||||||
|
hito = hitos.find { |h| h['id'] == id_db }
|
||||||
|
hitos_por_fase[hito['fase_num']] << hito if hito
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Iniciar renombrado secuencial
|
||||||
|
hitos_por_fase.each do |fase, hitos_fase|
|
||||||
|
grupo = 1
|
||||||
|
letra = 65 # 'A'
|
||||||
|
|
||||||
|
hitos_fase.each do |h|
|
||||||
|
nuevo_id = "#{fase}.#{grupo}.#{letra.chr}"
|
||||||
|
|
||||||
|
if h['id_hito'] != nuevo_id
|
||||||
|
db.rename_hito_id(codigo, h['id_hito'], nuevo_id)
|
||||||
|
puts " #{Color::GREEN}✔ #{h['id_hito'].ljust(8)} → #{nuevo_id.ljust(8)}#{Color::RESET} | #{h['titulo']}"
|
||||||
|
else
|
||||||
|
puts " #{Color::DIM}• #{nuevo_id.ljust(8)} (Sin cambios) | #{h['titulo']}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
|
||||||
|
letra += 1
|
||||||
|
if letra > 67 # 'C'
|
||||||
|
letra = 65
|
||||||
|
grupo += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
puts "#{Color::GREEN}✅ Ordenamiento aplicado exitosamente a #{codigo.upcase}.#{Color::RESET}"
|
||||||
|
|
||||||
|
else
|
||||||
|
# Generar archivo temporal para reordenamiento (Swarm-Ready)
|
||||||
|
tmp_file = "/tmp/adn_ordenar_#{codigo.downcase}.txt"
|
||||||
|
File.open(tmp_file, 'w') do |f|
|
||||||
|
f.puts "# ==========================================================================="
|
||||||
|
f.puts "# MODO ORDENAMIENTO DE HITOS - PROYECTO #{codigo.upcase}"
|
||||||
|
f.puts "# ==========================================================================="
|
||||||
|
f.puts "# Cambia el orden de las líneas (Cortar y Pegar) dentro de la misma FASE"
|
||||||
|
f.puts "# para definir la secuencia lógica de ejecución."
|
||||||
|
f.puts "# NO MODIFIQUES el número inicial ni los corchetes (ej: '42: [F3]')."
|
||||||
|
f.puts "# Cuando termines de editar, guarda el archivo y ejecuta en la terminal:"
|
||||||
|
f.puts "# adn db hito:ordenar #{codigo.upcase} --aplicar #{tmp_file}"
|
||||||
|
f.puts "# ===========================================================================\n\n"
|
||||||
|
|
||||||
|
fases = hitos.group_by { |h| h['fase_num'] }.sort_by { |k, v| k }
|
||||||
|
fases.each do |num_fase, hitos_fase|
|
||||||
|
f.puts "# --- FASE #{num_fase} ---"
|
||||||
|
hitos_fase.each do |h|
|
||||||
|
f.puts "#{h['id']}: [F#{num_fase}] #{h['titulo']}"
|
||||||
|
end
|
||||||
|
f.puts "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}✅ Archivo de configuración generado en: #{tmp_file}#{Color::RESET}"
|
||||||
|
puts "1. Abre el archivo y reordena las líneas como necesites."
|
||||||
|
puts "2. Ejecuta: #{Color::BOLD}adn db hito:ordenar #{codigo.upcase} --aplicar #{tmp_file}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: hito:renombrar <PROYECTO> <ID_HITO_ACTUAL> <NUEVO_ID_HITO>
|
||||||
|
def comando_hito_renombrar(args)
|
||||||
|
if args.length < 3
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db hito:renombrar <PROYECTO> <ID_HITO_ACTUAL> <NUEVO_ID_HITO>#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
proyecto = args[0]
|
||||||
|
id_actual = args[1]
|
||||||
|
id_nuevo = args[2]
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}🔄 Renombrando hito #{id_actual} → #{id_nuevo} (Proyecto #{proyecto.upcase})...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
res = db.rename_hito_id(proyecto, id_actual, id_nuevo)
|
||||||
|
puts "#{Color::GREEN}✅ Hito renombrado exitosamente. Título: #{res['titulo']}#{Color::RESET}"
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: hito:actualizar <PROYECTO> <ID_HITO> [OPCIONES]
|
||||||
|
def comando_hito_actualizar(args)
|
||||||
|
if args.length < 2
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db hito:actualizar <PROYECTO> <ID_HITO> [OPCIONES]#{Color::RESET}"
|
||||||
|
puts "Opciones soportadas: --estado=EJ, --horas-p=1.5, --horas-r=0, --desc=\"Texto\""
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
proyecto = args.shift
|
||||||
|
id_hito = args.shift
|
||||||
|
opts = { estado: nil, horas_presencial: nil, horas_remoto: nil, descripcion: nil }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Opciones:"
|
||||||
|
opt.on("--estado ESTADO", "Nuevo estado (ej: ✅, ⏳, 📋)") { |e| opts[:estado] = e }
|
||||||
|
opt.on("--horas-p HORAS", "Horas presenciales") { |h| opts[:horas_presencial] = h.to_f }
|
||||||
|
opt.on("--horas-r HORAS", "Horas remotas") { |h| opts[:horas_remoto] = h.to_f }
|
||||||
|
opt.on("--desc DESC", "Descripción o detalle para el hito") { |d| opts[:descripcion] = d }
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
parser.parse!(args)
|
||||||
|
rescue OptionParser::InvalidOption => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if opts.values.all?(&:nil?)
|
||||||
|
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --estado, --horas-p, --horas-r o --desc#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}🔄 Actualizando hito #{id_hito} del proyecto #{proyecto.upcase}...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
res = db.update_hito(
|
||||||
|
proyecto, id_hito,
|
||||||
|
estado: opts[:estado],
|
||||||
|
horas_presencial: opts[:horas_presencial],
|
||||||
|
horas_remoto: opts[:horas_remoto],
|
||||||
|
descripcion: opts[:descripcion]
|
||||||
|
)
|
||||||
|
puts "#{Color::GREEN}✅ Hito actualizado exitosamente.#{Color::RESET}"
|
||||||
|
hrs = "#{res['horas_presencial'].to_f}P / #{res['horas_remoto'].to_f}R"
|
||||||
|
puts " Estado actual : #{res['estado']}"
|
||||||
|
puts " Horas imputadas : #{hrs}"
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
module ADN
|
||||||
|
module SubcomandoDBNodo
|
||||||
|
# Comando: nodo:crear [OPCIONES] - Crear nuevo nodo
|
||||||
|
def comando_nodo_crear(args)
|
||||||
|
opts = { nombre: nil, ip: nil, tipo: 'servidor', descripcion: nil, activo: true }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db nodo:crear [OPCIONES]"
|
||||||
|
opt.on("--nombre NOMBRE", "Nombre único del nodo (requerido)") do |n|
|
||||||
|
opts[:nombre] = n
|
||||||
|
end
|
||||||
|
opt.on("--ip DIRECCION", "Dirección IP (opcional)") do |i|
|
||||||
|
opts[:ip] = i
|
||||||
|
end
|
||||||
|
opt.on("--tipo TIPO", "Tipo (servidor, hipervisor, vm, pc)") do |t|
|
||||||
|
opts[:tipo] = t
|
||||||
|
end
|
||||||
|
opt.on("--descripcion TEXTO", "Descripción del nodo") do |d|
|
||||||
|
opts[:descripcion] = d
|
||||||
|
end
|
||||||
|
opt.on("--inactivo", "Crear nodo como inactivo") do
|
||||||
|
opts[:activo] = 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
|
||||||
|
|
||||||
|
unless opts[:nombre]
|
||||||
|
puts "#{Color::RED}✗ Error: --nombre es requerido#{Color::RESET}"
|
||||||
|
puts parser
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "#{Color::CYAN}🖥️ Creando nodo '#{opts[:nombre]}'...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
begin
|
||||||
|
nodo = db.create_nodo(
|
||||||
|
nombre: opts[:nombre],
|
||||||
|
ip: opts[:ip],
|
||||||
|
tipo: opts[:tipo],
|
||||||
|
descripcion: opts[:descripcion],
|
||||||
|
activo: opts[:activo]
|
||||||
|
)
|
||||||
|
|
||||||
|
puts "#{Color::GREEN}✅ Nodo creado exitosamente#{Color::RESET}"
|
||||||
|
puts ""
|
||||||
|
puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}"
|
||||||
|
puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}"
|
||||||
|
puts " #{Color::BOLD}IP:#{Color::RESET} #{nodo['ip'] || '(sin IP)'}"
|
||||||
|
puts " #{Color::BOLD}Tipo:#{Color::RESET} #{nodo['tipo']}"
|
||||||
|
puts " #{Color::BOLD}Activo:#{Color::RESET} #{nodo['activo'] ? 'Sí' : 'No'}"
|
||||||
|
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{nodo['descripcion'] || '(sin descripción)'}"
|
||||||
|
puts " #{Color::BOLD}Creado:#{Color::RESET} #{nodo['created_at']}"
|
||||||
|
rescue PG::UniqueViolation
|
||||||
|
puts "#{Color::YELLOW}⚠ Ya existe un nodo con el nombre '#{opts[:nombre]}'#{Color::RESET}"
|
||||||
|
puts " Usa #{Color::DIM}adn db nodo:listar#{Color::RESET} para ver nodos existentes"
|
||||||
|
rescue => e
|
||||||
|
puts "#{Color::RED}✗ Error creando nodo: #{e.message}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comando: nodo:listar [OPCIONES] - Listar nodos del sistema
|
||||||
|
def comando_nodo_listar(args)
|
||||||
|
opts = { activo: nil, tipo: nil }
|
||||||
|
|
||||||
|
parser = OptionParser.new do |opt|
|
||||||
|
opt.banner = "Uso: adn db nodo:listar [OPCIONES]"
|
||||||
|
opt.on("--activo", "Mostrar solo nodos activos") do
|
||||||
|
opts[:activo] = true
|
||||||
|
end
|
||||||
|
opt.on("--inactivo", "Mostrar solo nodos inactivos") do
|
||||||
|
opts[:activo] = false
|
||||||
|
end
|
||||||
|
opt.on("--tipo TIPO", "Filtrar por tipo (servidor, hipervisor, vm, pc)") do |t|
|
||||||
|
opts[:tipo] = t
|
||||||
|
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}🖥️ Listando nodos...#{Color::RESET}"
|
||||||
|
|
||||||
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||||
|
nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], limit: 100)
|
||||||
|
|
||||||
|
if nodos.empty?
|
||||||
|
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{Color::RESET}"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}"
|
||||||
|
puts "#{"─" * 5} #{"─" * 20} #{"─" * 15} #{"─" * 12} #{"─" * 6} #{"─" * 30}"
|
||||||
|
|
||||||
|
nodos.each do |n|
|
||||||
|
id = n['id'].to_s
|
||||||
|
nombre = n['nombre']
|
||||||
|
ip = n['ip'] || '(sin IP)'
|
||||||
|
tipo = n['tipo']
|
||||||
|
activo = n['activo'] ? '✅' : '❌'
|
||||||
|
desc = n['descripcion'] || ''
|
||||||
|
desc = desc[0..27] + '...' if desc.length > 30
|
||||||
|
|
||||||
|
puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts ""
|
||||||
|
puts "Total: #{nodos.length} nodos"
|
||||||
|
|
||||||
|
# Estadísticas por tipo
|
||||||
|
tipos = nodos.group_by { |n| n['tipo'] }
|
||||||
|
puts "#{Color::DIM}Tipos: #{tipos.map { |t, ns| "#{t}: #{ns.length}" }.join(', ')}#{Color::RESET}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user