@@ -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")