0, 'msgs' => 0, 'media' => 0, 'statuses' => 0]; $logs = []; $total = 0; } $pages = $total > 0 ? (int)ceil($total / self::PER_PAGE) : 1; self::render(compact('stats', 'logs', 'total', 'page', 'pages', 'filter', 'search', 'date', 'user')); } // ─── GET /admin/live ──────────────────────────────────────────────────── public static function live(): void { SessionAuth::require(); $user = SessionAuth::user(); $userName = htmlspecialchars($user['name'] ?? 'Admin', ENT_QUOTES, 'UTF-8'); http_response_code(200); header('Content-Type: text/html; charset=utf-8'); echo << Live — Palmas360

Palmas360 Live Feed

← Dashboard 👤 {$userName} Salir
Conectado — actualizando cada 3s 0 eventos

Esperando eventos en tiempo real...

HTML; exit; } // ─── GET /admin/webhook/stream?after=N ─────────────────────────────────── public static function stream(): void { SessionAuth::require(); $afterId = max(0, (int)($_GET['after'] ?? 0)); $isInit = isset($_GET['init']); 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 header('Content-Type: application/json; charset=utf-8'); echo json_encode([]); exit; } $stmt = db()->prepare(" SELECT id, event_field, from_number, contact_name, message_type, message_preview, received_at FROM webhook_logs WHERE id > ? ORDER BY id ASC LIMIT 50 "); $stmt->execute([$afterId]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (\PDOException $e) { header('Content-Type: application/json; charset=utf-8'); echo '[]'; exit; } header('Content-Type: application/json; charset=utf-8'); echo json_encode($rows, JSON_UNESCAPED_UNICODE); exit; } // ─── GET /admin/webhook/raw?id=N ───────────────────────────────────────── public static function getRaw(): void { SessionAuth::require(); $id = max(0, (int)($_GET['id'] ?? 0)); if ($id === 0) { jsonResponse(400, ['error' => 'ID inválido']); } try { $stmt = db()->prepare('SELECT raw_payload FROM webhook_logs WHERE id = ? LIMIT 1'); $stmt->execute([$id]); $row = $stmt->fetch(); } catch (\PDOException $e) { jsonResponse(500, ['error' => 'Error de base de datos']); } if (!$row) { jsonResponse(404, ['error' => 'Registro no encontrado']); } header('Content-Type: application/json; charset=utf-8'); echo $row['raw_payload']; exit; } // ─── Consultas ──────────────────────────────────────────────────────────── private static function getStats(PDO $db): array { $stmt = $db->query(" SELECT COUNT(*) AS total, COALESCE(SUM(event_field='messages' AND message_type='text'),0) AS msgs, COALESCE(SUM(event_field='messages' AND message_type<>'text'),0) AS media, COALESCE(SUM(event_field='statuses'),0) AS statuses FROM webhook_logs WHERE DATE(received_at) = CURDATE() "); return $stmt->fetch() ?: ['total' => 0, 'msgs' => 0, 'media' => 0, 'statuses' => 0]; } private static function getLogs(PDO $db, int $page, string $filter, string $search, string $date): array { $where = ['DATE(received_at) = ?']; $params = [$date]; if ($filter !== '') { $where[] = 'event_field = ?'; $params[] = $filter; } if ($search !== '') { $where[] = '(from_number LIKE ? OR contact_name LIKE ? OR message_preview LIKE ?)'; $like = '%' . $search . '%'; array_push($params, $like, $like, $like); } $w = implode(' AND ', $where); $cnt = $db->prepare("SELECT COUNT(*) FROM webhook_logs WHERE $w"); $cnt->execute($params); $total = (int)$cnt->fetchColumn(); $limit = self::PER_PAGE; $offset = ($page - 1) * $limit; $stmt = $db->prepare(" SELECT id, event_field, from_number, contact_name, message_type, message_preview, received_at FROM webhook_logs WHERE $w ORDER BY received_at DESC LIMIT $limit OFFSET $offset "); $stmt->execute($params); return [$stmt->fetchAll(), $total]; } // ─── Helpers de vista ───────────────────────────────────────────────────── private static function h(mixed $v): string { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } private static function typeLabel(string $field, string $type): string { if ($field === 'statuses') { switch ($type) { case 'sent': $cls = 'badge-blue'; break; case 'delivered': $cls = 'badge-green'; break; case 'read': $cls = 'badge-teal'; break; case 'failed': $cls = 'badge-red'; break; default: $cls = 'badge-gray'; break; } return '' . self::h($type) . ''; } switch ($type) { case 'text': return '💬 Texto'; case 'image': return '📷 Imagen'; case 'audio': return '🎵 Audio'; case 'video': return '🎬 Video'; case 'document': return '📄 Documento'; case 'location': return '📍 Ubicación'; case 'interactive': return '🔘 Interactivo'; case 'button': return '🔲 Botón'; default: return self::h($type); } } // ─── Render ─────────────────────────────────────────────────────────────── private static function render(array $v): void { http_response_code(200); header('Content-Type: text/html; charset=utf-8'); // Variables para el heredoc $userName = self::h($v['user']['name'] ?? 'Admin'); $dateVal = self::h($v['date']); $maxDate = date('Y-m-d'); $filterVal = self::h($v['filter']); $searchVal = self::h($v['search']); $totalStr = number_format((int)$v['total']); $selMessages = $v['filter'] === 'messages' ? 'selected' : ''; $selStatuses = $v['filter'] === 'statuses' ? 'selected' : ''; $sTotal = (int)$v['stats']['total']; $sMsgs = (int)$v['stats']['msgs']; $sMedia = (int)$v['stats']['media']; $sStatuses = (int)$v['stats']['statuses']; // Filas de la tabla $rows = ''; if (empty($v['logs'])) { $rows = 'Sin eventos para esta fecha.'; } else { foreach ($v['logs'] as $log) { $id = (int)$log['id']; $time = self::h(substr($log['received_at'] ?? '', 11, 8)); $from = self::h($log['from_number'] ?? '—'); $name = self::h($log['contact_name'] ?? '—'); $type = self::typeLabel($log['event_field'] ?? '', $log['message_type'] ?? ''); $preview = self::h(mb_substr($log['message_preview'] ?? '—', 0, 70)); $rows .= "" . "#{$id}" . "{$time}" . "{$from}" . "{$name}" . "{$type}" . "{$preview}" . "" . "\n"; } } // Paginación $pager = ''; if ($v['pages'] > 1) { $pager = '
'; for ($i = 1; $i <= min((int)$v['pages'], 20); $i++) { $qs = '?page=' . $i . '&type=' . urlencode($v['filter']) . '&search=' . urlencode($v['search']) . '&date=' . urlencode($v['date']); $active = $i === (int)$v['page'] ? ' active' : ''; $pager .= "{$i}"; } $pager .= '
'; } echo << Admin — Palmas360

Palmas360 Palmas360  ·  Admin

Somos19D
👤 {$userName} Salir
{$sTotal}
📡 Total hoy
{$sMsgs}
💬 Mensajes texto
{$sMedia}
📎 Multimedia
{$sStatuses}
📊 Estados
{$totalStr} resultado(s)
{$rows}
ID Hora Número Nombre Tipo Preview
{$pager}
HTML; exit; } }