Reestructuración modular integral de servicios, reorganización de gestión en docs/ y limpieza profunda del repositorio
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user