- 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>
159 lines
5.6 KiB
PHP
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;
|
|
}
|
|
}
|