[P2606] WebServer NGINX: plan actualizado, ficha nodo, HTTPS operativo

- P2606: Plan v1.1 EN EJECUCIÓN. Fases 1-3 completadas.
- Ficha nodo srvv-nginx-rm.md. P2606 en adn/07_proyectos.md.
- Incluye cambios acumulados de sesiones anteriores.
This commit is contained in:
Ricardo Monla
2026-03-28 12:18:42 -03:00
parent 97d9b777b4
commit 160da47f96
78 changed files with 6461 additions and 624 deletions
+256
View File
@@ -0,0 +1,256 @@
# frozen_string_literal: true
require 'optparse'
require_relative '../core/colores'
require_relative '../core/help_formatter'
require_relative '../db/core/bitacora_db'
module ADN
class SubcomandoAmbitos
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty? || ['help', '-h', '--help'].include?(@args[0])
mostrar_ayuda
return
end
subcomando = @args[0]
sub_args = @args[1..-1] || []
case subcomando
when 'listar'
comando_listar(sub_args)
when 'crear'
comando_crear(sub_args)
when 'info'
comando_info(sub_args)
when 'nodos'
comando_nodos(sub_args)
else
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
puts "\n"
mostrar_ayuda
exit 1
end
rescue Interrupt
puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}"
exit 130
rescue => e
@logger.error("Error en operación de ámbitos: #{e.message}")
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
exit 1
end
private
def comando_listar(args)
opts = { activo: nil }
parser = OptionParser.new do |opt|
opt.on("--activo", "Mostrar solo ámbitos activos") { opts[:activo] = true }
opt.on("--inactivo", "Mostrar solo ámbitos inactivos") { opts[:activo] = false }
opt.on("-h", "--help", "Mostrar ayuda") do
puts HelpFormatter.generar_help(
titulo: "Listar Ámbitos",
descripcion: "Muestra la lista de ámbitos (agrupadores de nodos).",
uso: HelpFormatter.format_usage("adn ambitos listar", "[OPCIONES]"),
opciones: [
{ names: ["activo"], desc: "Mostrar solo ámbitos activos" },
{ names: ["inactivo"], desc: "Mostrar solo ámbitos inactivos" },
{ names: ["h", "help"], desc: "Mostrar esta ayuda" }
],
ejemplos: [{ cmd: "./adn/tools/run ambitos listar", desc: "Listar todos los ámbitos" }]
)
exit 0
end
end
parsear_argumentos!(parser, args)
puts "#{Color::CYAN}📋 Listando ámbitos...#{Color::RESET}\n\n"
BitacorasDB::BitacoraDB.with_connection do |db|
ambitos = db.list_ambitos(activo: opts[:activo])
if ambitos.empty?
puts "#{Color::YELLOW}⚠ No se encontraron ámbitos.#{Color::RESET}"
return
end
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Nodos".ljust(7)} #{"Activo".ljust(6)} #{"Padre".ljust(20)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 20} #{"" * 7} #{"" * 6} #{"" * 20}"
ambitos.each do |a|
id = a['id'].to_s.ljust(5)
nombre = a['nombre'].to_s.ljust(20)
count = a['count_nodos'].to_s.ljust(7)
activo = (a['activo'] ? '✅' : '❌').ljust(6)
padre = (a['parent_nombre'] || '-').ljust(20)
puts "#{id} #{nombre} #{count} #{activo} #{padre}"
end
puts "\nTotal: #{ambitos.length} ámbitos."
end
end
def comando_crear(args)
opts = { nombre: nil, descripcion: nil, activo: true }
parser = OptionParser.new do |opt|
opt.on("--nombre NOMBRE", "Nombre del ámbito") { |n| opts[:nombre] = n }
opt.on("--descripcion DESC", "Descripción del ámbito") { |d| opts[:descripcion] = d }
opt.on("-h", "--help", "Mostrar ayuda") do
puts HelpFormatter.generar_help(
titulo: "Crear Ámbito",
descripcion: "Genera un nuevo ámbito para agrupar nodos de infraestructura.",
uso: HelpFormatter.format_usage("adn ambitos crear", "--nombre NOMBRE [OPCIONES]"),
opciones: [
{ names: ["nombre NOMBRE"], desc: "Nombre único del ámbito (obligatorio)" },
{ names: ["descripcion DESC"], desc: "Descripción del propósito del ámbito" },
{ names: ["h", "help"], desc: "Mostrar esta ayuda" }
],
ejemplos: [{ cmd: "./adn/tools/run ambitos crear --nombre dtic-Pruebas", desc: "Crear un ámbito de pruebas" }]
)
exit 0
end
end
parsear_argumentos!(parser, args)
unless opts[:nombre]
puts "#{Color::RED}✗ Error: El parámetro --nombre es obligatorio.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN}📦 Creando ámbito '#{opts[:nombre]}'...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
begin
ambito = db.create_ambito(nombre: opts[:nombre], descripcion: opts[:descripcion])
puts "#{Color::GREEN}✅ Ámbito creado exitosamente. ID: #{ambito['id']}#{Color::RESET}"
rescue PG::UniqueViolation
puts "#{Color::RED}✗ Error: Ya existe un ámbito con el nombre '#{opts[:nombre]}'.#{Color::RESET}"
exit 1
end
end
end
def comando_info(args)
opts = {}
parser = OptionParser.new do |opt|
opt.on("-h", "--help", "Mostrar ayuda") do
puts HelpFormatter.generar_help(
titulo: "Info de Ámbito",
descripcion: "Muestra información y estadísticas detalladas de un ámbito.",
uso: HelpFormatter.format_usage("adn ambitos info", "<nombre_o_id>"),
opciones: [],
ejemplos: [{ cmd: "./adn/tools/run ambitos info dtic-DASUTEN", desc: "Ver detalles de dtic-DASUTEN" }]
)
exit 0
end
end
parsear_argumentos!(parser, args)
objetivo = args.first
if objetivo.nil? || objetivo.empty?
puts "#{Color::RED}✗ Error: Debes especificar el nombre o ID del ámbito.#{Color::RESET}"
exit 1
end
BitacorasDB::BitacoraDB.with_connection do |db|
ambito = db.get_ambito(objetivo)
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN} Info del Ámbito: #{ambito['nombre']}#{Color::RESET}\n\n"
puts " #{Color::BOLD}ID:#{Color::RESET} #{ambito['id']}"
puts " #{Color::BOLD}Nombre:#{Color::RESET} #{ambito['nombre']}"
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{ambito['descripcion'] || '(sin descripción)'}"
puts " #{Color::BOLD}Padre:#{Color::RESET} #{ambito['parent_nombre'] || '(raíz)'}"
puts " #{Color::BOLD}Activo:#{Color::RESET} #{ambito['activo'] ? 'Sí' : 'No'}"
puts " #{Color::BOLD}Nodos:#{Color::RESET} #{ambito['count_nodos']}"
puts ""
end
end
def comando_nodos(args)
opts = {}
parser = OptionParser.new do |opt|
opt.on("-h", "--help", "Mostrar ayuda") do
puts HelpFormatter.generar_help(
titulo: "Nodos de Ámbito",
descripcion: "Lista todos los nodos pertenecientes a un ámbito.",
uso: HelpFormatter.format_usage("adn ambitos nodos", "<nombre_o_id>"),
opciones: [],
ejemplos: [{ cmd: "./adn/tools/run ambitos nodos dtic-DASUTEN", desc: "Ver componentes agrupados bajo dtic-DASUTEN" }]
)
exit 0
end
end
parsear_argumentos!(parser, args)
objetivo = args.first
if objetivo.nil? || objetivo.empty?
puts "#{Color::RED}✗ Error: Debes especificar el nombre o ID del ámbito.#{Color::RESET}"
exit 1
end
BitacorasDB::BitacoraDB.with_connection do |db|
ambito = db.get_ambito(objetivo)
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN}🖥️ Nodos del ámbito '#{ambito['nombre']}'...#{Color::RESET}\n\n"
nodos = db.get_nodos_by_ambito(ambito['id'])
if nodos.empty?
puts "#{Color::YELLOW}⚠ Este ámbito no tiene nodos asignados.#{Color::RESET}"
return
end
nodos.each do |n|
tipo = n['tipo'].to_s.ljust(10)
ip = (n['ip'] || '-').ljust(15)
estado = n['activo'] ? "#{Color::GREEN}ON#{Color::RESET}" : "#{Color::RED}OFF#{Color::RESET}"
puts " #{Color::DIM}#{Color::RESET} #{Color::BOLD}#{n['nombre'].to_s.ljust(20)}#{Color::RESET} [#{tipo}] IP: #{ip} (#{estado})"
end
end
end
def mostrar_ayuda
puts HelpFormatter.generar_help(
titulo: "Gestión de Ámbitos (Modelo DB-First)",
descripcion: "Los ámbitos agrupan lógicamente a los nodos correspondientes a áreas organizativas.",
uso: HelpFormatter.format_usage("adn ambitos", "<subcomando> [OPCIONES]"),
opciones: [
{ names: ["h", "help"], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run ambitos listar", desc: "Listar ámbitos y cantidad de nodos" },
{ cmd: "./adn/tools/run ambitos info dtic-DIIAA", desc: "Ver detalle del ámbito DIIAA" },
{ cmd: "./adn/tools/run ambitos nodos dtic-DASUTEN", desc: "Listar hipervisores e infraestructura de DASUTEN" }
]
)
end
def parsear_argumentos!(parser, args)
begin
parser.parse!(args)
rescue OptionParser::InvalidOption => e
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
puts "\nEjecuta con --help para ver las opciones disponibles."
exit 1
end
end
end
end
+14 -130
View File
@@ -42,10 +42,12 @@ module ADN
require_relative 'db/bitacora'
require_relative 'db/evento'
require_relative 'db/nodo'
require_relative 'db/ambito'
require_relative 'db/hito'
require_relative 'db/misc'
include SubcomandoDBBitacora
include SubcomandoDBEvento
include SubcomandoDBAmbito
include SubcomandoDBNodo
include SubcomandoDBHito
include SubcomandoDBMisc
@@ -88,6 +90,8 @@ module ADN
comando_nodo_crear(sub_args)
when 'nodo:listar'
comando_nodo_listar(sub_args)
when 'nodo:actualizar'
comando_nodo_actualizar(sub_args)
when 'hito:listar'
comando_hito_listar(sub_args)
when 'hito:actualizar'
@@ -98,10 +102,20 @@ module ADN
comando_hito_ordenar(sub_args)
when 'estadisticas'
comando_estadisticas
when 'ambito:listar'
comando_ambito_listar(sub_args)
when 'ambito:crear'
comando_ambito_crear(sub_args)
when 'ambito:info'
comando_ambito_info(sub_args)
when 'ambito:nodos'
comando_ambito_nodos(sub_args)
when 'backup'
comando_backup(sub_args)
when 'archivar:md'
comando_archivar_md
when 'migrar:sql'
comando_migrar_sql(sub_args)
else
@logger.error("Subcomando desconocido: #{subcomando}")
@@ -181,134 +195,4 @@ end
end
end
module ADN
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
#
# @param args [Array<String>] Argumentos de línea de comandos
# @param logger [ADNLogger] Instancia de logger para registro
def initialize(args, logger)
@args = args
@logger = logger
@config = cargar_configuracion
end
# Ejecuta el subcomando según los argumentos proporcionados
def ejecutar
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
subcomando = @args[0]
sub_args = @args[1..-1] || []
case subcomando
when 'salud:bd'
comando_salud_bd
when 'bitacora:crear'
comando_bitacora_crear(sub_args)
when 'bitacora:listar'
comando_bitacora_listar(sub_args)
when 'evento:crear'
comando_evento_crear(sub_args)
when 'evento:listar'
comando_evento_listar(sub_args)
when 'evento:actualizar'
comando_evento_actualizar(sub_args)
when 'evento:eliminar'
comando_evento_eliminar(sub_args)
when 'nodo:crear'
comando_nodo_crear(sub_args)
when 'nodo:listar'
comando_nodo_listar(sub_args)
when 'hito:listar'
comando_hito_listar(sub_args)
when 'hito:actualizar'
comando_hito_actualizar(sub_args)
when 'hito:renombrar'
comando_hito_renombrar(sub_args)
when 'hito:ordenar'
comando_hito_ordenar(sub_args)
when 'estadisticas'
comando_estadisticas
when 'backup'
comando_backup(sub_args)
when 'archivar:md'
comando_archivar_md
else
@logger.error("Subcomando desconocido: #{subcomando}")
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
puts ""
mostrar_ayuda
exit 1
end
rescue Interrupt
puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}"
@logger.info("Operación de base de datos interrumpida por usuario")
exit 130
rescue => e
@logger.error("Error en operación de base de datos: #{e.message}", { backtrace: e.backtrace })
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
exit 1
end
private
# ─── COMANDOS PRINCIPALES ──────────────────────────────────────────────
# ─── MÉTODOS AUXILIARES ──────────────────────────────────────────────
# Carga la configuración desde config.yml
def cargar_configuracion
config_path = File.join(__dir__, 'config.yml')
return {} unless File.exist?(config_path)
begin
YAML.load_file(config_path)
rescue => e
@logger.error("Error cargando configuración: #{e.message}")
{}
end
end
# Muestra la ayuda general del subcomando
def mostrar_ayuda
puts "#{Color::BOLD}Uso: ./adn/tools/run db [SUBCOMANDO] [OPCIONES]#{Color::RESET}"
puts ""
puts "#{Color::BOLD}Subcomandos disponibles:#{Color::RESET}"
puts ""
puts " #{Color::CYAN}salud:bd#{Color::RESET} Verificar conexión y estado de la BD"
puts " #{Color::CYAN}backup [OPCIONES]#{Color::RESET} Crear backup de la base de datos"
puts " #{Color::CYAN}bitacora:crear [FECHA]#{Color::RESET} Crear nueva bitácora diaria"
puts " #{Color::CYAN}bitacora:listar [OPCIONES]#{Color::RESET} Listar bitácoras existentes"
puts " #{Color::CYAN}evento:crear [OPCIONES]#{Color::RESET} Crear nueva entrada cronológica"
puts " #{Color::CYAN}evento:listar [OPCIONES]#{Color::RESET} Listar entradas con filtros"
puts " #{Color::CYAN}nodo:crear [OPCIONES]#{Color::RESET} Crear nuevo nodo (servidor/VM/PC)"
puts " #{Color::CYAN}nodo:listar [OPCIONES]#{Color::RESET} Listar nodos del sistema"
puts " #{Color::CYAN}estadisticas#{Color::RESET} Mostrar estadísticas del sistema"
puts " #{Color::CYAN}archivar:md#{Color::RESET} Mueve los archivos .md migrados al archivo histórico"
puts ""
puts "#{Color::BOLD}Ejemplos:#{Color::RESET}"
puts " ./adn/tools/run db salud:bd"
puts " ./adn/tools/run db bitacora:crear --resumen \"Inicio de proyecto\""
puts " ./adn/tools/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8"
puts " ./adn/tools/run db nodo:listar --tipo servidor"
puts ""
puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)"
puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
end
end
end
+158
View File
@@ -0,0 +1,158 @@
# frozen_string_literal: true
require 'optparse'
require_relative '../../core/colores'
require_relative '../../core/help_formatter'
module ADN
module SubcomandoDBAmbito
# Comando: ambito:listar [OPCIONES] - Listar ámbitos
def comando_ambito_listar(args)
opts = { activo: nil }
parser = OptionParser.new do |opt|
opt.banner = "Uso: adn db ambito:listar [OPCIONES]"
opt.on("--activo", "Mostrar solo ámbitos activos") { opts[:activo] = true }
opt.on("--inactivo", "Mostrar solo ámbitos inactivos") { opts[:activo] = false }
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
puts "#{Color::CYAN}📋 Listando ámbitos...#{Color::RESET}\n\n"
BitacorasDB::BitacoraDB.with_connection do |db|
ambitos = db.list_ambitos(activo: opts[:activo])
if ambitos.empty?
puts "#{Color::YELLOW}⚠ No se encontraron ámbitos.#{Color::RESET}"
return
end
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Nodos".ljust(7)} #{"Activo".ljust(6)} #{"Padre".ljust(20)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 20} #{"" * 7} #{"" * 6} #{"" * 20}"
ambitos.each do |a|
id = a['id'].to_s.ljust(5)
nombre = a['nombre'].to_s.ljust(20)
count = a['count_nodos'].to_s.ljust(7)
activo = (a['activo'] ? '✅' : '❌').ljust(6)
padre = (a['parent_nombre'] || '-').ljust(20)
puts "#{id} #{nombre} #{count} #{activo} #{padre}"
end
puts "\nTotal: #{ambitos.length} ámbitos."
end
end
# Comando: ambito:crear [OPCIONES] - Crear nuevo ámbito
def comando_ambito_crear(args)
opts = { nombre: nil, descripcion: nil, parent_id: nil }
parser = OptionParser.new do |opt|
opt.banner = "Uso: adn db ambito:crear --nombre NOMBRE [OPCIONES]"
opt.on("--nombre NOMBRE", "Nombre único del ámbito (obligatorio)") { |n| opts[:nombre] = n }
opt.on("--descripcion DESC", "Descripción del ámbito") { |d| opts[:descripcion] = d }
opt.on("--parent-id ID", Integer, "ID del ámbito padre") { |p| opts[:parent_id] = p }
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
unless opts[:nombre]
puts "#{Color::RED}✗ Error: El parámetro --nombre es obligatorio.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN}📦 Creando ámbito '#{opts[:nombre]}'...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
begin
ambito = db.create_ambito(
nombre: opts[:nombre],
descripcion: opts[:descripcion],
parent_id: opts[:parent_id]
)
puts "#{Color::GREEN}✅ Ámbito creado exitosamente. ID: #{ambito['id']}#{Color::RESET}"
rescue PG::UniqueViolation
puts "#{Color::RED}✗ Error: Ya existe un ámbito con el nombre '#{opts[:nombre]}'.#{Color::RESET}"
exit 1
end
end
end
# Comando: ambito:info [NOMBRE_O_ID] - Info detallada
def comando_ambito_info(args)
objetivo = args.first
if objetivo.nil? || objetivo.start_with?('-')
puts "#{Color::YELLOW}Uso: adn db ambito:info <nombre_o_id>#{Color::RESET}"
return
end
BitacorasDB::BitacoraDB.with_connection do |db|
ambito = db.get_ambito(objetivo)
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN} Info del Ámbito: #{ambito['nombre']}#{Color::RESET}\n\n"
puts " #{Color::BOLD}ID:#{Color::RESET} #{ambito['id']}"
puts " #{Color::BOLD}Nombre:#{Color::RESET} #{ambito['nombre']}"
puts " #{Color::BOLD}Descripción:#{Color::RESET} #{ambito['descripcion'] || '(sin descripción)'}"
puts " #{Color::BOLD}Padre:#{Color::RESET} #{ambito['parent_nombre'] || '(raíz)'}"
puts " #{Color::BOLD}Activo:#{Color::RESET} #{ambito['activo'] ? 'Sí' : 'No'}"
puts " #{Color::BOLD}Nodos:#{Color::RESET} #{ambito['count_nodos']}"
puts ""
end
end
# Comando: ambito:nodos [NOMBRE_O_ID] - Listar nodos de un ámbito
def comando_ambito_nodos(args)
objetivo = args.first
if objetivo.nil? || objetivo.start_with?('-')
puts "#{Color::YELLOW}Uso: adn db ambito:nodos <nombre_o_id>#{Color::RESET}"
return
end
BitacorasDB::BitacoraDB.with_connection do |db|
ambito = db.get_ambito(objetivo)
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{objetivo}' no encontrado.#{Color::RESET}"
exit 1
end
puts "#{Color::CYAN}🖥️ Nodos del ámbito '#{ambito['nombre']}'...#{Color::RESET}\n\n"
nodos = db.get_nodos_by_ambito(ambito['id'])
if nodos.empty?
puts "#{Color::YELLOW}⚠ Este ámbito no tiene nodos asignados.#{Color::RESET}"
return
end
nodos.each do |n|
tipo = n['tipo'].to_s.ljust(10)
ip = (n['ip'] || '-').ljust(15)
estado = n['activo'] ? "#{Color::GREEN}ON#{Color::RESET}" : "#{Color::RED}OFF#{Color::RESET}"
puts " #{Color::DIM}#{Color::RESET} #{Color::BOLD}#{n['nombre'].to_s.ljust(20)}#{Color::RESET} [#{tipo}] IP: #{ip} (#{estado})"
end
end
end
end
end
+14 -7
View File
@@ -2,13 +2,16 @@ module ADN
module SubcomandoDBBitacora
# Comando: bitacora:crear [FECHA] - Crear nueva bitácora diaria
def comando_bitacora_crear(args)
opts = { fecha: Date.today, resumen: nil }
opts = { fecha: Date.today, resumen: nil, modo: 'P' }
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("--modo MODO", "Modo de la jornada: P=Presencial, R=Remoto (default: P)") do |m|
opts[:modo] = m.upcase
end
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -17,7 +20,6 @@ module ADN
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}"
@@ -29,11 +31,12 @@ module ADN
BitacorasDB::BitacoraDB.with_connection do |db|
begin
bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen])
bitacora = db.create_bitacora(fecha: opts[:fecha], resumen: opts[:resumen], modo: opts[:modo])
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}Modo:#{Color::RESET} #{bitacora['modo']} (#{modo_label(bitacora['modo'])})"
puts " #{Color::BOLD}Resumen:#{Color::RESET} #{bitacora['resumen'] || '(sin resumen)'}"
puts " #{Color::BOLD}Creada:#{Color::RESET} #{bitacora['created_at']}"
rescue PG::UniqueViolation
@@ -91,16 +94,17 @@ module ADN
end
puts ""
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Resumen".ljust(40)} #{"Creada".ljust(20)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 12} #{"" * 40} #{"" * 20}"
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Fecha".ljust(12)} #{"Modo"} #{"Resumen".ljust(38)} #{"Creada".ljust(20)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 12} #{"" * 4} #{"" * 38} #{"" * 20}"
bitacoras.each do |b|
fecha = b['fecha']
modo = b['modo'] || 'P'
resumen = b['resumen'] || ''
resumen = resumen[0..37] + '...' if resumen.length > 40
resumen = resumen[0..35] + '...' if resumen.length > 38
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)}"
puts "#{b['id'].to_s.ljust(5)} #{fecha.ljust(12)} [#{modo}] #{resumen.ljust(38)} #{creada.ljust(20)}"
end
puts ""
@@ -108,5 +112,8 @@ module ADN
end
end
def modo_label(modo)
{ 'P' => 'Presencial', 'R' => 'Remoto', 'S' => 'Servicio' }[modo] || modo
end
end
end
+134 -32
View File
@@ -10,18 +10,22 @@ module ADN
modo: nil,
es_ia: false,
nodo: nil,
tema: nil
tema: nil,
plantilla: nil,
fecha: nil,
ambito: nil,
doc_path: 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|
opt.on("--inicio HH:MM[:SS]", "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|
opt.on("--fin HH:MM[:SS]", "Hora de fin (opcional)") do |f|
opts[:fin] = f
end
opt.on("--estado ESTADO", "Estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
@@ -33,9 +37,21 @@ module ADN
opt.on("--nodo NOMBRE", "Nombre del nodo (servidor/VM)") do |n|
opts[:nodo] = n
end
opt.on("--fecha YYYY-MM-DD", "Fecha de la bitácora (default: hoy)") do |f|
opts[:fecha] = f
end
opt.on("--ambito NOMBRE_O_ID", "Ámbito del evento") do |a|
opts[:ambito] = a
end
opt.on("--doc RUTA", "Ruta al plan .md (ej: P2601_Dasuten/P2601.09_DASUTEN-sin-DC.md)") do |d|
opts[:doc_path] = d
end
opt.on("--ia", "Marcar como generado por IA") do
opts[:es_ia] = true
end
opt.on("--plantilla TIPO", "Usar plantilla (implementacion, investigacion, resolucion, documentacion, test, debug, planificacion)") do |p|
opts[:plantilla] = p
end
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -51,12 +67,32 @@ module ADN
end
# Validaciones básicas
unless opts[:inicio] && opts[:descripcion]
puts "#{Color::RED}✗ Error: --inicio y --descripcion son requeridos#{Color::RESET}"
unless opts[:descripcion]
puts "#{Color::RED}✗ Error: --descripcion es requerido#{Color::RESET}"
puts parser
exit 1
end
# Si no se especifica --inicio, usar hora actual
opts[:inicio] ||= Time.now.strftime('%H:%M:%S')
puts "#{Color::DIM} Hora de inicio: #{opts[:inicio]}#{Color::RESET}"
# Si se specify plantilla, mostrar y prependear al texto
if opts[:plantilla]
require_relative '../../templates/plantillas_eventos'
if ADN::PlantillasEventos.existe?(opts[:plantilla])
plantilla = ADN::PlantillasEventos.obtener(opts[:plantilla])
puts "#{Color::CYAN}📋 Plantilla: #{opts[:plantilla]}#{Color::RESET}"
puts plantilla
puts ""
puts "#{Color::YELLOW}💡 Completa la plantilla y usa --descripcion con el resultado#{Color::RESET}"
else
puts "#{Color::RED}✗ Tipo de plantilla desconocido: #{opts[:plantilla]}#{Color::RESET}"
puts "#{Color::YELLOW}Disponibles: #{ADN::PlantillasEventos.lista_tipos.join(', ')}#{Color::RESET}"
exit 1
end
end
# Obtener modo desde jornada actual si no se especifica
modo_resuelto = opts[:modo]
if modo_resuelto.nil?
@@ -79,24 +115,44 @@ module ADN
end
end
# Buscar bitácora para hoy (o crearla)
fecha_hoy = Date.today.to_s
bitacora = db.find_bitacora_by_fecha(fecha_hoy)
# Resolver ámbito si se especificó
ambito_id = nil
if opts[:ambito]
ambito = db.get_ambito(opts[:ambito])
if ambito
ambito_id = ambito['id']
puts " #{Color::DIM}Ámbito encontrado: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}"
else
puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}"
end
end
# Determinar fecha: --fecha o hoy
fecha_target = opts[:fecha] ? opts[:fecha] : Date.today.to_s
if opts[:fecha]
begin
Date.parse(opts[:fecha])
puts " #{Color::DIM} Fecha especificada: #{fecha_target}#{Color::RESET}"
rescue Date::Error
puts "#{Color::RED}✗ Formato de fecha inválido: #{opts[:fecha]}. Usar YYYY-MM-DD#{Color::RESET}"
exit 1
end
end
bitacora = db.find_bitacora_by_fecha(fecha_target)
if bitacora.nil?
puts " #{Color::DIM}Creando bitácora para hoy...#{Color::RESET}"
bitacora = db.create_bitacora(fecha: fecha_hoy)
puts " #{Color::DIM}Creando bitácora para #{fecha_target}...#{Color::RESET}"
bitacora = db.create_bitacora(fecha: fecha_target)
end
bitacora_id = bitacora['id']
# Obtener modo desde jornada actual si no se especificó
if modo_resuelto.nil?
entradas = db.list_entradas(bitacora_id: bitacora_id, limit: 10)
jornada = entradas.find { |e| e['descripcion']&.downcase&.include?('inicio de jornada') }
if jornada && jornada['modo']
modo_resuelto = jornada['modo']
puts " #{Color::DIM} Modo resuelto desde jornada: #{modo_resuelto == 'P' ? 'Presencial' : 'Remoto'}#{Color::RESET}"
if bitacora && bitacora['modo']
modo_resuelto = bitacora['modo']
puts " #{Color::DIM} Modo resuelto desde jornada: #{modo_resuelto == 'P' ? 'Presencial' : (modo_resuelto == 'R' ? 'Remoto' : 'Servicio')}#{Color::RESET}"
else
modo_resuelto = 'P' # default
puts " #{Color::DIM} Sin jornada activa, usando default: Presencial#{Color::RESET}"
@@ -128,7 +184,20 @@ module ADN
tema_id: nil # Por ahora sin tema
)
# Asignar doc_path si se especificó
if opts[:doc_path] && entrada
db.update_entrada(entrada['id'], { doc_path: opts[:doc_path] })
puts " #{Color::DIM} Doc vinculado: #{opts[:doc_path]}#{Color::RESET}"
end
# Asignar ámbito si se especificó
if ambito_id && entrada
db.update_entrada(entrada['id'], { ambito_id: ambito_id })
puts " #{Color::DIM} Ámbito asignado (ID: #{ambito_id})#{Color::RESET}"
end
puts "#{Color::GREEN}✅ Entrada creada exitosamente#{Color::RESET}"
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha_target}"
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']}"
@@ -141,7 +210,7 @@ module ADN
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\", --nodo \"Nombre\""
puts "Opciones soportadas: --fin \"HH:MM[:SS]\", --inicio \"HH:MM[:SS]\", --estado \"\", --descripcion \"Texto\", --nodo \"Nombre\""
return
end
@@ -150,12 +219,14 @@ module ADN
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("--inicio HH:MM[:SS]", "Nueva hora de inicio") { |i| opts[:inicio] = i }
opt.on("--fin HH:MM[:SS]", "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("--modo MODO", "Modo (P=Presencial, R=Remoto)") { |m| opts[:modo] = m }
opt.on("--nodo NOMBRE", "Nuevo nodo") { |n| opts[:nodo_raw] = n }
opt.on("--ambito NOMBRE_O_ID", "Nuevo ámbito") { |a| opts[:ambito_raw] = a }
opt.on("--doc RUTA", "Ruta al plan .md vinculado") { |d| opts[:doc_path] = d }
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -199,6 +270,20 @@ module ADN
opts.delete(:nodo_raw)
end
if opts.key?(:ambito_raw)
if opts[:ambito_raw].nil? || opts[:ambito_raw].empty?
opts[:ambito_id] = nil
else
ambito = db.get_ambito(opts[:ambito_raw])
if ambito
opts[:ambito_id] = ambito['id']
else
puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito_raw]}' no encontrado#{Color::RESET}"
end
end
opts.delete(:ambito_raw)
end
begin
res = db.update_entrada(id_evento, opts)
if res
@@ -232,6 +317,9 @@ module ADN
opt.on("--nodo NOMBRE", "Filtrar por nombre de nodo") do |n|
opts[:nodo] = n
end
opt.on("--ambito NOMBRE_O_ID", "Filtrar por ámbito") do |a|
opts[:ambito] = a
end
opt.on("--estado ESTADO", "Filtrar por estado (⏳, ✅, ❌, ⚠️, ⏸️)") do |e|
opts[:estado] = e
end
@@ -276,7 +364,20 @@ module ADN
end
end
# Obtener ID de ámbito si se especificó nombre
ambito_id = nil
if opts[:ambito]
ambito = db.get_ambito(opts[:ambito])
if ambito
ambito_id = ambito['id']
puts " #{Color::DIM}Filtrando por ámbito: #{opts[:ambito]} (ID: #{ambito_id})#{Color::RESET}"
else
puts "#{Color::YELLOW}⚠ Ámbito '#{opts[:ambito]}' no encontrado#{Color::RESET}"
end
end
entradas = db.list_entradas(
ambito_id: ambito_id,
nodo_id: nodo_id,
estado: opts[:estado],
buscar: opts[:buscar],
@@ -296,9 +397,9 @@ module ADN
entradas.each_with_index do |e, i|
puts "#{Color::DIM}#{"" * 60}#{Color::RESET}" if i > 0
id = e['id'].to_s
inicio = e['inicio'][0..7]
fin = e['fin'] ? e['fin'][0..7] : '--:--'
nodo = e['nodo_nombre'] || '(sin nodo)'
inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--'
fin = e['fin'] ? e['fin'][0..7] : '--:--:--'
nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}"
estado = e['estado']
desc = e['descripcion']
@@ -306,22 +407,23 @@ module ADN
puts " #{desc}"
end
else
# Formato tabla compacta (original)
# Formato tabla compacta (original ajustado)
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}"
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(9)} #{"Fin".ljust(9)} #{"Ámbito/Nodo".ljust(25)} #{"Est".ljust(3)} #{"Descripción".ljust(35)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 9} #{"" * 9} #{"" * 25} #{"" * 3} #{"" * 35}"
entradas.each do |e|
id = e['id'].to_s
inicio = e['inicio'][0..7] # HH:MM:SS -> HH:MM
inicio = e['inicio'] ? e['inicio'][0..7] : '--:--'
fin = e['fin'] ? e['fin'][0..7] : '--:--'
nodo = e['nodo_nombre'] || '(sin nodo)'
nodo = nodo[0..13] + '...' if nodo.length > 15
nodo = "#{e['ambito_nombre'] || '-'} / #{e['nodo_nombre'] || '-'}"
nodo = nodo[0..23] + '...' if nodo.length > 25
estado = e['estado']
desc = e['descripcion']
desc = desc[0..37] + '...' if desc.length > 40
desc = e['descripcion'] || ''
desc = desc.split("\n").first || '' # Tomar solo la primera línea
desc = desc[0..32] + '...' if desc.length > 35
puts "#{id.ljust(5)} #{inicio.ljust(8)} #{fin.ljust(8)} #{nodo.ljust(15)} #{estado.ljust(3)} #{desc.ljust(40)}"
puts "#{id.ljust(5)} #{inicio.ljust(9)} #{fin.ljust(9)} #{nodo.ljust(25)} #{estado.ljust(3)} #{desc.ljust(35)}"
end
end
@@ -397,13 +499,13 @@ module ADN
entradas.each do |e|
id = e['id'].to_s
inicio = e['inicio'][0..4] # HH:MM:SS -> HH:MM
inicio = e['inicio'] ? e['inicio'][0..7] : '--:--:--' # HH:MM:SS
nodo = (e['nodo_nombre'] || '(sin nodo)')[0..14]
desc = e['descripcion']
desc = desc[0..47] + '...' if desc.length > 50
estado_icon = e['estado']
puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(5)} #{nodo.ljust(15)} #{desc}"
puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(11)} #{nodo.ljust(15)} #{desc}"
end
end
+39 -3
View File
@@ -86,8 +86,9 @@ module ADN
puts " #{Color::DIM}Entradas completadas:#{Color::RESET} #{porcentaje_completadas}%"
end
end
end
def comando_backup(args)
def comando_backup(args)
opts = { output: nil, compress: true, timestamp: true }
parser = OptionParser.new do |opt|
@@ -185,10 +186,45 @@ module ADN
end
end
def comando_migrar_sql(args)
opts = {}
parser = OptionParser.new do |opt|
opt.banner = "Uso: adn db migrar:sql ARCHIVO_SQL"
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
sql_file = args.first
if sql_file.nil? || !File.exist?(sql_file)
puts "#{Color::RED}✗ Error: Archivo SQL no especificado o no encontrado.#{Color::RESET}"
puts parser
exit 1
end
puts "#{Color::CYAN}🔄 Ejecutando script SQL: #{sql_file}...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
begin
sql = File.read(sql_file)
db.send(:execute, sql)
puts "#{Color::GREEN}✅ Script SQL ejecutado exitosamente.#{Color::RESET}"
rescue => e
puts "#{Color::RED}✗ Error ejecutando SQL: #{e.message}#{Color::RESET}"
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'
+121 -5
View File
@@ -21,6 +21,9 @@ module ADN
opt.on("--inactivo", "Crear nodo como inactivo") do
opts[:activo] = false
end
opt.on("--ambito NOMBRE_O_ID", "Ámbito Padre (Opcional)") do |a|
opts[:ambito] = a
end
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -45,18 +48,30 @@ module ADN
BitacorasDB::BitacoraDB.with_connection do |db|
begin
ambito_id = nil
if opts[:ambito]
ambito = db.get_ambito(opts[:ambito])
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}"
exit 1
end
ambito_id = ambito['id']
end
nodo = db.create_nodo(
nombre: opts[:nombre],
ip: opts[:ip],
tipo: opts[:tipo],
descripcion: opts[:descripcion],
activo: opts[:activo]
activo: opts[:activo],
ambito_id: ambito_id
)
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}Ámbito:#{Color::RESET} #{opts[:ambito] ? ambito['nombre'] : '(sin ámbito)'}"
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'}"
@@ -86,6 +101,9 @@ module ADN
opt.on("--tipo TIPO", "Filtrar por tipo (servidor, hipervisor, vm, pc)") do |t|
opts[:tipo] = t
end
opt.on("--ambito NOMBRE_O_ID", "Filtrar por ámbito") do |a|
opts[:ambito] = a
end
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -103,7 +121,18 @@ module ADN
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)
ambito_id = nil
if opts[:ambito]
ambito = db.get_ambito(opts[:ambito])
if ambito
ambito_id = ambito['id']
else
puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}"
exit 1
end
end
nodos = db.list_nodos(activo: opts[:activo], tipo: opts[:tipo], ambito_id: ambito_id, limit: 100)
if nodos.empty?
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{Color::RESET}"
@@ -111,19 +140,21 @@ module ADN
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}"
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Nombre".ljust(20)} #{"Ámbito".ljust(15)} #{"IP".ljust(15)} #{"Tipo".ljust(12)} #{"Activo".ljust(6)} #{"Descripción".ljust(30)}#{Color::RESET}"
puts "#{"" * 5} #{"" * 20} #{"" * 15} #{"" * 15} #{"" * 12} #{"" * 6} #{"" * 30}"
nodos.each do |n|
id = n['id'].to_s
nombre = n['nombre']
ambito_n = (n['ambito_nombre'] || '-').to_s
ambito_n = ambito_n[0..12] + '...' if ambito_n.length > 15
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)}"
puts "#{id.ljust(5)} #{nombre.ljust(20)} #{ambito_n.ljust(15)} #{ip.ljust(15)} #{tipo.ljust(12)} #{activo.ljust(6)} #{desc.ljust(30)}"
end
puts ""
@@ -135,5 +166,90 @@ module ADN
end
end
# Comando: nodo:actualizar NOMBRE [OPCIONES] - Actualizar nodo existente
def comando_nodo_actualizar(args)
return if args.empty?
nombre = args.shift
opts = { ip: nil, tipo: nil, descripcion: nil, activo: nil, ambito: nil }
parser = OptionParser.new do |opt|
opt.banner = "Uso: adn db nodo:actualizar NOMBRE [OPCIONES]"
opt.on("--ip DIRECCION", "Nueva dirección IP") do |i|
opts[:ip] = i
end
opt.on("--tipo TIPO", "Nuevo tipo (servidor, hipervisor, vm, pc)") do |t|
opts[:tipo] = t
end
opt.on("--descripcion TEXTO", "Nueva descripción") do |d|
opts[:descripcion] = d
end
opt.on("--activo", "Marcar como activo") do
opts[:activo] = true
end
opt.on("--inactivo", "Marcar como inactivo") do
opts[:activo] = false
end
opt.on("--ambito NOMBRE_O_ID", "Nuevo ámbito") do |a|
opts[:ambito] = a
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}🔄 Actualizando nodo '#{nombre}'...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
begin
ambito_id = nil
if opts[:ambito]
ambito = db.get_ambito(opts[:ambito])
if ambito.nil?
puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado.#{Color::RESET}"
exit 1
end
ambito_id = ambito['id']
end
nodo = db.update_nodo(
nombre: nombre,
ip: opts[:ip],
tipo: opts[:tipo],
descripcion: opts[:descripcion],
activo: opts[:activo],
ambito_id: ambito_id
)
if nodo.nil?
puts "#{Color::YELLOW}⚠ No se encontró el nodo '#{nombre}'#{Color::RESET}"
exit 1
end
puts "#{Color::GREEN}✅ Nodo actualizado exitosamente#{Color::RESET}"
puts ""
puts " #{Color::BOLD}ID:#{Color::RESET} #{nodo['id']}"
puts " #{Color::BOLD}Nombre:#{Color::RESET} #{nodo['nombre']}"
puts " #{Color::BOLD}Ámbito:#{Color::RESET} #{ambito_id ? opts[:ambito] : '(sin ámbito)'}"
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}Actualizado:#{Color::RESET} #{nodo['updated_at']}"
rescue => e
puts "#{Color::RED}✗ Error actualizando nodo: #{e.message}#{Color::RESET}"
end
end
end
end
end
+33 -3
View File
@@ -29,6 +29,8 @@ class SubcomandoGenerar
generar_nodo
when 'proyecto'
generar_proyecto
when 'evento'
generar_evento
else
@logger.error("Tipo de generación desconocido: #{tipo}")
mostrar_ayuda_generar
@@ -46,13 +48,14 @@ class SubcomandoGenerar
opciones: [
{ names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" },
{ names: ['nodo <nombre>'], desc: "Crea ficha de nodo" },
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" }
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" },
{ names: ['evento <tipo>'], desc: "Crea plantilla de detalle para evento" }
],
ejemplos: [
{ cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" },
{ cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" },
{ cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" },
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" },
{ cmd: "./adn/tools/run generar evento implementacion", desc: "Ver plantilla de implementación" }
]
)
@@ -250,4 +253,31 @@ class SubcomandoGenerar
@logger.exito("Manifiesto de proyecto creado: #{codigo}")
puts "#{Color::GREEN}✓ Manifiesto creado en: #{ruta_proyecto}#{Color::RESET}"
end
def generar_evento
require_relative '../templates/plantillas_eventos'
tipo = @args[0]
if tipo.nil? || tipo.empty?
puts "#{Color::CYAN}📋 Tipos de plantillas de eventos disponibles:#{Color::RESET}"
puts ""
ADN::PlantillasEventos.lista_tipos.each do |t|
puts " #{Color::GREEN}#{t}#{Color::RESET}"
end
puts ""
puts "Uso: #{Color::DIM}./adn/tools/run generar evento <tipo>#{Color::RESET}"
puts "Ejemplo: #{Color::DIM}./adn/tools/run generar evento implementacion#{Color::RESET}"
return
end
if ADN::PlantillasEventos.existe?(tipo)
puts "#{Color::CYAN}📝 Plantilla: #{tipo}#{Color::RESET}"
puts ""
puts ADN::PlantillasEventos.obtener(tipo)
else
puts "#{Color::RED}✗ Tipo de plantilla desconocido: #{tipo}#{Color::RESET}"
puts "#{Color::YELLOW}Tipos disponibles:#{Color::RESET}"
ADN::PlantillasEventos.lista_tipos.each { |t| puts " - #{t}" }
end
end
end
+31
View File
@@ -148,6 +148,9 @@ module ADN
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}"
@logger.exito("Jornada #{tipo_str} iniciada en Base de Datos.")
end
# 4. Verificar y mostrar URL de bitácora web
verificar_y_mostrar_bitacora_web
end
def ejecutar_cierre
@@ -233,5 +236,33 @@ module ADN
puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}"
end
end
def verificar_y_mostrar_bitacora_web
@logger.info("Verificando estado de bitácora web...")
# Verificar si los servicios Docker están activos
begin
output = `docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml ps 2>&1`
# Docker muestra "Up" cuando los servicios están activos
servicios_activos = output.include?('Up')
if servicios_activos
puts ""
puts "#{Color::CYAN}🌐 Bitácora Web disponible:#{Color::RESET}"
puts " #{Color::GREEN}✓ Dashboard local:#{Color::RESET} http://localhost:5174"
puts " #{Color::GREEN}✓ Acceso remoto:#{Color::RESET} http://srv-ns8/bitacoras"
@logger.exito("Bitácora web verificada y disponible")
else
puts ""
puts "#{Color::YELLOW}⚠ Bitácora Web:#{Color::RESET}"
puts " Servicios Docker no están activos. Ejecutar:"
puts " #{Color::DIM}docker compose -f servicios/nginx/dtic-bitacoras/docker-compose.yml up -d#{Color::RESET}"
@logger.warn("Bitácora web: servicios no activos")
end
rescue StandardError => e
@logger.error("Error verificando bitácora web: #{e.message}")
puts "#{Color::RED}✗ Error verificando servicios: #{e.message}#{Color::RESET}"
end
end
end
end
+74 -7
View File
@@ -54,14 +54,14 @@ module ADN
descripcion: "Gestión y consulta de nodos de infraestructura",
uso: "./adn/tools/run nodos <acción> [opciones]",
opciones: [
{ names: ['agrupar [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" },
{ names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" },
{ names: ['agrupar [--por ambitos|servidor]'], desc: "Agrupar nodos de la red" },
{ names: ['listar [--tipo TIPO] [--ambito AMB]'], desc: "Listar fichas de nodos (.md)" },
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
],
ejemplos: [
{ cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" },
{ cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" },
{ cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" },
{ cmd: "./adn/tools/run nodos agrupar --por ambitos", desc: "Agrupar nodos estructurados desde la DB" },
{ cmd: "./adn/tools/run nodos agrupar --por servidor --servidor srv-pmox1", desc: "Agrupar VMs del servidor en base a fichas" },
{ cmd: "./adn/tools/run nodos listar --ambito dtic-DASUTEN", desc: "Listar nodos MD cruzados con ámbito DB" },
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
]
)
@@ -110,12 +110,59 @@ module ADN
# ─── Agrupar ────────────────────────────────────────────────────────
def agrupar_nodos
opts = { servidor: nil }
opts = { servidor: nil, por: 'servidor' }
parser = OptionParser.new do |opt|
opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s }
opt.on('--por CAMPO', 'Agrupar por: servidor, ambitos') { |c| opts[:por] = c }
end
parser.order!(@args)
if opts[:por] == 'ambitos'
agrupar_por_ambitos
else
agrupar_por_servidor(opts)
end
end
def agrupar_por_ambitos
puts "#{Color::CYAN}📋 Agrupando nodos por Ámbitos (Desde DB)#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
require_relative '../db/core/bitacora_db'
ambitos_hash = Hash.new { |h, k| h[k] = [] }
sin_ambito = []
BitacorasDB::BitacoraDB.with_connection do |db|
nodos = db.list_nodos(limit: 1000)
nodos.each do |n|
if n['ambito_nombre']
ambitos_hash[n['ambito_nombre']] << n['nombre']
else
sin_ambito << n['nombre']
end
end
end
ambitos_hash.keys.sort.each do |amb|
vms = ambitos_hash[amb]
puts "#{Color::GREEN}🏢 #{amb}#{Color::RESET} (#{vms.length} nodos)"
vms.sort.each { |vm| puts " ├── 💻 #{vm}" }
puts ""
end
if sin_ambito.any?
puts "#{Color::YELLOW}❓ Nodos sin ámbito asignado:#{Color::RESET}"
sin_ambito.sort.each { |nodo| puts " ├── 💻 #{nodo}" }
puts ""
end
total_vms = ambitos_hash.values.flatten.length + sin_ambito.length
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Total: #{ambitos_hash.keys.length} ámbitos, #{total_vms} nodos#{Color::RESET}"
end
def agrupar_por_servidor(opts)
puts "#{Color::CYAN}📋 Agrupando nodos por Servidor Host#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
@@ -173,14 +220,30 @@ module ADN
# ─── Listar ─────────────────────────────────────────────────────────
def listar_nodos
opts = { tipo: nil }
opts = { tipo: nil, ambito: nil }
parser = OptionParser.new do |opt|
opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t }
opt.on('--ambito NOMBRE', 'Cruzar nombres de base de datos para filtrar') { |a| opts[:ambito] = a }
end
parser.order!(@args)
puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
nodos_validos = nil
if opts[:ambito]
require_relative '../db/core/bitacora_db'
BitacorasDB::BitacoraDB.with_connection do |db|
ambito = db.get_ambito(opts[:ambito])
if ambito
nodos_db = db.list_nodos(ambito_id: ambito['id'], limit: 1000)
nodos_validos = nodos_db.map { |n| n['nombre'] }
else
puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado en la DB.#{Color::RESET}"
return
end
end
end
archivos = Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort
nodos = archivos.map { |a| File.basename(a, '.md') }
@@ -189,6 +252,10 @@ module ADN
if opts[:tipo]
nodos.select! { |n| n.start_with?(opts[:tipo]) }
end
if nodos_validos
nodos.select! { |n| nodos_validos.include?(n) }
end
if nodos.empty?
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{opts[:tipo] ? " con prefijo '#{opts[:tipo]}'" : ''}#{Color::RESET}"
+87 -36
View File
@@ -14,38 +14,41 @@ module ADN
end
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run salud [OPCIONES]"
opts.separator ""
opts.separator "Muestra salud general del sistema ADN"
opts.separator ""
opts.separator "Opciones:"
opts.on("--dashboard", "Mostrar en formato dashboard (no implementado)") do
@opciones[:dashboard] = true
end
opts.on("--json", "Salida en formato JSON") do
@opciones[:json] = true
end
opts.on("--mejoras", "Incluir sugerencias de mejora continua") do
@opciones[:mejoras] = true
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit 1
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
opts = { dashboard: false, json: false, mejoras: false, ambito: nil }
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
if @args.include?('-h') || @args.include?('--help') || @args.empty?
require_relative '../core/help_formatter'
puts ADN::HelpFormatter.generar_help(
titulo: "Salud del Sistema ADN",
descripcion: "Muestra métricas y estado general del ecosistema (Archivos MD + DB)",
uso: "./adn/tools/run salud [opciones]",
opciones: [
{ names: ['--json'], desc: "Salida en formato JSON" },
{ names: ['--mejoras'], desc: "Sugerencias de mejora continua" },
{ names: ['--ambito AMBITO'], desc: "Filtrar estadísticas de DB por ámbito" }
],
ejemplos: [
{ cmd: "./adn/tools/run salud", desc: "Reporte general" },
{ cmd: "./adn/tools/run salud --ambito dtic-DASUTEN", desc: "Reporte con foco en el ámbito" }
]
)
exit 0
end
parser = OptionParser.new do |opt|
opt.on("--json") { @opciones[:json] = true }
opt.on("--mejoras") { @opciones[:mejoras] = true }
opt.on("--dashboard") { @opciones[:dashboard] = true }
opt.on("--ambito NOMBRE") { |a| @opciones[:ambito] = a }
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts "#{Color::YELLOW}⚠ Error: #{e.message}#{Color::RESET}"
exit 1
end
mostrar_salud
end
@@ -69,23 +72,34 @@ module ADN
datos[:sugerencias_mejora] = obtener_sugerencias_mejora
end
datos[:db] = obtener_estadisticas_db(@opciones[:ambito])
if @opciones[:json]
puts JSON.pretty_generate(datos)
return
end
if @opciones[:dashboard]
puts "#{Color::YELLOW}⚠ El dashboard será implementado en Fase 3#{Color::RESET}"
# TODO: Implementar dashboard en Fase 3
puts "#{Color::YELLOW}⚠ El dashboard interactivo estructurado será implementado en Fase 3#{Color::RESET}"
end
# Mostrar en formato legible
puts "#{Color::CYAN}🧬 Salud del Sistema ADN#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts " #{Color::GREEN}Hebras ADN:#{Color::RESET} #{datos[:hebras_adn]} / 7"
puts " #{Color::GREEN}Bitácoras:#{Color::RESET} #{datos[:bitacoras]}"
puts " #{Color::GREEN}Nodos registrados:#{Color::RESET} #{datos[:nodos]}"
puts " #{Color::GREEN}Proyectos activos:#{Color::RESET} #{datos[:proyectos]}"
puts " #{Color::GREEN}Bitácoras MD:#{Color::RESET} #{datos[:bitacoras]}"
puts " #{Color::GREEN}Nodos MD:#{Color::RESET} #{datos[:nodos]}"
puts " #{Color::GREEN}Proyectos MD:#{Color::RESET} #{datos[:proyectos]}"
if datos[:db]
puts ""
db_stats = datos[:db]
ambito_str = @opciones[:ambito] ? " (Ámbito: #{@opciones[:ambito]})" : " (Global)"
puts " #{Color::CYAN}Estadísticas Base de Datos#{ambito_str}:#{Color::RESET}"
puts " - Ámbitos Totales: #{db_stats[:ambitos]}" unless @opciones[:ambito]
puts " - Nodos (DB): #{db_stats[:nodos]}"
puts " - Eventos (DB): #{db_stats[:eventos]}"
end
if datos[:errores_recientes] > 0
puts " #{Color::RED}Errores recientes:#{Color::RESET} #{datos[:errores_recientes]}"
@@ -96,6 +110,19 @@ module ADN
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Para más detalles: ./adn/tools/run salud --json#{Color::RESET}"
if @opciones[:mejoras] && datos[:sugerencias_mejora] && !datos[:sugerencias_mejora].empty?
puts
puts "#{Color::CYAN}💡 Sugerencias de Mejora Continua#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
datos[:sugerencias_mejora].each do |sug|
prioridad_emoji = { alta: '🔴', media: '🟡', baja: '🟢' }[sug[:prioridad]] || '⚪'
puts " #{prioridad_emoji} #{sug[:tipo].to_s.tr('_', ' ')}"
puts " #{sug[:descripcion]}"
puts "#{sug[:accion_sugerida]}"
puts
end
end
@logger.exito("Reporte de salud generado")
end
@@ -120,6 +147,30 @@ module ADN
Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).count
end
def obtener_estadisticas_db(ambito_nombre = nil)
require_relative '../db/core/bitacora_db'
stats = { ambitos: 0, nodos: 0, eventos: 0 }
begin
BitacorasDB::BitacoraDB.with_connection do |db|
ambito_id = nil
if ambito_nombre
ambito = db.get_ambito(ambito_nombre)
return nil unless ambito
ambito_id = ambito['id']
else
stats[:ambitos] = db.list_ambitos.length
end
stats[:nodos] = db.list_nodos(ambito_id: ambito_id, limit: 1000).length
stats[:eventos] = db.list_entradas(ambito_id: ambito_id, limit: 10000).length
end
rescue StandardError => e
@logger.error("Error obteniendo stats DB: #{e.message}")
return nil
end
stats
end
def contar_proyectos
Dir.glob(File.join(ADN::PROYECTOS_DIR, "*.md")).count
end