feat: scheduler live status — endpoint + UI polling every 15s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9d79fe932b
commit
467a24baa4
@@ -37,11 +37,12 @@
|
|||||||
<h3 class="font-semibold text-gray-800">
|
<h3 class="font-semibold text-gray-800">
|
||||||
<i class="fas fa-robot mr-2 text-green-500"></i>Estado del Scheduler
|
<i class="fas fa-robot mr-2 text-green-500"></i>Estado del Scheduler
|
||||||
</h3>
|
</h3>
|
||||||
<span class="flex items-center gap-2 text-sm text-green-600 font-medium">
|
<span id="sched-badge" class="flex items-center gap-2 text-sm font-medium text-gray-400">
|
||||||
<span class="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
<span id="sched-dot" class="w-2 h-2 rounded-full bg-gray-300"></span>
|
||||||
Activo — cada 1 minuto
|
<span id="sched-label">Verificando…</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sched-live" class="px-5 pt-3 pb-2 text-xs text-gray-500 flex flex-wrap gap-x-6 gap-y-1 border-b border-gray-100"></div>
|
||||||
<div class="p-5 grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm text-gray-600">
|
<div class="p-5 grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm text-gray-600">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<i class="fas fa-database text-blue-400 mt-0.5"></i>
|
<i class="fas fa-database text-blue-400 mt-0.5"></i>
|
||||||
@@ -170,11 +171,44 @@ function switchTab(name) {
|
|||||||
if (name === 'scheduler' && !_schedulerLoaded) {
|
if (name === 'scheduler' && !_schedulerLoaded) {
|
||||||
_schedulerLoaded = true;
|
_schedulerLoaded = true;
|
||||||
cargarHistorialScheduler();
|
cargarHistorialScheduler();
|
||||||
|
pollSchedulerStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('erp-tab', name);
|
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(`<span><i class="fas fa-clock mr-1 text-blue-400"></i>Próxima ejecución: <strong>${nx.toLocaleTimeString('es-CO')}</strong></span>`);
|
||||||
|
}
|
||||||
|
if (d.ultimo_log) {
|
||||||
|
const ul = d.ultimo_log;
|
||||||
|
const ts = new Date(ul.created_at).toLocaleTimeString('es-CO');
|
||||||
|
parts.push(`<span><i class="fas fa-history mr-1 text-purple-400"></i>Último sync scheduler: <strong>${ts}</strong> · ${ul.total_procesados ?? 0} procesados, ${ul.total_errores ?? 0} errores</span>`);
|
||||||
|
}
|
||||||
|
live.innerHTML = parts.join('');
|
||||||
|
} catch(_) {}
|
||||||
|
_schedPollTimer = setTimeout(pollSchedulerStatus, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
const _origenLabel = { manual: 'Manual', tercero: 'Tercero', scheduler: 'Scheduler', automation: 'Automatización' };
|
||||||
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
const _modoLabel = { insertar: 'Solo nuevos', upsert: 'Upsert' };
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,26 @@ async def shutdown():
|
|||||||
_scheduler.shutdown(wait=False)
|
_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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return RedirectResponse(url="/dashboard")
|
return RedirectResponse(url="/dashboard")
|
||||||
|
|||||||
Reference in New Issue
Block a user