From 467a24baa4e7b06ba73b251f8572c13b293bea46 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:21:32 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20scheduler=20live=20status=20=E2=80=94?= =?UTF-8?q?=20endpoint=20+=20UI=20polling=20every=2015s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/templates/envios_erp.html | 40 ++++++++++++++++++++++++++++++++--- main.py | 20 ++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/app/templates/envios_erp.html b/app/templates/envios_erp.html index e746a31..606c05d 100644 --- a/app/templates/envios_erp.html +++ b/app/templates/envios_erp.html @@ -37,11 +37,12 @@

Estado del Scheduler

- - - Activo — cada 1 minuto + + + Verificando… +
@@ -170,11 +171,44 @@ function switchTab(name) { if (name === 'scheduler' && !_schedulerLoaded) { _schedulerLoaded = true; cargarHistorialScheduler(); + pollSchedulerStatus(); } sessionStorage.setItem('erp-tab', name); } +let _schedPollTimer = null; +async function pollSchedulerStatus() { + try { + const r = await fetch('/api/scheduler/status'); + const d = await r.json(); + const dot = document.getElementById('sched-dot'); + const label = document.getElementById('sched-label'); + const live = document.getElementById('sched-live'); + if (d.running && d.job_exists) { + dot.className = 'w-2 h-2 rounded-full bg-green-400 animate-pulse'; + label.textContent = 'Activo — cada 1 minuto'; + label.className = 'text-green-600'; + } else { + dot.className = 'w-2 h-2 rounded-full bg-red-400'; + label.textContent = d.running ? 'Sin tarea registrada' : 'DETENIDO'; + label.className = 'text-red-600'; + } + const parts = []; + if (d.next_run) { + const nx = new Date(d.next_run); + parts.push(`Próxima ejecución: ${nx.toLocaleTimeString('es-CO')}`); + } + if (d.ultimo_log) { + const ul = d.ultimo_log; + const ts = new Date(ul.created_at).toLocaleTimeString('es-CO'); + parts.push(`Último sync scheduler: ${ts} · ${ul.total_procesados ?? 0} procesados, ${ul.total_errores ?? 0} errores`); + } + live.innerHTML = parts.join(''); + } catch(_) {} + _schedPollTimer = setTimeout(pollSchedulerStatus, 15000); +} + const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' }; const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' }; diff --git a/main.py b/main.py index 42572ee..f900382 100644 --- a/main.py +++ b/main.py @@ -73,6 +73,26 @@ async def shutdown(): _scheduler.shutdown(wait=False) +@app.get("/api/scheduler/status") +async def scheduler_status(): + from fastapi.responses import JSONResponse + from app.database import get_connection + job = _scheduler.get_job("wa_sync_recientes") + next_run = job.next_run_time.isoformat() if job and job.next_run_time else None + conn = get_connection() + row = conn.execute( + "SELECT created_at, total_procesados, total_errores, origen " + "FROM sync_wa_log WHERE origen='scheduler' ORDER BY id DESC LIMIT 1" + ).fetchone() + conn.close() + return JSONResponse({ + "running": _scheduler.running, + "job_exists": job is not None, + "next_run": next_run, + "ultimo_log": dict(row) if row else None, + }) + + @app.get("/") async def root(): return RedirectResponse(url="/dashboard")