- Bet impact simulator on /predictions shows rank headroom and safe bet amounts - Password-protected /visitors page with visitor log table and stats - Console now logs real visitor IPs instead of Cloudflare tunnel IPs
99 lines
4.2 KiB
HTML
99 lines
4.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Visitor Log</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1117; --surface: #1a1d27; --surface2: #232736;
|
|
--border: #2d3148; --text: #e4e6f0; --text2: #8b8fa3; --text3: #5a5f75;
|
|
--accent: #6c5ce7; --green: #10b981; --red: #ef4444;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); }
|
|
|
|
.header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 10px 20px; background: var(--surface); border-bottom: 1px solid var(--border);
|
|
}
|
|
.header h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; }
|
|
.nav-links { display: flex; gap: 14px; }
|
|
.nav-link { font-size: 12px; color: var(--accent); text-decoration: none; font-weight: 600; }
|
|
.nav-link:hover { color: #a78bfa; }
|
|
|
|
.content { padding: 16px 20px; max-width: 1400px; margin: 0 auto; }
|
|
|
|
.stats-bar {
|
|
display: flex; gap: 24px; margin-bottom: 16px; padding: 14px 20px;
|
|
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
}
|
|
.stat-item { text-align: center; }
|
|
.stat-item .s-label { font-size: 10px; color: var(--text3); font-weight: 600; text-transform: uppercase; margin-bottom: 2px; }
|
|
.stat-item .s-value { font-size: 22px; font-weight: 800; }
|
|
|
|
.table-wrap {
|
|
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
overflow: auto; max-height: calc(100vh - 180px);
|
|
}
|
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
th { background: var(--surface2); color: var(--text2); font-weight: 600; text-transform: uppercase; font-size: 11px; position: sticky; top: 0; z-index: 1; }
|
|
td { color: var(--text); }
|
|
tr:hover td { background: var(--surface2); }
|
|
.loading { text-align: center; padding: 60px; color: var(--text2); font-size: 14px; }
|
|
.ua { max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Visitor Log</h1>
|
|
<div class="nav-links">
|
|
<a href="/" class="nav-link">Dashboard</a>
|
|
<a href="/predictions" class="nav-link">Predictions</a>
|
|
<a href="/analytics" class="nav-link">Analytics</a>
|
|
<a href="/patterns" class="nav-link">Patterns</a>
|
|
</div>
|
|
</div>
|
|
<div class="content">
|
|
<div id="stats" class="stats-bar" style="display:none"></div>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Time</th><th>IP</th><th>Country</th><th>Method</th><th>Path</th><th>User Agent</th></tr></thead>
|
|
<tbody id="tbody"><tr><td colspan="6" class="loading">Loading...</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
fetch('/api/visitors?limit=200')
|
|
.then(r => { if (r.status === 401) throw new Error('Auth required'); return r.json(); })
|
|
.then(data => {
|
|
const tbody = document.getElementById('tbody');
|
|
if (!data.length) { tbody.innerHTML = '<tr><td colspan="6" style="color:#5a5f75;text-align:center;padding:40px">No visitors yet</td></tr>'; return; }
|
|
|
|
const ips = new Set(data.map(v => v.ip));
|
|
const countries = new Set(data.filter(v => v.country).map(v => v.country));
|
|
const stats = document.getElementById('stats');
|
|
stats.style.display = 'flex';
|
|
stats.innerHTML = `
|
|
<div class="stat-item"><div class="s-label">Total Visits</div><div class="s-value">${data.length}</div></div>
|
|
<div class="stat-item"><div class="s-label">Unique IPs</div><div class="s-value" style="color:#10b981">${ips.size}</div></div>
|
|
<div class="stat-item"><div class="s-label">Countries</div><div class="s-value" style="color:#6c5ce7">${countries.size}</div></div>
|
|
`;
|
|
|
|
tbody.innerHTML = data.map(v => `<tr>
|
|
<td>${v.created_at}</td>
|
|
<td>${v.ip}</td>
|
|
<td>${v.country || '--'}</td>
|
|
<td>${v.method}</td>
|
|
<td>${v.path}</td>
|
|
<td class="ua" title="${v.user_agent.replace(/"/g, '"')}">${v.user_agent}</td>
|
|
</tr>`).join('');
|
|
})
|
|
.catch(err => {
|
|
document.getElementById('tbody').innerHTML = `<tr><td colspan="6" style="color:#ef4444;text-align:center;padding:40px">${err.message}</td></tr>`;
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|