Refactor(ADN): Transición absoluta a DB-First, extirpación de MDExporter e inferencia de auto-modo

This commit is contained in:
Ricardo Monla
2026-03-10 16:00:33 -03:00
parent af5f193fab
commit 8aa1532396
4 changed files with 545 additions and 153 deletions
+157 -3
View File
@@ -46,7 +46,7 @@ module BitacorasDB
ESTADOS_VALIDOS = ['⏳', '✅', '❌', '⚠️', '⏸️'].freeze
# Modos válidos para entradas
MODOS_VALIDOS = ['P', 'R'].freeze # P=Presencial, R=Remoto
MODOS_VALIDOS = ['P', 'R', 'S'].freeze # P=Presencial, R=Remoto, S=Servicio
# Inicializa la conexión a la base de datos
#
@@ -281,7 +281,7 @@ module BitacorasDB
# @param activo [Boolean] Si el nodo está activo
# @return [Hash] Nodo creado con ID
def create_nodo(nombre:, ip: nil, tipo: 'servidor', descripcion: nil, activo: true)
tipos_validos = ['servidor', 'hipervisor', 'vm', 'pc', 'vmware', 'xenserver']
tipos_validos = ['servidor', 'hipervisor', 'vm', 'pc', 'vmware', 'xenserver', 'servicio']
unless tipos_validos.include?(tipo.downcase)
raise ArgumentError, "Tipo de nodo inválido. Válidos: #{tipos_validos.join(', ')}"
@@ -365,13 +365,28 @@ module BitacorasDB
# @param bitacora_id [Integer, nil] ID de la bitácora
# @param nodo_id [Integer, nil] ID del nodo
# @return [Hash] Entrada creada con ID
def create_entrada(inicio:, descripcion:, fin: nil, estado: '⏳', modo: 'P', es_ia: false,
def create_entrada(inicio:, descripcion:, fin: nil, estado: nil, modo: nil, es_ia: false,
tema_id: nil, bitacora_id: nil, nodo_id: nil)
# Validar formato de tiempo
inicio_time = parse_time(inicio)
fin_time = parse_time(fin) if fin
# Inferir Estado
if estado.nil? || estado.strip.empty?
estado = fin_time ? '✅' : '⏳'
end
# Inferir Modo
if modo.nil? || modo.strip.empty?
if bitacora_id
res = @connection.exec_params("SELECT modo FROM #{SCHEMA}.#{TABLES[:entradas]} WHERE bitacora_id = $1 ORDER BY id DESC LIMIT 1", [bitacora_id])
modo = res.ntuples > 0 ? res[0]['modo'] : 'P'
else
modo = 'P'
end
end
# Validar estado
unless ESTADOS_VALIDOS.include?(estado)
raise ArgumentError, "Estado inválido. Válidos: #{ESTADOS_VALIDOS.join(', ')}"
@@ -400,6 +415,44 @@ module BitacorasDB
result.first
end
# Actualiza una entrada existente
def update_entrada(id, params)
# Auto-inferir estado si se está mandando una hora de fin y no se fuerza un estado explícito
if params.key?(:fin) && !params.key?(:estado)
if params[:fin].nil? || params[:fin].to_s.strip.empty?
params[:estado] = '⏳'
else
params[:estado] = '✅'
end
end
set_clauses = []
values = []
i = 1
params.each do |k, v|
if [:fin, :descripcion, :estado, :modo, :es_ia].include?(k)
val = (k == :fin) ? parse_time(v) : v
set_clauses << "#{k} = $#{i}"
values << val
i += 1
end
end
return nil if set_clauses.empty?
query = <<~SQL
UPDATE #{SCHEMA}.#{TABLES[:entradas]}
SET #{set_clauses.join(', ')}, updated_at = CURRENT_TIMESTAMP
WHERE id = $#{i}
RETURNING *
SQL
values << id
result = @connection.exec_params(query, values)
result.first&.to_h
end
# Lista entradas con filtros avanzados
#
# @param bitacora_id [Integer, nil] Filtrar por bitácora
@@ -630,6 +683,107 @@ module BitacorasDB
create_bitacora(fecha: Date.today, resumen: resumen)
end
# ==========================================
# PROYECTOS, FASES E HITOS (Planes Sistemáticos)
# ==========================================
def find_proyecto_by_codigo(codigo)
q = "SELECT * FROM bitacoras.proyectos WHERE codigo = $1"
res = execute(q, [codigo.upcase])
return nil if res.ntuples.zero?
res.first.to_h
end
def list_hitos(proyecto_codigo)
proyecto = find_proyecto_by_codigo(proyecto_codigo)
raise StandardError, "Proyecto no encontrado: #{proyecto_codigo}" unless proyecto
q = <<~SQL
SELECT f.numero as fase_num, f.nombre as fase_nombre,
h.id, h.id_hito, h.titulo, h.estado, h.horas_presencial, h.horas_remoto
FROM bitacoras.fases f
JOIN bitacoras.hitos h ON h.fase_id = f.id
WHERE f.proyecto_id = $1
ORDER BY f.numero, h.id_hito
SQL
res = execute(q, [proyecto['id']])
res.map(&:to_h)
end
def rename_hito_id(proyecto_codigo, id_actual, id_nuevo)
q_busqueda = <<~SQL
SELECT h.id, h.titulo
FROM bitacoras.hitos h
JOIN bitacoras.fases f ON h.fase_id = f.id
JOIN bitacoras.proyectos p ON f.proyecto_id = p.id
WHERE p.codigo = $1 AND h.id_hito = $2
SQL
res = execute(q_busqueda, [proyecto_codigo.upcase, id_actual])
raise StandardError, "Hito #{id_actual} del proyecto #{proyecto_codigo} no encontrado" if res.ntuples.zero?
hito = res.first
q_update = "UPDATE bitacoras.hitos SET id_hito = $1 WHERE id = $2 RETURNING titulo"
res_update = execute(q_update, [id_nuevo, hito['id']])
res_update.first
end
def update_hito(proyecto_codigo, id_hito, estado: nil, horas_presencial: nil, horas_remoto: nil, descripcion: nil)
q_busqueda = <<~SQL
SELECT h.id, h.horas_presencial, h.horas_remoto, h.estado, h.descripcion
FROM bitacoras.hitos h
JOIN bitacoras.fases f ON h.fase_id = f.id
JOIN bitacoras.proyectos p ON f.proyecto_id = p.id
WHERE p.codigo = $1 AND h.id_hito = $2
SQL
res = execute(q_busqueda, [proyecto_codigo.upcase, id_hito])
raise StandardError, "Hito #{id_hito} del proyecto #{proyecto_codigo} no encontrado" if res.ntuples.zero?
hito = res.first
updates = []
params = []
param_count = 1
if estado
updates << "estado = $#{param_count}"
params << estado
param_count += 1
end
unless horas_presencial.nil?
updates << "horas_presencial = $#{param_count}"
params << horas_presencial
param_count += 1
end
unless horas_remoto.nil?
updates << "horas_remoto = $#{param_count}"
params << horas_remoto
param_count += 1
end
if descripcion
updates << "descripcion = $#{param_count}"
params << descripcion
param_count += 1
end
return hito if updates.empty?
updates << "updated_at = NOW()"
params << hito['id']
query = "UPDATE bitacoras.hitos SET #{updates.join(', ')} WHERE id = $#{param_count} RETURNING *"
resultado = execute(query, params)
resultado.first.to_h
end
private
# Ruta por defecto al archivo de configuración