add bet impact simulator, visitor log page, and fix console logging
- 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
This commit is contained in:
98
static/visitors.html
Normal file
98
static/visitors.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user