Files
3pmonitor/static/analytics.html
Junaid Saeed Uppal b07b073cc0 add predictions page with game theory analysis and card stats
Bayesian next-chair predictor (Markov chains, base rate, streak regression),
statistical tests (chi-squared, runs test, autocorrelation), theory
backtesting with rolling accuracy, and card-level analysis (value/suit
distribution, face card frequency, top winning cards).
2026-02-25 23:16:37 +05:00

584 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Teen Patti Analytics</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #232736;
--surface3: #2a2e42;
--border: #2d3148;
--text: #e4e6f0;
--text2: #8b8fa3;
--text3: #5a5f75;
--accent: #6c5ce7;
--chair-a: #3b82f6;
--chair-b: #ec4899;
--chair-c: #f59e0b;
--green: #10b981;
--red: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* Header */
.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-link {
font-size: 12px; color: var(--accent); text-decoration: none;
font-weight: 600; transition: color 0.2s;
}
.nav-link:hover { color: #a78bfa; }
/* Period Selector */
.period-bar {
display: flex; align-items: center; gap: 6px;
padding: 10px 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.period-label { font-size: 11px; color: var(--text2); font-weight: 600; margin-right: 4px; }
.period-btn {
padding: 4px 12px; border-radius: 4px; font-size: 11px;
font-weight: 700; border: 1px solid var(--border);
background: var(--surface2); color: var(--text2);
cursor: pointer; transition: all 0.2s;
}
.period-btn:hover { border-color: var(--accent); color: var(--text); }
.period-btn.active {
background: var(--accent); border-color: var(--accent); color: #fff;
}
.loading-indicator {
font-size: 11px; color: var(--text3); margin-left: auto;
}
/* Content */
.content { padding: 16px 20px; max-width: 1200px; margin: 0 auto; }
/* Summary Cards */
.summary-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.summary-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
text-align: center;
}
.summary-card-label {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px;
color: var(--text3); font-weight: 700;
}
.summary-card-value {
font-size: 22px; font-weight: 800; margin-top: 4px;
font-variant-numeric: tabular-nums;
}
/* Section */
.section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
margin-bottom: 16px;
}
.section-title {
font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px;
color: var(--text2); margin-bottom: 10px; font-weight: 700;
}
/* Chart */
.chart-container { position: relative; height: 200px; }
/* Two column layout */
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
/* Horizontal Bar */
.hbar-row {
display: flex; align-items: center; gap: 8px;
padding: 5px 0; font-size: 13px;
}
.hbar-label {
font-weight: 700; min-width: 80px; font-size: 12px;
}
.hbar-bg {
flex: 1; height: 22px; border-radius: 4px;
background: var(--surface3); overflow: hidden;
}
.hbar-fill {
height: 100%; border-radius: 4px;
transition: width 0.4s ease; min-width: 2px;
}
.hbar-value {
font-weight: 700; min-width: 60px; text-align: right;
font-variant-numeric: tabular-nums; font-size: 12px;
}
.hbar-pct {
font-size: 11px; color: var(--text3); min-width: 40px; text-align: right;
}
/* Leaderboard Table */
.lb-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.lb-table th {
text-align: left; padding: 6px 8px;
border-bottom: 2px solid var(--border);
color: var(--text3); font-size: 10px;
text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;
}
.lb-table td { padding: 5px 8px; border-bottom: 1px solid #2d314830; }
.lb-table tr:hover { background: var(--surface2); }
.lb-table .positive { color: var(--green); }
.lb-table .negative { color: var(--red); }
.lb-table .rank-col { width: 30px; color: var(--text3); font-weight: 700; }
.lb-table .name-col {
max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lb-table .num-col {
text-align: right; font-variant-numeric: tabular-nums; font-weight: 600;
}
.lb-table .pnl-col {
text-align: right; font-variant-numeric: tabular-nums; font-weight: 800;
}
/* History Table */
.history-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.history-table th {
text-align: left; padding: 5px 6px;
border-bottom: 2px solid var(--border);
color: var(--text3); font-size: 10px;
text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;
}
.history-table td { padding: 4px 6px; border-bottom: 1px solid #2d314830; }
.history-table tr:hover { background: var(--surface2); }
.winner-cell { font-weight: 800; }
.winner-A { color: var(--chair-a); }
.winner-B { color: var(--chair-b); }
.winner-C { color: var(--chair-c); }
.history-hand { font-family: 'SF Mono', Consolas, monospace; font-size: 11px; white-space: nowrap; }
.history-hand.winning-hand { font-weight: 700; }
.history-hand.losing-hand { opacity: 0.4; }
.history-pot { font-size: 9px; color: var(--text3); font-variant-numeric: tabular-nums; margin-right: 3px; }
.hand-tag {
font-size: 9px; padding: 1px 4px; border-radius: 3px;
margin-left: 4px; font-weight: 600; display: inline-block;
vertical-align: middle;
}
.hand-tag-win { background: #10b98120; color: var(--green); }
.hand-tag-type { background: var(--surface3); color: var(--text2); }
.card-red { color: #ef4444; }
.card-white { color: #e4e6f0; }
.bet-rank {
font-size: 9px; padding: 2px 6px; border-radius: 3px;
font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase;
}
.bet-rank-low { background: #10b98125; color: #34d399; }
.bet-rank-mid { background: #f59e0b25; color: #fbbf24; }
.bet-rank-high { background: #ef444425; color: #f87171; }
.history-scroll { max-height: 500px; overflow-y: auto; }
@media (max-width: 768px) {
.summary-row { grid-template-columns: repeat(3, 1fr); }
.two-col { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<h1>Teen Patti Analytics</h1>
<div style="display:flex;gap:14px">
<a href="/" class="nav-link">Live Dashboard &rarr;</a>
<a href="/patterns" class="nav-link">Patterns &rarr;</a>
<a href="/predictions" class="nav-link">Predictions &rarr;</a>
</div>
</div>
<div class="period-bar">
<span class="period-label">Period:</span>
<button class="period-btn" data-period="1h">1H</button>
<button class="period-btn" data-period="6h">6H</button>
<button class="period-btn" data-period="24h">24H</button>
<button class="period-btn" data-period="7d">7D</button>
<button class="period-btn active" data-period="all">ALL</button>
<span class="loading-indicator" id="loading" style="display:none">Loading...</span>
</div>
<div class="content">
<!-- Summary Cards -->
<div class="summary-row">
<div class="summary-card">
<div class="summary-card-label">Total Games</div>
<div class="summary-card-value" id="s-games">-</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Volume</div>
<div class="summary-card-value" id="s-volume">-</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Avg Pot</div>
<div class="summary-card-value" id="s-avg-pot">-</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Bets Placed</div>
<div class="summary-card-value" id="s-bets">-</div>
</div>
<div class="summary-card">
<div class="summary-card-label">Unique Bettors</div>
<div class="summary-card-value" id="s-bettors">-</div>
</div>
</div>
<!-- Volume Over Time Chart -->
<div class="section">
<div class="section-title">Volume Over Time</div>
<div class="chart-container">
<canvas id="volume-chart"></canvas>
</div>
</div>
<!-- Win Distribution + Hand Type Distribution -->
<div class="two-col">
<div class="section">
<div class="section-title">Win Distribution</div>
<div id="win-dist"></div>
<div class="section-title" style="margin-top:14px">Winner Bet Rank</div>
<div id="bet-rank-dist"></div>
</div>
<div class="section">
<div class="section-title">Hand Type Distribution</div>
<div id="hand-type-dist"></div>
</div>
</div>
<!-- Leaderboard -->
<div class="section">
<div class="section-title">Leaderboard (Top 20 by P&amp;L)</div>
<table class="lb-table">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th style="text-align:right">Bets</th>
<th style="text-align:right">Wins</th>
<th style="text-align:right">W%</th>
<th style="text-align:right">P&amp;L</th>
<th style="text-align:right">Wagered</th>
</tr>
</thead>
<tbody id="lb-body"></tbody>
</table>
</div>
<!-- Game History -->
<div class="section">
<div class="section-title">Game History</div>
<div class="history-scroll">
<table class="history-table">
<thead>
<tr>
<th>#</th>
<th>W</th>
<th>Pot</th>
<th>Bet</th>
<th>Hand A</th>
<th>Hand B</th>
<th>Hand C</th>
</tr>
</thead>
<tbody id="history-body"></tbody>
</table>
</div>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
const fmt = n => {
if (n === null || n === undefined) return '0';
const abs = Math.abs(n);
if (abs >= 1e6) return (n/1e6).toFixed(1) + 'M';
if (abs >= 1e3) return (n/1e3).toFixed(1) + 'K';
return String(n);
};
const fmtFull = n => {
if (n === null || n === undefined) return '0';
return Number(n).toLocaleString();
};
const escHtml = s => {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
};
const CHAIRS = {1:'C', 2:'B', 3:'A'};
const CHAIR_COLORS = {A:'#3b82f6', B:'#ec4899', C:'#f59e0b'};
const HAND_TYPES = {1:'High Card', 2:'Pair', 3:'Flush', 4:'Straight', 5:'Str. Flush', 6:'Trail'};
function colorCard(text) {
return text.replace(/([^\s]+)/g, m => {
if (m.includes('\u2665') || m.includes('\u2666'))
return `<span class="card-red">${m}</span>`;
return `<span class="card-white">${m}</span>`;
});
}
function getBetRank(betA, betB, betC, winnerChair) {
const bets = [betA || 0, betB || 0, betC || 0];
const winIdx = winnerChair === 'A' ? 0 : winnerChair === 'B' ? 1 : 2;
const winBet = bets[winIdx];
if (winBet === 0) return null;
const sorted = [...bets].sort((a, b) => a - b);
if (winBet <= sorted[0]) return 'low';
if (winBet >= sorted[2]) return 'high';
return 'mid';
}
function betRankCell(rank) {
if (!rank) return '<td>&mdash;</td>';
return `<td><span class="bet-rank bet-rank-${rank}">${rank}</span></td>`;
}
function buildHandCell(handStr, handTypeId, isWinner, chairBet) {
if (!handStr) return '<td>&mdash;</td>';
const cls = isWinner ? 'history-hand winning-hand' : 'history-hand losing-hand';
const typeName = HAND_TYPES[handTypeId] || '';
let tag = '';
if (isWinner) {
tag = `<span class="hand-tag hand-tag-win">WIN</span>`;
} else if (handTypeId >= 2) {
tag = `<span class="hand-tag hand-tag-type">${typeName}</span>`;
}
const potStr = chairBet ? `<span class="history-pot">${fmt(chairBet)}</span>` : '';
return `<td>${potStr}<span class="${cls}">${colorCard(handStr)}</span>${tag}</td>`;
}
// ── Chart ──
let volumeChart;
function initChart() {
const ctx = $('volume-chart').getContext('2d');
volumeChart = new Chart(ctx, {
type: 'bar',
data: {
labels: [],
datasets: [
{
label: 'Games',
data: [],
backgroundColor: '#6c5ce740',
borderColor: '#6c5ce7',
borderWidth: 1,
yAxisID: 'y',
order: 2,
},
{
label: 'Volume',
data: [],
type: 'line',
borderColor: '#10b981',
backgroundColor: '#10b98118',
borderWidth: 2,
pointRadius: 2,
fill: true,
tension: 0.3,
yAxisID: 'y1',
order: 1,
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 300 },
plugins: {
legend: { labels: { color: '#8b8fa3', font: { size: 10 }, boxWidth: 12 } },
},
scales: {
x: {
ticks: { color: '#5a5f75', font: { size: 9 }, maxRotation: 45 },
grid: { color: '#2d314820' },
},
y: {
position: 'left',
title: { display: true, text: 'Games', color: '#5a5f75', font: { size: 10 } },
ticks: { color: '#5a5f75', font: { size: 10 } },
grid: { color: '#2d314830' },
},
y1: {
position: 'right',
title: { display: true, text: 'Volume', color: '#5a5f75', font: { size: 10 } },
ticks: { color: '#5a5f75', callback: v => fmt(v), font: { size: 10 } },
grid: { drawOnChartArea: false },
},
}
}
});
}
// ── Rendering ──
function renderSummary(s) {
$('s-games').textContent = fmtFull(s.total_games);
$('s-volume').textContent = fmt(s.total_volume);
$('s-avg-pot').textContent = fmt(s.avg_pot);
$('s-bets').textContent = fmtFull(s.total_bets_placed);
$('s-bettors').textContent = fmtFull(s.unique_bettors);
}
function renderVolumeChart(hourly) {
const labels = hourly.map(h => {
const d = new Date(h.hour);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
});
volumeChart.data.labels = labels;
volumeChart.data.datasets[0].data = hourly.map(h => h.games);
volumeChart.data.datasets[1].data = hourly.map(h => h.volume);
volumeChart.update();
}
function renderHBar(container, items, colorFn) {
const total = items.reduce((s, i) => s + i.value, 0) || 1;
const max = Math.max(...items.map(i => i.value)) || 1;
container.innerHTML = items.map(item => {
const pct = (item.value / max * 100).toFixed(0);
const totalPct = (item.value / total * 100).toFixed(1);
const color = colorFn(item.label);
return `
<div class="hbar-row">
<span class="hbar-label" style="color:${color}">${item.label}</span>
<div class="hbar-bg">
<div class="hbar-fill" style="width:${pct}%;background:${color}"></div>
</div>
<span class="hbar-value">${fmtFull(item.value)}</span>
<span class="hbar-pct">${totalPct}%</span>
</div>
`;
}).join('');
}
function renderWinDist(dist) {
const chairs = dist.chairs;
renderHBar($('win-dist'), [
{ label: 'A', value: chairs.A || 0 },
{ label: 'B', value: chairs.B || 0 },
{ label: 'C', value: chairs.C || 0 },
], l => CHAIR_COLORS[l]);
const br = dist.bet_rank;
renderHBar($('bet-rank-dist'), [
{ label: 'High', value: br.high || 0 },
{ label: 'Mid', value: br.mid || 0 },
{ label: 'Low', value: br.low || 0 },
], l => l === 'High' ? '#f87171' : l === 'Mid' ? '#fbbf24' : '#34d399');
}
function renderHandTypes(dist) {
const order = ['Trail', 'Straight Flush', 'Straight', 'Flush', 'Pair', 'High Card'];
const items = order
.filter(t => dist[t] !== undefined)
.map(t => ({ label: t, value: dist[t] }));
renderHBar($('hand-type-dist'), items, () => '#6c5ce7');
}
function renderLeaderboard(leaders) {
const body = $('lb-body');
body.innerHTML = leaders.map((l, i) => {
const pnlClass = l.pnl >= 0 ? 'positive' : 'negative';
const pnlSign = l.pnl >= 0 ? '+' : '';
const wr = l.total_bets > 0 ? Math.round(l.wins / l.total_bets * 100) : 0;
return `
<tr>
<td class="rank-col">${i + 1}</td>
<td class="name-col">${escHtml(l.nick_name)}</td>
<td class="num-col">${l.total_bets}</td>
<td class="num-col">${l.wins}</td>
<td class="num-col">${wr}%</td>
<td class="pnl-col ${pnlClass}">${pnlSign}${fmtFull(l.pnl)}</td>
<td class="num-col">${fmt(l.total_wagered)}</td>
</tr>
`;
}).join('');
}
function renderGames(games) {
const body = $('history-body');
body.innerHTML = games.map(g => {
const w = CHAIRS[g.winner] || '?';
const rank = getBetRank(g.bet_a, g.bet_b, g.bet_c, w);
return `
<tr>
<td>${g.game_no}</td>
<td class="winner-cell winner-${w}">${w}</td>
<td>${fmt(g.total_pot)}</td>
${betRankCell(rank)}
${buildHandCell(g.hand_a, g.hand_type_a, w === 'A', g.bet_a)}
${buildHandCell(g.hand_b, g.hand_type_b, w === 'B', g.bet_b)}
${buildHandCell(g.hand_c, g.hand_type_c, w === 'C', g.bet_c)}
</tr>
`;
}).join('');
}
// ── Data Fetching ──
let currentPeriod = 'all';
async function fetchAnalytics(period) {
currentPeriod = period;
$('loading').style.display = 'inline';
// Update active button
document.querySelectorAll('.period-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.period === period);
});
try {
const resp = await fetch(`/api/analytics?period=${period}`);
const data = await resp.json();
renderSummary(data.summary);
renderVolumeChart(data.hourly_volume);
renderWinDist(data.win_distribution);
renderHandTypes(data.hand_type_distribution);
renderLeaderboard(data.leaderboard);
renderGames(data.games);
} catch (e) {
console.error('Failed to fetch analytics:', e);
} finally {
$('loading').style.display = 'none';
}
}
// ── Init ──
document.querySelectorAll('.period-btn').forEach(btn => {
btn.addEventListener('click', () => fetchAnalytics(btn.dataset.period));
});
initChart();
fetchAnalytics('all');
</script>
</body>
</html>