Feat(ADN): Implementado y ejecutado archivar:md para mover Markdowns obsoletos a histórico DB-First
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/bitacoras-db/cli/exportar_todo.rb
|
||||
# ==========================================
|
||||
# Exporta TODAS las bitácoras de la DB a archivos .md
|
||||
# Uso: ruby tools/bitacoras-db/cli/exportar_todo.rb [--dir DIRECTORIO]
|
||||
# — o vía enjambre —
|
||||
# ./tools/adn/run db exportar:md --todo
|
||||
|
||||
require 'date'
|
||||
require 'optparse'
|
||||
require_relative '../core/bitacora_db'
|
||||
require_relative '../exporters/md_exporter'
|
||||
|
||||
module BitacorasDB
|
||||
module CLI
|
||||
class ExportarTodo
|
||||
def initialize(bitacoras_dir, logger = nil)
|
||||
@bitacoras_dir = bitacoras_dir
|
||||
@logger = logger || default_logger
|
||||
@resultados = { ok: 0, error: 0 }
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
@logger.info("Exportando todas las bitácoras de DB a MD...")
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
bitacoras = db.list_bitacoras(limit: 500)
|
||||
|
||||
if bitacoras.empty?
|
||||
@logger.info("No hay bitácoras en la base de datos.")
|
||||
return
|
||||
end
|
||||
|
||||
exporter = BitacorasDB::Exporters::MDExporter.new(db, @logger)
|
||||
|
||||
bitacoras.each do |b|
|
||||
fecha = b['fecha'].to_s
|
||||
fecha = fecha.split('T').first if fecha.include?('T')
|
||||
archivo = File.join(@bitacoras_dir, "#{fecha}.md")
|
||||
|
||||
if exporter.exportar_bitacora(fecha, archivo)
|
||||
@resultados[:ok] += 1
|
||||
else
|
||||
@resultados[:error] += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@logger.info("📊 Exportación masiva completada:")
|
||||
@logger.info(" ✅ Exitosas: #{@resultados[:ok]}")
|
||||
@logger.info(" ❌ Errores: #{@resultados[:error]}") if @resultados[:error] > 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_logger
|
||||
Class.new do
|
||||
def info(msg); puts "[INFO] #{msg}"; end
|
||||
def error(msg); puts "[ERROR] #{msg}"; end
|
||||
def exito(msg); puts "[OK] #{msg}"; end
|
||||
end.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Ejecución directa
|
||||
if __FILE__ == $0
|
||||
bitacoras_dir = File.expand_path('../../../docs/bitacoras', __dir__)
|
||||
BitacorasDB::CLI::ExportarTodo.new(bitacoras_dir).ejecutar
|
||||
end
|
||||
@@ -0,0 +1,121 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# tools/bitacoras-db/cli/verificar_migracion.rb
|
||||
# ================================================
|
||||
# Verifica que todas las bitácoras MD existentes estén correctamente
|
||||
# migradas en la base de datos. Reporta faltantes y discrepancias.
|
||||
#
|
||||
# Uso:
|
||||
# ruby tools/bitacoras-db/cli/verificar_migracion.rb
|
||||
# — o vía enjambre —
|
||||
# ./tools/adn/run db verificar
|
||||
|
||||
require 'date'
|
||||
require_relative '../core/bitacora_db'
|
||||
|
||||
module BitacorasDB
|
||||
module CLI
|
||||
class VerificarMigracion
|
||||
COLORES = {
|
||||
reset: "\e[0m", bold: "\e[1m", red: "\e[31m",
|
||||
green: "\e[32m", yellow: "\e[33m", cyan: "\e[36m", dim: "\e[2m"
|
||||
}.freeze
|
||||
|
||||
def initialize(bitacoras_dir)
|
||||
@bitacoras_dir = bitacoras_dir
|
||||
@resultados = { ok: [], sin_entradas: [], no_migrada: [], sin_md: [] }
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
puts "#{COLORES[:cyan]}🔍 Verificación de migración MD ↔ DB#{COLORES[:reset]}\n\n"
|
||||
|
||||
archivos_md = Dir.glob(File.join(@bitacoras_dir, '*.md')).sort
|
||||
puts "#{COLORES[:dim]}Archivos MD encontrados: #{archivos_md.size}#{COLORES[:reset]}"
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# 1. Validar cada MD contra la DB
|
||||
archivos_md.each do |archivo|
|
||||
fecha = File.basename(archivo, '.md')
|
||||
bitacora = db.find_bitacora_by_fecha(fecha)
|
||||
|
||||
unless bitacora
|
||||
@resultados[:no_migrada] << fecha
|
||||
next
|
||||
end
|
||||
|
||||
entradas = db.execute(
|
||||
"SELECT COUNT(*) as total FROM bitacoras.entradas WHERE bitacora_id = $1",
|
||||
[bitacora['id']]
|
||||
)
|
||||
total = entradas.first['total'].to_i
|
||||
|
||||
if total > 0
|
||||
@resultados[:ok] << { fecha: fecha, entradas: total }
|
||||
else
|
||||
@resultados[:sin_entradas] << fecha
|
||||
end
|
||||
end
|
||||
|
||||
# 2. Bitácoras en DB que no tienen MD
|
||||
todas_db = db.list_bitacoras(limit: 500)
|
||||
fechas_md = archivos_md.map { |a| File.basename(a, '.md') }
|
||||
todas_db.each do |b|
|
||||
f = b['fecha'].to_s
|
||||
f = f.split('T').first if f.include?('T') # por si viene con timestamp
|
||||
@resultados[:sin_md] << f unless fechas_md.include?(f)
|
||||
end
|
||||
end
|
||||
|
||||
mostrar_resultados
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_resultados
|
||||
puts "\n#{COLORES[:bold]}📊 Resultado de verificación:#{COLORES[:reset]}\n"
|
||||
|
||||
# OK
|
||||
puts "#{COLORES[:green]}✅ Migradas correctamente: #{@resultados[:ok].size}#{COLORES[:reset]}"
|
||||
@resultados[:ok].each do |r|
|
||||
puts " #{COLORES[:dim]}#{r[:fecha]} (#{r[:entradas]} entradas)#{COLORES[:reset]}"
|
||||
end
|
||||
|
||||
# Sin entradas
|
||||
if @resultados[:sin_entradas].any?
|
||||
puts "\n#{COLORES[:yellow]}⚠ En DB pero sin entradas: #{@resultados[:sin_entradas].size}#{COLORES[:reset]}"
|
||||
@resultados[:sin_entradas].each { |f| puts " #{f}" }
|
||||
end
|
||||
|
||||
# No migradas
|
||||
if @resultados[:no_migrada].any?
|
||||
puts "\n#{COLORES[:red]}❌ No migradas (MD existe, DB no): #{@resultados[:no_migrada].size}#{COLORES[:reset]}"
|
||||
@resultados[:no_migrada].each { |f| puts " #{f}" }
|
||||
puts " #{COLORES[:dim]}Ejecuta: ./tools/adn/run db migrar:md FECHA#{COLORES[:reset]}"
|
||||
end
|
||||
|
||||
# Sin MD
|
||||
if @resultados[:sin_md].any?
|
||||
puts "\n#{COLORES[:cyan]}ℹ En DB sin archivo MD: #{@resultados[:sin_md].size}#{COLORES[:reset]}"
|
||||
@resultados[:sin_md].each { |f| puts " #{f}" }
|
||||
puts " #{COLORES[:dim]}Ejecuta: ./tools/adn/run db exportar:md FECHA#{COLORES[:reset]}"
|
||||
end
|
||||
|
||||
total = @resultados[:ok].size + @resultados[:sin_entradas].size
|
||||
total_md = @resultados[:ok].size + @resultados[:sin_entradas].size + @resultados[:no_migrada].size
|
||||
pct = total_md > 0 ? ((@resultados[:ok].size.to_f / total_md) * 100).round(1) : 0
|
||||
|
||||
puts "\n#{COLORES[:bold]}📈 Cobertura: #{pct}% (#{@resultados[:ok].size}/#{total_md})#{COLORES[:reset]}"
|
||||
|
||||
if @resultados[:no_migrada].empty? && @resultados[:sin_entradas].empty?
|
||||
puts "#{COLORES[:green]}#{COLORES[:bold]}🎉 ¡Migración completa! Todas las bitácoras están en la DB.#{COLORES[:reset]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Ejecución directa
|
||||
if __FILE__ == $0
|
||||
bitacoras_dir = File.expand_path('../../../docs/bitacoras', __dir__)
|
||||
BitacorasDB::CLI::VerificarMigracion.new(bitacoras_dir).ejecutar
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'date'
|
||||
|
||||
module BitacorasDB
|
||||
module Exporters
|
||||
class MDExporter
|
||||
def initialize(db, logger = nil)
|
||||
@db = db
|
||||
@logger = logger || default_logger
|
||||
end
|
||||
|
||||
def exportar_bitacora(fecha, ruta_destino)
|
||||
@logger.info("Exportando bitácora #{fecha} a #{ruta_destino}...")
|
||||
|
||||
bitacora = @db.find_bitacora_by_fecha(fecha)
|
||||
unless bitacora
|
||||
@logger.error("No se encontró la bitácora para la fecha: #{fecha}")
|
||||
return false
|
||||
end
|
||||
|
||||
bitacora_id = bitacora['id']
|
||||
|
||||
# Obtener datos estructurados (incluyendo lo nuevo)
|
||||
entradas = @db.execute("
|
||||
SELECT e.*, n.nombre as nodo_nombre
|
||||
FROM bitacoras.entradas e
|
||||
LEFT JOIN bitacoras.nodos n ON e.nodo_id = n.id
|
||||
WHERE e.bitacora_id = $1
|
||||
ORDER BY n.nombre, e.inicio ASC
|
||||
", [bitacora_id])
|
||||
|
||||
gestion = @db.execute("
|
||||
SELECT g.*, n.nombre as nodo_nombre
|
||||
FROM bitacoras.gestion g
|
||||
LEFT JOIN bitacoras.nodos n ON g.nodo_id = n.id
|
||||
WHERE g.bitacora_id = $1 OR g.activo = true
|
||||
ORDER BY g.tipo, g.orden
|
||||
", [bitacora_id]) rescue [] # Por si la tabla no existe aún en otros entornos
|
||||
|
||||
resumenes = @db.execute("
|
||||
SELECT r.*, n.nombre as nodo_nombre
|
||||
FROM bitacoras.resumen_nodos r
|
||||
JOIN bitacoras.nodos n ON r.nodo_id = n.id
|
||||
WHERE r.bitacora_id = $1
|
||||
", [bitacora_id]) rescue []
|
||||
|
||||
# Generar contenido Markdown
|
||||
contenido = generar_markdown(fecha, bitacora['resumen'], gestion, resumenes, entradas)
|
||||
|
||||
File.write(ruta_destino, contenido)
|
||||
@logger.info("✅ Exportación completada con éxito.")
|
||||
true
|
||||
rescue => e
|
||||
@logger.error("Error exportando bitácora: #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generar_markdown(fecha, resumen_gen, gestion, resumenes, entradas)
|
||||
y, m, d = fecha.to_s.split('-')
|
||||
md = String.new("<!-- GENERADO AUTOMÁTICAMENTE por adn db exportar:md — No editar manualmente -->\n")
|
||||
md << "# Bitácora de Operaciones - #{d}/#{m}/#{y}\n\n"
|
||||
|
||||
if gestion && gestion.ntuples > 0
|
||||
md << "## 📋 Control de Gestión\n\n"
|
||||
|
||||
pendientes = gestion.select { |g| g['tipo'] == 'pendiente' }
|
||||
if pendientes.any?
|
||||
md << "### 📝 Resumen de Actividades\n" # Formato ADN
|
||||
md << "| NODO | PENDIENTES |\n| :--- | :--- |\n"
|
||||
pendientes.each do |p|
|
||||
md << "| #{p['nodo_nombre'] || '—'} | #{p['detalle']} |\n"
|
||||
end
|
||||
md << "\n"
|
||||
end
|
||||
|
||||
en_proceso = gestion.select { |g| g['tipo'] == 'proceso' }
|
||||
if en_proceso.any?
|
||||
md << "### ⏳ En Proceso\n"
|
||||
md << "| NODO | ACTIVIDAD |\n| :--- | :--- |\n"
|
||||
en_proceso.each do |p|
|
||||
md << "| #{p['nodo_nombre'] || '—'} | #{p['detalle']} |\n"
|
||||
end
|
||||
md << "\n"
|
||||
end
|
||||
end
|
||||
|
||||
if resumenes && resumenes.ntuples > 0
|
||||
md << "### 📝 Resumen Integral\n"
|
||||
md << "| NODO | RESUMEN INTEGRAL |\n| :--- | :--- |\n"
|
||||
resumenes.each do |r|
|
||||
md << "| #{r['nodo_nombre']} | #{r['resumen']} |\n"
|
||||
end
|
||||
md << "\n"
|
||||
end
|
||||
|
||||
md << "## 📂 Actividades Detalladas\n\n"
|
||||
|
||||
por_nodo = entradas.group_by { |e| e['nodo_nombre'] || 'sin-nodo' }
|
||||
por_nodo.each do |nodo, entries|
|
||||
md << "### #{nodo}\n\n"
|
||||
md << "| I | F | Descripción | E |\n"
|
||||
md << "| :--- | :--- | :--- | :--- |\n"
|
||||
entries.each do |e|
|
||||
inicio = e['inicio'] ? e['inicio'][0..4] : '-'
|
||||
fin = e['fin'] ? e['fin'][0..4] : '-'
|
||||
ia = e['es_ia'] == 't' ? '(IA) ' : ''
|
||||
estado = "[#{e['modo']}] #{e['estado']}"
|
||||
md << "| #{inicio} | #{fin} | #{ia}#{e['descripcion']} | #{estado} |\n"
|
||||
end
|
||||
md << "\n"
|
||||
end
|
||||
|
||||
md << "---\n"
|
||||
md << "<!-- GENERADO AUTOMÁTICAMENTE desde PostgreSQL el #{Time.now.strftime('%Y-%m-%d %H:%M')} -->\n"
|
||||
md << "<!-- Fuente de verdad: DB. Regenerar con: adn db exportar:md #{fecha} -->\n"
|
||||
md
|
||||
end
|
||||
|
||||
def default_logger
|
||||
Class.new do
|
||||
def info(msg); puts "[INFO] #{msg}"; end
|
||||
def error(msg); puts "[ERROR] #{msg}"; end
|
||||
end.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,6 +31,7 @@
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'yaml'
|
||||
require 'digest'
|
||||
|
||||
module BitacorasDB
|
||||
module Importers
|
||||
@@ -43,14 +44,16 @@ module BitacorasDB
|
||||
'⏸️' => '⏸️',
|
||||
'⏳' => '⏳',
|
||||
'👁️' => '⏳', # Sincronización mapea a ⏳
|
||||
'➡️' => '⏳' # Tarea continúa en otro día
|
||||
'➡️' => '⏳', # Tarea continúa en otro día
|
||||
'🎭' => '✅' # Evento/Servicio
|
||||
}.freeze
|
||||
|
||||
# Modos posibles en archivos .md
|
||||
MODOS_MD = {
|
||||
'[P]' => 'P', # Presencial
|
||||
'[R]' => 'R', # Remoto
|
||||
'[P/R]' => 'P' # Presencial/Remoto -> Presencial por defecto
|
||||
'[P/R]' => 'P', # Presencial/Remoto -> Presencial por defecto
|
||||
'[S]' => 'S' # Servicio (No nodo técnico)
|
||||
}.freeze
|
||||
|
||||
# Tipos de nodo inferidos del nombre
|
||||
@@ -61,6 +64,7 @@ module BitacorasDB
|
||||
/^pcv-/ => 'vm',
|
||||
/^pc-/ => 'pc',
|
||||
/^srvv-/ => 'vm',
|
||||
/^dtic-/ => 'servicio',
|
||||
/-pmox\d+$/ => 'hipervisor',
|
||||
/-xen\d+$/ => 'hipervisor'
|
||||
}.freeze
|
||||
@@ -264,7 +268,7 @@ module BitacorasDB
|
||||
end
|
||||
|
||||
# Detectar inicio de tabla de entradas
|
||||
if linea.match?(/^\| I \| F \| Descripción \| E \|$/)
|
||||
if linea.match?(/^\| I \| F \| Descripción \| (?:J \| )?E \|$/)
|
||||
en_tabla_entradas = true
|
||||
encabezados_tabla = linea.split('|').map(&:strip).reject(&:empty?)
|
||||
entradas_tabla = []
|
||||
@@ -343,20 +347,32 @@ module BitacorasDB
|
||||
|
||||
# Parsea línea de tema (ej: "✅ - SEC01 - Refactorización ns8-candados")
|
||||
def parsear_tema(linea)
|
||||
# Patrón: estado - id - título (id puede contener puntos, como ADN01.1)
|
||||
match = linea.match(/^([✅❌⚠️⏸️⏳👁️➡️]+)\s*-\s*([A-Z0-9.]+)\s*-\s*(.+)$/)
|
||||
return nil unless match
|
||||
# Intentar formato estándar: estado - id - título (ej: ✅ - ADN01 - Título)
|
||||
match_std = linea.match(/^([^\s-]+)\s*-\s*([A-Z0-9.]+)\s*-\s*(.+)$/)
|
||||
if match_std
|
||||
return {
|
||||
id_tema: match_std[2],
|
||||
titulo: match_std[3].strip,
|
||||
estado: ESTADOS_MD[match_std[1]] || '⏳',
|
||||
descripcion: match_std[3].strip
|
||||
}
|
||||
end
|
||||
|
||||
estado = match[1]
|
||||
id_tema = match[2]
|
||||
titulo = match[3].strip
|
||||
# Intentar formato simplificado: estado - título (ej: 🎭 - Servicio Auditorio)
|
||||
match_simple = linea.match(/^([^\s-]+)\s*-\s*(.+)$/)
|
||||
if match_simple
|
||||
titulo = match_simple[2].strip
|
||||
# Generar un ID determinista basado en el título para estabilidad
|
||||
id_gen = "GEN" + Digest::MD5.hexdigest(titulo)[0..5].upcase
|
||||
return {
|
||||
id_tema: id_gen,
|
||||
titulo: titulo,
|
||||
estado: ESTADOS_MD[match_simple[1]] || '⏳',
|
||||
descripcion: titulo
|
||||
}
|
||||
end
|
||||
|
||||
{
|
||||
id_tema: id_tema,
|
||||
titulo: titulo,
|
||||
estado: ESTADOS_MD[estado] || '⏳',
|
||||
descripcion: titulo # Por defecto, usar título como descripción
|
||||
}
|
||||
nil
|
||||
end
|
||||
|
||||
# Parsea líneas de tabla de entradas
|
||||
@@ -370,6 +386,7 @@ module BitacorasDB
|
||||
when 'i' then columnas[:inicio] = idx
|
||||
when 'f' then columnas[:fin] = idx
|
||||
when 'descripción' then columnas[:descripcion] = idx
|
||||
when 'j' then columnas[:jornada] = idx
|
||||
when 'e' then columnas[:estado] = idx
|
||||
end
|
||||
end
|
||||
@@ -379,28 +396,39 @@ module BitacorasDB
|
||||
next if linea.match?(/^[\|\s:-]+$/) # Solo pipes, espacios, guiones y dos puntos
|
||||
next if linea.match?(/^\|?\s*:?-+:?\s*\|/) # Separadores con :---, ---, :-:, etc.
|
||||
|
||||
celdas = linea.split('|').map(&:strip).reject(&:empty?)
|
||||
# Limpiar pipes iniciales y finales antes de split
|
||||
linea_limpia = linea.strip.gsub(/^\||\|$/, '')
|
||||
celdas = linea_limpia.split('|').map(&:strip)
|
||||
next if celdas.length < 3 # Mínimo: inicio, descripción, estado
|
||||
|
||||
# Validar que la celda de inicio no sea un separador
|
||||
# Validar que la celda de inicio tenga contenido
|
||||
valor_inicio = celdas[columnas[:inicio]]
|
||||
next if valor_inicio.nil? || valor_inicio.match?(/^:?-+:?$/)
|
||||
next if valor_inicio.nil? || valor_inicio.empty? || valor_inicio.match?(/^:?-+:?$/)
|
||||
|
||||
fin_valor = nil
|
||||
if columnas[:fin] && celdas[columnas[:fin]] != '-'
|
||||
fin_valor = celdas[columnas[:fin]]
|
||||
if columnas[:fin] && !celdas[columnas[:fin]].nil?
|
||||
raw_fin = celdas[columnas[:fin]].strip
|
||||
fin_valor = raw_fin if !raw_fin.empty? && raw_fin != '-'
|
||||
end
|
||||
|
||||
entrada = {
|
||||
inicio: celdas[columnas[:inicio]],
|
||||
fin: fin_valor,
|
||||
inicio: valor_inicio.gsub(/\s*\(.*?\)\s*/, '').strip,
|
||||
fin: fin_valor ? fin_valor.gsub(/\s*\(.*?\)\s*/, '').strip : nil,
|
||||
descripcion: celdas[columnas[:descripcion]],
|
||||
estado_modo: celdas[columnas[:estado]]
|
||||
}
|
||||
|
||||
# Parsear estado y modo (ej: "[P] ✅")
|
||||
if entrada[:estado_modo]
|
||||
estado_modo = parsear_estado_modo(entrada[:estado_modo])
|
||||
# Parsear estado y modo
|
||||
if columnas[:jornada] && columnas[:estado]
|
||||
# Nuevo formato: J y E separados
|
||||
# Unimos temporalmente para usar parsear_estado_modo existente
|
||||
campo_combinado = "[#{celdas[columnas[:jornada]]}] #{celdas[columnas[:estado]]}"
|
||||
estado_modo = parsear_estado_modo(campo_combinado)
|
||||
entrada[:estado] = estado_modo[:estado]
|
||||
entrada[:modo] = estado_modo[:modo]
|
||||
elsif columnas[:estado]
|
||||
# Formato viejo o solo E
|
||||
estado_modo = parsear_estado_modo(celdas[columnas[:estado]])
|
||||
entrada[:estado] = estado_modo[:estado]
|
||||
entrada[:modo] = estado_modo[:modo]
|
||||
end
|
||||
@@ -456,6 +484,16 @@ module BitacorasDB
|
||||
|
||||
bitacora_id = bitacora['id']
|
||||
|
||||
# 1b. Si se solicita sobrescribir, limpiar entradas previas de esta bitácora
|
||||
if opciones[:sobrescribir]
|
||||
@logger.info("Limpiando datos previos de la bitácora #{estructura[:fecha]}...")
|
||||
# Borrar entradas primero (FK)
|
||||
@db.execute("DELETE FROM bitacoras.entradas WHERE bitacora_id = $1", [bitacora_id])
|
||||
# Borrar temas (FK)
|
||||
@db.execute("DELETE FROM bitacoras.temas WHERE bitacora_id = $1", [bitacora_id])
|
||||
@estadisticas[:datos_limpiados] = true
|
||||
end
|
||||
|
||||
# 2. Procesar cada nodo
|
||||
estructura[:nodos].each do |nombre_nodo, datos_nodo|
|
||||
# Buscar o crear nodo
|
||||
@@ -505,19 +543,43 @@ module BitacorasDB
|
||||
# Validar campos requeridos
|
||||
next unless entrada_md[:inicio] && entrada_md[:descripcion]
|
||||
|
||||
# Crear entrada en BD
|
||||
@db.create_entrada(
|
||||
inicio: entrada_md[:inicio],
|
||||
fin: entrada_md[:fin],
|
||||
descripcion: entrada_md[:descripcion],
|
||||
estado: entrada_md[:estado] || '⏳',
|
||||
modo: entrada_md[:modo] || 'P',
|
||||
es_ia: false, # Las importadas de .md no son IA
|
||||
tema_id: tema_id,
|
||||
bitacora_id: bitacora_id,
|
||||
nodo_id: nodo_id
|
||||
)
|
||||
@estadisticas[:entradas_creadas] += 1
|
||||
# Intentar buscar entrada existente para evitar duplicados (Upsert)
|
||||
# Buscamos por: bitacora_id, nodo_id y hora de inicio (única para un nodo en un día)
|
||||
entradas_nodo = @db.list_entradas(bitacora_id: bitacora_id, nodo_id: nodo_id, limit: 100)
|
||||
# Normalizar hora para comparación
|
||||
inicio_norm = @db.send(:parse_time, entrada_md[:inicio])
|
||||
|
||||
existente = entradas_nodo.find { |e| e['inicio'] == inicio_norm }
|
||||
|
||||
begin
|
||||
if existente
|
||||
# Actualizar si ya existe
|
||||
@db.update_entrada(existente['id'], {
|
||||
fin: entrada_md[:fin],
|
||||
descripcion: entrada_md[:descripcion],
|
||||
estado: entrada_md[:estado] || '⏳',
|
||||
modo: entrada_md[:modo] || 'P'
|
||||
})
|
||||
@estadisticas[:entradas_actualizadas] ||= 0
|
||||
@estadisticas[:entradas_actualizadas] += 1
|
||||
else
|
||||
# Crear si no existe
|
||||
@db.create_entrada(
|
||||
inicio: entrada_md[:inicio],
|
||||
fin: entrada_md[:fin],
|
||||
descripcion: entrada_md[:descripcion],
|
||||
estado: entrada_md[:estado] || '⏳',
|
||||
modo: entrada_md[:modo] || 'P',
|
||||
es_ia: false,
|
||||
tema_id: tema_id,
|
||||
bitacora_id: bitacora_id,
|
||||
nodo_id: nodo_id
|
||||
)
|
||||
@estadisticas[:entradas_creadas] += 1
|
||||
end
|
||||
rescue => e
|
||||
@logger.error("Error procesando entrada '#{entrada_md[:inicio]} - #{entrada_md[:descripcion]}': #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user