fix: replace SSE with polling in live feed to stop blocking FPM workers

SSE kept a PHP-FPM worker open for 28s per connection, blocking the whole
server with only 20 workers shared across all vhosts. Polling with
setInterval(3000) releases the worker on each request immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-22 11:37:21 -05:00
co-authored by Claude Sonnet 4.6
parent c702dbadc1
commit 483cc4797e
+27 -19
View File
@@ -115,7 +115,7 @@ function togglePause() {
paused = !paused;
document.getElementById('btnPause').innerHTML = paused ? '&#9654; Reanudar' : '&#9646;&#9646; Pausar';
document.getElementById('dot').className = 'dot' + (paused ? ' paused' : '');
if (paused) { document.getElementById('status').textContent = 'En pausa'; es && es.close(); }
if (paused) { document.getElementById('status').textContent = 'En pausa'; clearInterval(pollTimer); pollTimer = null; }
else { connect(); }
}
@@ -154,17 +154,28 @@ function addCards(rows) {
}
}
function connect() {
es = new EventSource('/admin/webhook/stream/sse');
es.onopen = () => {
let lastId = 0;
let pollTimer = null;
async function poll() {
if (paused) return;
try {
const r = await fetch('/admin/webhook/stream?after=' + lastId + (lastId === 0 ? '&init=1' : ''));
if (!r.ok) throw new Error('HTTP ' + r.status);
const d = await r.json();
if (d.last_id !== undefined) { lastId = d.last_id; }
if (d.rows && d.rows.length) { lastId = d.rows[d.rows.length-1].id; addCards(d.rows); }
document.getElementById('dot').className = 'dot';
document.getElementById('status').textContent = 'Conectado — tiempo real (SSE)';
};
es.onmessage = e => { if (!paused) try { addCards(JSON.parse(e.data)); } catch(_) {} };
es.onerror = () => {
document.getElementById('status').textContent = 'Conectado — polling cada 3s';
} catch(e) {
document.getElementById('dot').className = 'dot paused';
document.getElementById('status').textContent = 'Reconectando...';
};
document.getElementById('status').textContent = 'Error de conexion, reintentando...';
}
}
function connect() {
poll();
pollTimer = setInterval(poll, 3000);
}
function esc(s) {
@@ -181,7 +192,7 @@ async function showRaw(id) {
}
function closeModal() { document.getElementById('modal').style.display = 'none'; }
document.addEventListener('keydown', e => { if(e.key==='Escape') closeModal(); });
window.addEventListener('pagehide', () => { es && es.close(); });
window.addEventListener('pagehide', () => { clearInterval(pollTimer); });
connect();
</script>
@@ -261,13 +272,10 @@ HTML;
try {
if ($isInit) {
// En init devolvemos solo el último ID (sin datos) para anclar
$stmt = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1');
$row = $stmt->fetch();
$afterId = $row ? (int)$row['id'] : 0;
// Devolvemos array vacío — solo queremos anclar lastId en el cliente
$row = db()->query('SELECT id FROM webhook_logs ORDER BY id DESC LIMIT 1')->fetch();
$lastId = $row ? (int)$row['id'] : 0;
header('Content-Type: application/json; charset=utf-8');
echo json_encode([]);
echo json_encode(['rows' => [], 'last_id' => $lastId]);
exit;
}
@@ -283,12 +291,12 @@ HTML;
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
header('Content-Type: application/json; charset=utf-8');
echo '[]';
echo json_encode(['rows' => [], 'last_id' => $afterId]);
exit;
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
echo json_encode(['rows' => $rows, 'last_id' => $rows ? (int)end($rows)['id'] : $afterId], JSON_UNESCAPED_UNICODE);
exit;
}