# 📦 Estructura propuesta ``` /mesacontrol/ ├─ db.php # Conexión PDO + sesión persistente (sin timeout) ├─ login.php # Formulario de acceso de agentes ├─ dashboard.php # Panel de estado (disponible/pausa/baño/comida/entrenamiento) ├─ api_login.php # POST: autentica y marca "disponible" ├─ api_state.php # POST: cambia estado del agente ├─ api_heartbeat.php # POST: latido para evitar timeout y marcar last_seen ├─ api_logout.php # POST/BEACON: marca logout y cierra sesión └─ hard_logout.php # GET: forzar salida manual (link) ``` --- # 🧱 SQL (tablas + evento opcional auto-logout por inactividad) ```sql -- Tabla de estados de agente (simple y eficiente) CREATE TABLE IF NOT EXISTS mesacontrol_agente_estado ( id INT AUTO_INCREMENT PRIMARY KEY, login VARCHAR(64) NOT NULL, estado ENUM('logout','disponible','pausa','baño','comida','entrenamiento') NOT NULL DEFAULT 'logout', session_id VARCHAR(128) DEFAULT NULL, last_change DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, user_agent VARCHAR(255) DEFAULT NULL, ip VARCHAR(64) DEFAULT NULL, is_active TINYINT(1) NOT NULL DEFAULT 0, UNIQUE KEY uk_login (login), KEY idx_seen (last_seen) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- (Opcional) Evento MySQL para dar logout si no hay heartbeat > 2 min -- Requiere: SET GLOBAL event_scheduler = ON; DROP EVENT IF EXISTS ev_mesacontrol_autologout; CREATE EVENT ev_mesacontrol_autologout ON SCHEDULE EVERY 1 MINUTE DO UPDATE mesacontrol_agente_estado SET estado='logout', is_active=0 WHERE is_active=1 AND TIMESTAMPDIFF(SECOND, last_seen, NOW()) > 120; -- 2 minutos sin latido ``` > Nota: Si prefieres manejar la caducidad desde PHP (sin EVENT), puedes ejecutar el UPDATE anterior en cualquier endpoint sensible (por ejemplo en `api_heartbeat.php` o `dashboard.php` al cargar). --- # 🔧 db.php ```php $lifetime, 'path' => '/', 'secure' => false, // pon true si usas HTTPS 'httponly' => true, 'samesite' => 'Lax', ]); if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } function pdo(): PDO { static $pdo = null; if ($pdo) return $pdo; $dsn = 'mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4'; $pdo = new PDO($dsn, DB_USER, DB_PASS, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); return $pdo; } function agent_require_login() { if (empty($_SESSION['agent_login'])) { header('Location: login.php'); exit; } } function sanitize_state(string $s): string { $allowed = ['logout','disponible','pausa','baño','comida','entrenamiento']; return in_array($s, $allowed, true) ? $s : 'logout'; } function agent_upsert_state(string $login, string $estado, bool $active, ?string $sessionId=null) { $pdo = pdo(); $estado = sanitize_state($estado); $sql = "INSERT INTO mesacontrol_agente_estado (login, estado, is_active, session_id, last_change, last_seen, user_agent, ip) VALUES (:login, :estado, :active, :sid, NOW(), NOW(), :ua, :ip) ON DUPLICATE KEY UPDATE estado=VALUES(estado), is_active=VALUES(is_active), session_id=VALUES(session_id), last_change=NOW(), last_seen=NOW(), user_agent=VALUES(user_agent), ip=VALUES(ip)"; $stmt = $pdo->prepare($sql); $stmt->execute([ ':login' => $login, ':estado' => $estado, ':active' => $active ? 1 : 0, ':sid' => $sessionId, ':ua' => $_SERVER['HTTP_USER_AGENT'] ?? null, ':ip' => $_SERVER['REMOTE_ADDR'] ?? null, ]); } function agent_touch_seen(string $login) { $pdo = pdo(); $stmt = $pdo->prepare("UPDATE mesacontrol_agente_estado SET last_seen=NOW() WHERE login=?"); $stmt->execute([$login]); } ``` --- # 🔐 login.php ```php Login Mesa de Control

Acceso Agentes

``` --- # 🚪 api_login.php ```php false,'msg'=>'Faltan credenciales']); exit; } try { $pdo = pdo(); $stmt = $pdo->prepare("SELECT login, pswd, name FROM sec_users WHERE active='Y' AND login=? LIMIT 1"); $stmt->execute([$login]); $u = $stmt->fetch(); if (!$u) { http_response_code(401); echo json_encode(['ok'=>false,'msg'=>'Usuario no encontrado o inactivo']); exit; } // Si tu tabla guarda hash, reemplaza esta verificación por password_verify() if (!hash_equals((string)$u['pswd'], (string)$pswd)) { http_response_code(401); echo json_encode(['ok'=>false,'msg'=>'Contraseña incorrecta']); exit; } $_SESSION['agent_login'] = $u['login']; $_SESSION['agent_name'] = $u['name'] ?? $u['login']; // Estado: disponible y activo agent_upsert_state($u['login'], 'disponible', true, session_id()); echo json_encode(['ok'=>true,'redirect'=>'dashboard.php']); } catch (Throwable $e) { http_response_code(500); echo json_encode(['ok'=>false,'msg'=>'Error: '.$e->getMessage()]); } ``` --- # 📊 dashboard.php ```php exec("UPDATE mesacontrol_agente_estado SET estado='logout', is_active=0 WHERE is_active=1 AND TIMESTAMPDIFF(SECOND,last_seen,NOW())>120"); $stmt = $pdo->prepare("SELECT estado, last_change, last_seen FROM mesacontrol_agente_estado WHERE login=?"); $stmt->execute([$login]); $est = $stmt->fetch() ?: ['estado'=>'logout','last_change'=>null,'last_seen'=>null]; ?> Mesa de Control — Estado de Agente

Mi estado actual:

Último cambio: · Último latido:

Esta página no caduca: mientras permanezca abierta se envía un heartbeat cada 20s. Si se cierra la pestaña/ventana o se cae el navegador, el sistema intenta marcar logout automáticamente; si no alcanza a enviar, el evento/guard de inactividad lo hará en ~2 minutos.

``` --- # 🔁 api_state.php ```php prepare("SELECT estado, last_change, last_seen FROM mesacontrol_agente_estado WHERE login=?"); $stmt->execute([$login]); $row = $stmt->fetch(); echo json_encode(['ok'=>true] + $row); } catch (Throwable $e) { http_response_code(500); echo json_encode(['ok'=>false,'msg'=>$e->getMessage()]); } ``` --- # ❤️ api_heartbeat.php ```php prepare("SELECT last_seen FROM mesacontrol_agente_estado WHERE login=?"); $stmt->execute([$login]); $row = $stmt->fetch(); echo json_encode(['ok'=>true,'last_seen'=>$row['last_seen'] ?? date('Y-m-d H:i:s')]); } catch (Throwable $e) { http_response_code(500); echo json_encode(['ok'=>false,'msg'=>$e->getMessage()]); } ``` --- # 👋 api_logout.php ```php true]); exit; } $login = $_SESSION['agent_login']; try { agent_upsert_state($login, 'logout', false, null); } catch (Throwable $e) { // continuar a destruir sesión de todos modos } // Cerrar sesión PHP limpiamente $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time()-42000, $params['path'], $params['domain'] ?? '', $params['secure'], $params['httponly']); } session_destroy(); echo json_encode(['ok'=>true]); ``` --- # 🔓 hard_logout.php ```php