Files
dtic-DIIAA/tools/orquestador/lib/hitos/manager.rb
T

316 lines
8.3 KiB
Ruby

#!/usr/bin/env ruby
# frozen_string_literal: true
# tools/orquestador/lib/hitos/manager.rb
# ======================================
# Clase para manejo centralizado de hitos del dashboard P2601
# Filosofía: "Menos es más" - código reutilizable y modular
require_relative '../dashboard_core'
module DashboardCore
class HitosManager < Base
attr_reader :hitos_cache
def initialize(dashboard_path = nil)
super(dashboard_path)
@hitos_cache = nil
end
# Métodos principales para gestión de hitos
def cargar_hitos(force_reload = false)
return @hitos_cache if @hitos_cache && !force_reload
contenido = leer_dashboard
@hitos_cache = extraer_hitos(contenido)
@hitos_cache
end
def buscar_hito_por_id(id_hito)
hitos = cargar_hitos
hitos.find { |h| h[:id] == id_hito }
end
def buscar_hitos_por_fase(numero_fase)
hitos = cargar_hitos
hitos.select do |hito|
info = HitoIdGenerator.parsear_id(hito[:id])
info && info[:fase] == numero_fase
end
end
def buscar_hitos_por_estado(estado)
hitos = cargar_hitos
hitos.select { |h| h[:estado] == estado }
end
def buscar_hitos_por_tag(tag)
hitos = cargar_hitos
hitos.select { |h| h[:tags].include?(tag) }
end
def agregar_hito(hito_data)
# Validar datos del hito
unless hito_data[:id] && hito_data[:titulo]
raise "ID y título son requeridos para agregar un hito"
end
# Validar formato del ID
unless HitoIdGenerator.es_valido?(hito_data[:id])
raise "ID de hito inválido. Formato esperado: fNNNNN (ej: f05001)"
end
# Verificar que no exista ya
if buscar_hito_por_id(hito_data[:id])
raise "El hito con ID #{hito_data[:id]} ya existe"
end
# Crear backup
crear_backup('agregar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Generar HTML del hito
hito_html = generar_html_hito(hito_data)
# Insertar en la timeline
nuevo_contenido = insertar_hitos_en_timeline(contenido, hito_html, 'append')
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{hito_data[:id]} agregado exitosamente"
true
end
def actualizar_hito(id_hito, cambios)
# Verificar que el hito exista
hito_existente = buscar_hito_por_id(id_hito)
unless hito_existente
raise "Hito #{id_hito} no encontrado"
end
# Validar cambios permitidos
cambios_permitidos = [:estado, :fecha, :titulo, :descripcion, :tags]
cambios.each_key do |campo|
unless cambios_permitidos.include?(campo)
raise "Campo '#{campo}' no es modificable"
end
end
# Validar estado si se está actualizando
if cambios[:estado] && !Validator.validar_estado(cambios[:estado])
raise "Estado '#{cambios[:estado]}' no válido"
end
# Crear backup
crear_backup('actualizar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Actualizar hito en el contenido
nuevo_estado = cambios[:estado] || hito_existente[:estado]
nueva_fecha = cambios[:fecha] || Time.now.strftime('%Y-%m-%d')
nuevo_contenido, encontrado = actualizar_hito_en_contenido(
contenido, id_hito, nuevo_estado, nueva_fecha
)
unless encontrado
raise "No se pudo encontrar el hito #{id_hito} para actualizar"
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{id_hito} actualizado exitosamente"
true
end
def eliminar_hito(id_hito)
# Verificar que el hito exista
unless buscar_hito_por_id(id_hito)
raise "Hito #{id_hito} no encontrado"
end
# Crear backup
crear_backup('eliminar_hito')
# Leer contenido actual
contenido = leer_dashboard
# Patrón para encontrar y eliminar el hito específico
patron_hito = /<div class="tl-item">[\s\S]*?<span class="tl-id">#{Regexp.escape(id_hito)}<\/span>[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/
nuevo_contenido = contenido.gsub(patron_hito, '')
# Verificar que se eliminó algo
if nuevo_contenido == contenido
raise "No se pudo eliminar el hito #{id_hito}"
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ Hito #{id_hito} eliminado exitosamente"
true
end
def agregar_hitos_fase(fase_nombre, modo = 'append')
# Validar fase
unless Validator.validar_fase(fase_nombre)
raise "Fase '#{fase_nombre}' no válida. Fases disponibles: #{FaseManager.fases_disponibles.join(', ')}"
end
# Validar modo
unless Validator.validar_modo_insercion(modo)
raise "Modo '#{modo}' no válido. Use 'append' o 'replace'."
end
# Crear backup
crear_backup("agregar_#{fase_nombre}")
# Leer contenido actual
contenido = leer_dashboard
# Generar HTML de hitos de la fase
hitos_html = FaseManager.generar_hitos_html(fase_nombre)
# Insertar en la timeline
nuevo_contenido = insertar_hitos_en_timeline(contenido, hitos_html, modo)
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
hitos_agregados = cargar_hitos(true)
# Filtrar hitos de esta fase
numero_fase = FaseManager.extraer_numero_fase(fase_nombre)
hitos_fase = buscar_hitos_por_fase(numero_fase)
puts "✅ #{hitos_fase.size} hitos de #{fase_nombre} agregados exitosamente"
hitos_fase
end
# Métodos de consulta y reporte
def estadisticas
hitos = cargar_hitos
{
total: hitos.size,
por_estado: contar_por_estado(hitos),
por_fase: contar_por_fase(hitos),
por_tag: contar_por_tag(hitos),
ultima_actualizacion: Time.now.strftime('%Y-%m-%d %H:%M:%S')
}
end
def reporte_detallado
hitos = cargar_hitos
Reporter.generar_reporte_hitos(hitos)
end
def verificar_duplicados
contenido = leer_dashboard
duplicados = buscar_hitos_duplicados(contenido)
if duplicados.empty?
"✅ No se encontraron hitos duplicados"
else
reporte = "⚠️ HITOS DUPLICADOS ENCONTRADOS:\n"
reporte += "=" * 40 + "\n"
duplicados.each do |id|
reporte += "• #{id}\n"
end
reporte += "\nTotal: #{duplicados.size} duplicados"
reporte
end
end
def limpiar_duplicados
contenido = leer_dashboard
duplicados = buscar_hitos_duplicados(contenido)
if duplicados.empty?
puts "✅ No hay hitos duplicados para limpiar"
return true
end
puts "🧹 Limpiando #{duplicados.size} hitos duplicados..."
# Crear backup
crear_backup('limpiar_duplicados')
# Extraer todos los hitos únicos
hitos_unicos = extraer_hitos(contenido)
# Reconstruir timeline con hitos únicos
timeline_html = hitos_unicos.map { |h| generar_html_hito(h) }.join("\n\n")
# Reemplazar la timeline completa
timeline_pattern = /<div class="timeline">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/m
nuevo_contenido = contenido.gsub(timeline_pattern) do |match|
<<~HTML
<div class="timeline">
#{timeline_html}
</div>
HTML
end
# Guardar cambios
guardar_dashboard(nuevo_contenido)
# Actualizar cache
@hitos_cache = nil
cargar_hitos(true)
puts "✅ #{duplicados.size} hitos duplicados eliminados"
true
end
private
def contar_por_estado(hitos)
conteo = Hash.new(0)
hitos.each { |h| conteo[h[:estado]] += 1 }
conteo
end
def contar_por_fase(hitos)
conteo = Hash.new(0)
hitos.each do |hito|
if info = HitoIdGenerator.parsear_id(hito[:id])
conteo[info[:fase]] += 1
end
end
conteo
end
def contar_por_tag(hitos)
conteo = Hash.new(0)
hitos.each do |hito|
hito[:tags].each { |tag| conteo[tag] += 1 }
end
conteo
end
end
end