197 lines
6.8 KiB
HTML
197 lines
6.8 KiB
HTML
<!doctype html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Webhook Logs - Tiempo Real</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
pre.json { max-height: 60vh; overflow: auto; background: #f8f9fa; padding: 1rem; border-radius: 6px; }
|
|
.new-row { animation: highlight 1.5s ease; }
|
|
@keyframes highlight { from { background: #e6ffed } to { background: transparent } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h3><i class="fab fa-whatsapp text-success"></i> Webhook Logs (Tiempo Real)</h3>
|
|
<div>
|
|
<input id="searchInput" class="form-control d-inline-block" style="width:260px" placeholder="Buscar ID o texto...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<button class="btn btn-sm btn-outline-secondary" id="refreshBtn">Actualizar</button>
|
|
<label class="ms-3">Auto-refresh: <input id="autoRefresh" type="checkbox" checked></label>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body p-2">
|
|
<table class="table table-sm table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Timestamp</th>
|
|
<th>Snippet</th>
|
|
<th>IP</th>
|
|
<th>Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logsTable">
|
|
<tr><td colspan="5" class="text-center py-4 text-muted">Cargando...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5>Payload (crudo)</h5>
|
|
<pre id="rawPayload" class="json">Selecciona un registro para ver el payload completo</pre>
|
|
<div class="mt-2 d-flex gap-2">
|
|
<button id="downloadBtn" class="btn btn-sm btn-outline-primary" disabled>Descargar JSON</button>
|
|
<button id="replayBtn" class="btn btn-sm btn-outline-success" disabled>Reproducir (Simular)</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 text-muted">Nota: Esta vista muestra los webhooks crudos como llegaron y se actualiza cada 3s (auto-refresh).</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"></script>
|
|
<script>
|
|
const API_LIST = 'api/get_webhook_logs.php?debug=true';
|
|
const API_GET = 'api/get_webhook_log.php?debug=true';
|
|
let auto = true;
|
|
let latestId = 0;
|
|
let pollingInterval = 3000;
|
|
let lastSelected = null;
|
|
|
|
async function fetchLogs(sinceId = 0, search = '') {
|
|
const params = new URLSearchParams();
|
|
if (sinceId) params.set('since_id', sinceId);
|
|
if (search) params.set('search', search);
|
|
params.set('limit', 30);
|
|
|
|
const res = await fetch(API_LIST + '&' + params.toString());
|
|
return await res.json();
|
|
}
|
|
|
|
async function refresh(full = true) {
|
|
const search = document.getElementById('searchInput').value.trim();
|
|
try {
|
|
const data = await fetchLogs(full ? 0 : latestId, search);
|
|
if (!data.success) { console.error(data); return; }
|
|
|
|
const rows = data.data || [];
|
|
if (!full && rows.length) {
|
|
// append new rows
|
|
prependRows(rows.reverse()); // since API returns ascending when since_id used
|
|
} else {
|
|
renderFullTable(rows);
|
|
}
|
|
|
|
if (rows.length) {
|
|
latestId = Math.max(...rows.map(r => r.id), latestId);
|
|
}
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
function renderFullTable(rows) {
|
|
const tbody = document.getElementById('logsTable');
|
|
if (!rows || rows.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No hay registros</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = rows.map(r => rowHtml(r)).join('');
|
|
}
|
|
|
|
function prependRows(rows) {
|
|
const tbody = document.getElementById('logsTable');
|
|
rows.forEach(r => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = rowHtml(r);
|
|
tr.classList.add('new-row');
|
|
tbody.insertBefore(tr, tbody.firstChild);
|
|
});
|
|
}
|
|
|
|
function rowHtml(r) {
|
|
const snippet = (r.snippet || '').replace(/</g, '<').substring(0, 120);
|
|
return `
|
|
<tr data-id="${r.id}" onclick="selectLog(${r.id}, this)">
|
|
<td>${r.id}</td>
|
|
<td>${r.created_at}</td>
|
|
<td><small class="text-muted">${snippet}</small></td>
|
|
<td>${r.ip_address || ''}</td>
|
|
<td><button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); viewRaw(${r.id})">Ver</button></td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
async function viewRaw(id) {
|
|
const res = await fetch(API_GET + '&id=' + id);
|
|
const data = await res.json();
|
|
if (!data.success) { alert(data.error || 'Error'); return; }
|
|
const payload = data.data;
|
|
lastSelected = payload;
|
|
document.getElementById('rawPayload').textContent = payload.request_body_pretty || payload.request_body || '';
|
|
document.getElementById('downloadBtn').disabled = false;
|
|
document.getElementById('replayBtn').disabled = false;
|
|
}
|
|
|
|
function selectLog(id, el) {
|
|
// highlight
|
|
const rows = document.querySelectorAll('#logsTable tr');
|
|
rows.forEach(r => r.classList.remove('table-active'));
|
|
el.classList.add('table-active');
|
|
viewRaw(id);
|
|
}
|
|
|
|
function downloadSelected() {
|
|
if (!lastSelected) return;
|
|
const blob = new Blob([lastSelected.request_body || ''], {type: 'application/json'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `webhook_${lastSelected.id}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
async function replaySelected() {
|
|
if (!lastSelected) return alert('Selecciona un registro primero');
|
|
try {
|
|
const payload = lastSelected.request_body;
|
|
const resp = await fetch('api/webhook_replay.php', {method: 'POST', headers: {'Content-Type':'application/json'}, body: payload});
|
|
const data = await resp.json();
|
|
alert(data.success ? 'Reproducción ejecutada' : ('Error: ' + (data.error || '')));
|
|
} catch (e) {
|
|
alert('Error reproduciendo: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
document.getElementById('refreshBtn').addEventListener('click', () => refresh(true));
|
|
document.getElementById('autoRefresh').addEventListener('change', (e) => { auto = e.target.checked; });
|
|
document.getElementById('downloadBtn').addEventListener('click', downloadSelected);
|
|
document.getElementById('replayBtn').addEventListener('click', replaySelected);
|
|
document.getElementById('searchInput').addEventListener('input', (e) => { setTimeout(()=>refresh(true), 350); });
|
|
|
|
// Polling loop
|
|
setInterval(() => { if (!auto) return; refresh(false); }, pollingInterval);
|
|
|
|
// Initial load
|
|
refresh(true);
|
|
</script>
|
|
</body>
|
|
</html> |