Files
whatsapp/modules/turnero/api/sse_turno.php
Lizandro GuarnizoandClaude Sonnet 4.6 d24b234e30 fix(display): corregir turnos que no aparecen en pantalla TV
- sse_turno.php: sleep(2)→sleep(1) para detectar notificarSSE en máx 1s
- display*.php: visibilitychange + focus → fetch inmediato al despertar pantalla
- display*.php: Wake Lock API para evitar throttling del browser en TVs
- display.php: SSE open → cargarSnapshot() inmediato al reconectar

Causa raíz: browsers throttlean setInterval a >1min en tabs sin foco;
las TVs inactivas dejaban de recibir actualizaciones aunque el servidor
las enviara correctamente.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 22:47:13 -05:00

159 lines
5.6 KiB
PHP

<?php
/**
* GET /modules/turnero/api/sse_turno.php
* Server-Sent Events — actualiza pantallas TV y kiosko en tiempo real.
* No requiere autenticación (acceso público).
*
* Query params:
* area string opcional "recepcion" | "lugar" (default: "recepcion")
* lugar_id int requerido si area = "lugar"
*
* Eventos emitidos:
* cola_update — estado completo de la cola cada vez que haya cambio o cada 30 s
* ping — comentario de keep-alive cada 15 s
*/
// ── Asegurar que la columna sse_ping_at exista (idempotente) ──
require_once __DIR__ . '/_helpers.php';
$pdo = db();
$pdo->exec(
"ALTER TABLE turnero_sesiones
ADD COLUMN IF NOT EXISTS sse_ping_at DATETIME NULL DEFAULT NULL"
);
// ── Headers SSE ───────────────────────────────────────────────
@set_time_limit(0);
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', false);
header('Content-Type: text/event-stream; charset=UTF-8');
header('Cache-Control: no-store, no-cache');
header('X-Accel-Buffering: no'); // Nginx: deshabilitar buffering
header('Connection: keep-alive');
// ── Parámetros ────────────────────────────────────────────────
$area = isset($_GET['area']) ? trim($_GET['area']) : 'recepcion';
$lugarId = isset($_GET['lugar_id']) ? (int) $_GET['lugar_id'] : null;
if (!in_array($area, ['recepcion', 'lugar'], true)) {
$area = 'recepcion';
}
// ── Helpers de emisión ────────────────────────────────────────
function sseEvent(string $event, mixed $payload): void
{
echo "event: {$event}\n";
echo 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
}
function ssePing(): void
{
echo ": ping\n\n";
flush();
}
// ── Función que arma el snapshot de la cola ───────────────────
function buildSnapshot(PDO $pdo, int $sesionId, string $area, ?int $lugarId): array
{
if ($area === 'recepcion') {
$estadosCola = ["'espera'", "'en_recepcion'"];
$filtroLugar = '';
$bindsCola = [$sesionId];
$estadoActivo = "'en_recepcion'";
$campoLlamado = 'llamado_recepcion_at';
$bindActivo = [$sesionId];
$filtroActivo = '';
} else {
$estadosCola = ["'en_espera_lugar'", "'en_servicio'"];
$filtroLugar = 'AND t.lugar_destino_id = ?';
$bindsCola = [$sesionId, $lugarId];
$estadoActivo = "'en_servicio'";
$campoLlamado = 'llamado_lugar_at';
$bindActivo = [$sesionId, $lugarId];
$filtroActivo = 'AND t.lugar_destino_id = ?';
}
$inEstados = implode(', ', $estadosCola);
$stmt = $pdo->prepare(
"SELECT t.id, t.codigo, t.numero, t.estado, t.paciente_nombre, t.creado_at,
p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre,
p.color AS prioridad_color, p.orden_peso
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
WHERE t.sesion_id = ?
AND t.estado IN ({$inEstados})
{$filtroLugar}
ORDER BY p.orden_peso ASC, t.creado_at ASC"
);
$stmt->execute($bindsCola);
$cola = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare(
"SELECT t.id, t.codigo, t.numero, t.estado, t.paciente_nombre,
t.{$campoLlamado} AS llamado_at,
p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre,
p.color AS prioridad_color
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
WHERE t.sesion_id = ?
AND t.estado = {$estadoActivo}
{$filtroActivo}
ORDER BY t.{$campoLlamado} DESC
LIMIT 1"
);
$stmt->execute($bindActivo);
$activo = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
return [
'activo' => $activo,
'cola' => $cola,
'timestamp' => date('c'),
];
}
// ── Loop principal ────────────────────────────────────────────
$sesionId = obtenerOCrearSesionHoy();
$lastKeepalive = time();
$lastEmit = 0;
// Emitir estado inicial inmediatamente
$snapshot = buildSnapshot($pdo, $sesionId, $area, $lugarId);
sseEvent('cola_update', $snapshot);
// Inicializar $lastPing con el valor ACTUAL (no null) para que la
// primera iteración del loop no emita un snapshot extra sin cambios
$stmt = $pdo->prepare('SELECT sse_ping_at FROM turnero_sesiones WHERE id = ?');
$stmt->execute([$sesionId]);
$lastPing = $stmt->fetchColumn() ?: null;
while (!connection_aborted()) {
sleep(1);
// Leer el último sse_ping_at de la sesión
$stmt = $pdo->prepare('SELECT sse_ping_at FROM turnero_sesiones WHERE id = ?');
$stmt->execute([$sesionId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$pingActual = $row['sse_ping_at'] ?? null;
// Emitir si hubo cambio (notificarSSE actualiza sse_ping_at) o cada 30 s
$ahora = time();
$forzar = ($ahora - $lastEmit) >= 30;
if ($pingActual !== $lastPing || $forzar) {
$snapshot = buildSnapshot($pdo, $sesionId, $area, $lugarId);
sseEvent('cola_update', $snapshot);
$lastPing = $pingActual;
$lastEmit = $ahora;
}
// Keep-alive cada 15 s
if (($ahora - $lastKeepalive) >= 15) {
ssePing();
$lastKeepalive = $ahora;
}
}