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:
@@ -178,8 +178,26 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||
.result-flash.loss { background: rgba(239,68,68,0.15); border: 1px solid var(--red); color: var(--red); }
|
||||
@keyframes flashIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
/* Bet impact simulator */
|
||||
.impact-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 12px; }
|
||||
.impact-table th, .impact-table td { padding: 8px 10px; text-align: center; border-bottom: 1px solid var(--border); }
|
||||
.impact-table th { color: var(--text2); font-weight: 600; text-transform: uppercase; font-size: 11px; background: var(--surface2); }
|
||||
.impact-rank { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 700; text-transform: uppercase; }
|
||||
.impact-rank.high { background: rgba(239,68,68,0.2); color: var(--red); }
|
||||
.impact-rank.mid { background: rgba(108,92,231,0.2); color: var(--accent); }
|
||||
.impact-rank.low { background: rgba(16,185,129,0.2); color: var(--green); }
|
||||
.impact-headroom { font-weight: 800; font-variant-numeric: tabular-nums; }
|
||||
.impact-recs { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.impact-rec {
|
||||
background: var(--surface2); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 12px 16px; text-align: center;
|
||||
}
|
||||
.impact-rec .rec-label { font-size: 10px; color: var(--text3); font-weight: 600; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.impact-rec .rec-value { font-size: 20px; font-weight: 800; }
|
||||
.impact-rec .rec-note { font-size: 10px; color: var(--text3); margin-top: 4px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pred-cards, .two-col, .stat-cards, .trends-grid, .crowd-stats { grid-template-columns: 1fr; }
|
||||
.pred-cards, .two-col, .stat-cards, .trends-grid, .crowd-stats, .impact-recs { grid-template-columns: 1fr; }
|
||||
.advisor-grid { grid-template-columns: 1fr; gap: 10px; }
|
||||
.backtest-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.pred-card .prob { font-size: 28px; }
|
||||
@@ -229,6 +247,12 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||
<div id="advisor-content" class="advisor-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bet Impact Simulator -->
|
||||
<div id="bet-impact" class="bet-advisor" style="margin-top:12px">
|
||||
<div class="panel-title">Bet Impact Simulator</div>
|
||||
<div id="impact-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel" style="margin-top:8px">
|
||||
<div class="panel-title">Signal Breakdown</div>
|
||||
<table class="signal-table" id="signal-table">
|
||||
@@ -556,6 +580,119 @@ function renderBetAdvisor(data, pot) {
|
||||
${whaleHtml}
|
||||
${pubHtml}
|
||||
`;
|
||||
|
||||
renderBetImpact();
|
||||
}
|
||||
|
||||
// ── Bet Impact Simulator ──
|
||||
function getRank(chair, bets) {
|
||||
const vals = CHAIRS.map(c => bets[c] || 0);
|
||||
const v = bets[chair] || 0;
|
||||
const maxV = Math.max(...vals);
|
||||
const minV = Math.min(...vals);
|
||||
if (maxV === minV) return 'mid';
|
||||
if (v >= maxV) return 'high';
|
||||
if (v <= minV) return 'low';
|
||||
return 'mid';
|
||||
}
|
||||
|
||||
function rankWinRate(rank) {
|
||||
const br = predictionData?.bet_rank || {};
|
||||
const total = (br.high || 0) + (br.mid || 0) + (br.low || 0);
|
||||
if (total === 0) return null;
|
||||
return (br[rank] || 0) / total * 100;
|
||||
}
|
||||
|
||||
function computeHeadroom(chair, bets) {
|
||||
const curRank = getRank(chair, bets);
|
||||
const others = CHAIRS.filter(c => c !== chair).map(c => bets[c] || 0);
|
||||
const myBet = bets[chair] || 0;
|
||||
const maxOther = Math.max(...others);
|
||||
const minOther = Math.min(...others);
|
||||
|
||||
if (curRank === 'low') {
|
||||
// Can add up to (minOther - myBet) before leaving low
|
||||
// If two others are equal, second-lowest is minOther
|
||||
const sorted = others.slice().sort((a, b) => a - b);
|
||||
return Math.max(0, sorted[0] - myBet);
|
||||
} else if (curRank === 'mid') {
|
||||
// Can add up to (maxOther - myBet) before becoming high
|
||||
return Math.max(0, maxOther - myBet);
|
||||
} else {
|
||||
// Already high — headroom is infinite (rank can't go higher)
|
||||
return Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBetImpact() {
|
||||
const el = $('impact-content');
|
||||
const totalBets = liveBets.A + liveBets.B + liveBets.C;
|
||||
if (totalBets === 0) {
|
||||
el.innerHTML = '<div style="color:var(--text3);font-size:12px;padding:8px 0">Waiting for bets to calculate impact...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Table
|
||||
let rows = '';
|
||||
for (const c of CHAIRS) {
|
||||
const curRank = getRank(c, liveBets);
|
||||
const curWR = rankWinRate(curRank);
|
||||
const headroom = computeHeadroom(c, liveBets);
|
||||
|
||||
// What rank would the chair move to?
|
||||
let nextRank = '--';
|
||||
let nextWR = null;
|
||||
let wrChange = '';
|
||||
if (headroom !== Infinity && headroom < 1e9) {
|
||||
// Simulate adding headroom+1
|
||||
const simBets = {...liveBets, [c]: (liveBets[c] || 0) + headroom + 1};
|
||||
const nr = getRank(c, simBets);
|
||||
nextRank = nr;
|
||||
nextWR = rankWinRate(nr);
|
||||
if (curWR !== null && nextWR !== null) {
|
||||
const diff = nextWR - curWR;
|
||||
wrChange = `<span style="color:${diff >= 0 ? 'var(--green)' : 'var(--red)'}">${diff >= 0 ? '+' : ''}${diff.toFixed(1)}%</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
rows += `<tr>
|
||||
<td style="font-weight:700;color:${CHAIR_COLORS[c]}">Chair ${c}</td>
|
||||
<td><span class="impact-rank ${curRank}">${curRank}</span></td>
|
||||
<td>${curWR !== null ? curWR.toFixed(1) + '%' : '--'}</td>
|
||||
<td class="impact-headroom" style="color:${headroom === Infinity ? 'var(--green)' : headroom === 0 ? 'var(--red)' : 'var(--text)'}">${headroom === Infinity ? 'No limit' : fmt(headroom)}</td>
|
||||
<td>${nextRank !== '--' ? `<span class="impact-rank ${nextRank}">${nextRank}</span>` : '--'}</td>
|
||||
<td>${wrChange || '--'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Recommendations: safe bet = 80% of headroom for top pick & 2nd pick
|
||||
const ranked = CHAIRS.slice().sort((a, b) => (predictionData?.prediction?.[b] || 0) - (predictionData?.prediction?.[a] || 0));
|
||||
const best = ranked[0], second = ranked[1];
|
||||
const headBest = computeHeadroom(best, liveBets);
|
||||
const headSecond = computeHeadroom(second, liveBets);
|
||||
const safeBest = headBest === Infinity ? 'No limit' : fmt(Math.floor(headBest * 0.8));
|
||||
const safeSecond = headSecond === Infinity ? 'No limit' : fmt(Math.floor(headSecond * 0.8));
|
||||
const bestRank = getRank(best, liveBets);
|
||||
const secondRank = getRank(second, liveBets);
|
||||
|
||||
el.innerHTML = `
|
||||
<table class="impact-table">
|
||||
<thead><tr><th>Chair</th><th>Rank</th><th>Win Rate</th><th>Max Bet to Keep Rank</th><th>Next Rank</th><th>Win Rate Change</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
<div class="impact-recs">
|
||||
<div class="impact-rec">
|
||||
<div class="rec-label">Safe Bet on ${best} (Top Pick)</div>
|
||||
<div class="rec-value" style="color:${CHAIR_COLORS[best]}">${safeBest}</div>
|
||||
<div class="rec-note">80% of headroom · keeps <span class="impact-rank ${bestRank}" style="font-size:9px">${bestRank}</span> rank</div>
|
||||
</div>
|
||||
<div class="impact-rec">
|
||||
<div class="rec-label">Safe Bet on ${second} (2nd Pick)</div>
|
||||
<div class="rec-value" style="color:${CHAIR_COLORS[second]}">${safeSecond}</div>
|
||||
<div class="rec-note">80% of headroom · keeps <span class="impact-rank ${secondRank}" style="font-size:9px">${secondRank}</span> rank</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Whale & Public Trends ──
|
||||
|
||||
Reference in New Issue
Block a user