[DRON] Heartbeat con output acumulado en Bitácora Web

Mejora en el sistema de latidos de los drones para mostrar el progreso
acumulado en lugar de solo la última línea.

Cambios realizados:
- ejecutor.rb: Acumulación de output en array accumulated_out[]
- actualizar_heartbeat_web(): Muestra últimas 15 líneas en bloque ```text
- A01.P009: Documentada Fase F1.5b con ejemplo técnico
- A04.P006: Actualizado plan DASUTEN con mejora y lección aprendida

Beneficio: Visibilidad completa del progreso de drones multi-paso
(ej: [1/3], [2/3], [3/3] en pipelines de backup/restore)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-04-15 10:45:11 -03:00
co-authored by Claude Opus 4.6
parent 1bc6c94f78
commit 4a0cdd883e
3 changed files with 127 additions and 9 deletions
+28 -4
View File
@@ -78,6 +78,8 @@ module Dron
def ejecutar_comando(cmd, dron_id, timeout, evento_id, nota, inicio)
start_time = Time.now
output = []
last_out = ""
accumulated_out = [] # Acumular todas las líneas de output para el heartbeat
exit_code = nil
heartbeat_thread = Thread.new do
@@ -90,7 +92,8 @@ module Dron
ADN::DB::DronDB.actualizar_heartbeat(dron_id)
# Actualizar Bitácora Web (events.descripcion) — VISIBLE para el usuario
actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick) if evento_id
# Pasamos el output acumulado para mostrar todo el progreso
actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick, accumulated_out.join("\n")) if evento_id
rescue StandardError => e
Base.logger.warn("⚠️ Error en heartbeat ##{tick}: #{e.message}")
end
@@ -99,7 +102,17 @@ module Dron
begin
Timeout.timeout(timeout) do
Open3.popen3(cmd) do |_stdin, stdout, stderr, wait_thr|
out_thread = Thread.new { output << stdout.read }
out_thread = Thread.new do
begin
while chunk = stdout.readpartial(1024)
output << chunk
lineas = chunk.split(/[\r\n]/).reject(&:empty?)
last_out = lineas.last&.strip || last_out
accumulated_out.concat(lineas) # Acumular líneas para heartbeat
end
rescue EOFError, IOError
end
end
err_thread = Thread.new { output << stderr.read }
out_thread.join
err_thread.join
@@ -116,7 +129,9 @@ module Dron
heartbeat_thread.exit
end
{ exit_code: exit_code || 1, output: output.join("\n").strip, duration: (Time.now - start_time).round(2) }
raw_output = output.join("\n").strip
safe_output = raw_output.dup.force_encoding('UTF-8').scrub('?')
{ exit_code: exit_code || 1, output: safe_output, duration: (Time.now - start_time).round(2) }
end
# ─── Bitácora Web: Crear/Vincular evento ─────────────────────
@@ -180,12 +195,21 @@ module Dron
# ─── Bitácora Web: Heartbeat visible ─────────────────────────
def actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick)
def actualizar_heartbeat_web(evento_id, dron_id, nota, cmd, inicio, tick, accumulated_out = "")
duracion = Base.formato_duracion(Time.now - inicio)
descripcion = "🛸 #{nota} (`#{dron_id}`)\n" \
"├─ Comando: `#{cmd}`\n" \
"└─ ❤️ #{duracion} — heartbeat ##{tick}"
unless accumulated_out.nil? || accumulated_out.empty?
# Limpiar caracteres no imprimibles y mostrar todo el output acumulado
clean_out = accumulated_out.gsub(/[^[:print:]\n]/, '').strip
# Mostrar últimas 15 líneas para no saturar la bitácora
lineas = clean_out.split("\n")
lineas_mostrar = lineas.last(15)
descripcion += "\n\n```text\n#{lineas_mostrar.join("\n")}\n```"
end
BitacorasDB::BitacoraDB.with_connection do |db|
# Intentar actualizar como entrada primero, luego como evento
entrada = db.execute("SELECT id FROM bitacoras.entradas WHERE id = $1", [evento_id]).first