Advertise the jackpot that will actually be paid (B-65)

participant_count and jackpot_sats were computed over every round_participants
row, while the draw only picks from confirmed participants and the payout only
spends their sats. So the advertised jackpot could exceed the one paid out, and a
player whose bet was later abandoned appeared in the count and then vanished
again.

Counting only confirmed rows would have fixed the arithmetic and broken something
else: the player who just bet would see neither themselves nor their money for a
whole block. So this is the same confirmed/in-flight split the balance already
exposes (balance_sats vs pending_balance_sats): participant_count and
jackpot_sats are now the confirmed, authoritative figures, and
pending_participant_count/pending_jackpot_sats/has_pending_bets report what is in
flight — inclusive figures, not deltas, matching the balance pair's convention.

/'s round card shows the confirmed numbers big and the difference as an amber
"+N in attesa" suffix, reusing .balance-pending's colour for the same "not
settled yet" meaning. The two new spans render from server data through t(), so
they carry no data-i18n and onLanguageChange() repaints them from the last
response — the one-mechanism-per-element rule. Both strings are in all 7
languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-04 11:52:10 +02:00
co-authored by Claude Opus 5
parent c0314e2bf0
commit c4b2dc3ea2
8 changed files with 167 additions and 32 deletions
+48 -10
View File
@@ -368,6 +368,49 @@ async function checkLastRoundResult() {
let lastJackpotValue = null;
// The round's own confirmed/in-flight split (B-65). The big numbers are the
// confirmed ones — the players the draw will pick from and the pool the payout
// will actually spend — because a jackpot advertised larger than the one paid out
// is the kind of gap nobody forgives. What has been bet but hasn't confirmed yet
// is shown next to them instead of being folded in, so the player who just bet
// still sees their own bet immediately (the same reasoning as the amber
// pending balance, see setBalanceDisplay).
//
// Kept out of the [data-i18n] mechanism on purpose: these come from server data,
// so onLanguageChange() re-renders them through t() like every other dynamic bit.
let lastRoundStats = null;
function renderRoundStats(data) {
lastRoundStats = data;
document.getElementById('round-players').textContent = data.participant_count;
const jackpotEl = document.getElementById('round-jackpot');
const jackpotValue = data.jackpot_sats / SATS_PER_PLM;
jackpotEl.textContent = formatPlm(data.jackpot_sats);
if (lastJackpotValue !== null && jackpotValue !== lastJackpotValue) {
jackpotEl.classList.remove('jackpot-bump');
void jackpotEl.offsetWidth; // restart the animation
jackpotEl.classList.add('jackpot-bump');
}
lastJackpotValue = jackpotValue;
// pending_* are inclusive of the confirmed figures (like pending_balance_sats),
// so what's shown alongside is the difference.
const pendingPlayers = (data.pending_participant_count || 0) - data.participant_count;
const pendingJackpotSats = (data.pending_jackpot_sats || 0) - data.jackpot_sats;
const show = !!data.has_pending_bets && pendingPlayers > 0;
const playersPendingEl = document.getElementById('round-players-pending');
playersPendingEl.textContent = show ? t('round.playersPending', { n: pendingPlayers }) : '';
playersPendingEl.classList.toggle('hidden', !show);
const jackpotPendingEl = document.getElementById('round-jackpot-pending');
jackpotPendingEl.textContent = show
? t('round.jackpotPending', { amount: formatPlm(pendingJackpotSats) })
: '';
jackpotPendingEl.classList.toggle('hidden', !show);
}
let timerHitZero = false;
function updateRoundTimer() {
@@ -451,16 +494,7 @@ async function refreshRound() {
: t('round.none');
betAmountSats = data.bet_amount_sats;
renderBetButton();
document.getElementById('round-players').textContent = data.participant_count;
const jackpotEl = document.getElementById('round-jackpot');
const jackpotValue = data.jackpot_sats / SATS_PER_PLM;
jackpotEl.textContent = formatPlm(data.jackpot_sats);
if (lastJackpotValue !== null && jackpotValue !== lastJackpotValue) {
jackpotEl.classList.remove('jackpot-bump');
void jackpotEl.offsetWidth; // restart the animation
jackpotEl.classList.add('jackpot-bump');
}
lastJackpotValue = jackpotValue;
renderRoundStats(data);
if (data.server_time) serverTimeOffsetMs = new Date(data.server_time) - new Date();
roundCloseAt = data.closes_at ? new Date(data.closes_at) : null;
updateRoundTimer();
@@ -858,6 +892,10 @@ function connectRoundEvents() {
function onLanguageChange() {
renderBetButton();
renderChainStatusBar(); // repaints from remembered state, without waiting for the next poll
// Same reason, for the round's "+N in attesa" suffixes (B-65): repaint from what
// was last received rather than leaving them in the old language until the
// refreshRound() below happens to come back.
if (lastRoundStats) renderRoundStats(lastRoundStats);
if (token) {
refreshRound();
refreshMe();
+14
View File
@@ -63,6 +63,8 @@ const TRANSLATIONS = {
'round.players': 'Players',
'round.jackpot': 'Jackpot',
'round.playersPending': '+{n} pending',
'round.jackpotPending': '+{amount} pending',
'round.status.open': 'open',
'round.status.closing': 'closing',
'round.status.drawing': 'drawing in progress',
@@ -228,6 +230,8 @@ const TRANSLATIONS = {
'round.players': 'Giocatori',
'round.jackpot': 'Jackpot',
'round.playersPending': '+{n} in attesa',
'round.jackpotPending': '+{amount} in attesa',
'round.status.open': 'aperto',
'round.status.closing': 'in chiusura',
'round.status.drawing': 'estrazione in corso',
@@ -390,6 +394,8 @@ const TRANSLATIONS = {
'round.players': 'Jugadores',
'round.jackpot': 'Bote',
'round.playersPending': '+{n} pendientes',
'round.jackpotPending': '+{amount} pendientes',
'round.status.open': 'abierta',
'round.status.closing': 'cerrando',
'round.status.drawing': 'sorteo en curso',
@@ -552,6 +558,8 @@ const TRANSLATIONS = {
'round.players': 'Joueurs',
'round.jackpot': 'Jackpot',
'round.playersPending': '+{n} en attente',
'round.jackpotPending': '+{amount} en attente',
'round.status.open': 'ouvert',
'round.status.closing': 'en fermeture',
'round.status.drawing': 'tirage en cours',
@@ -714,6 +722,8 @@ const TRANSLATIONS = {
'round.players': 'Spieler',
'round.jackpot': 'Jackpot',
'round.playersPending': '+{n} ausstehend',
'round.jackpotPending': '+{amount} ausstehend',
'round.status.open': 'offen',
'round.status.closing': 'wird geschlossen',
'round.status.drawing': 'Ziehung läuft',
@@ -876,6 +886,8 @@ const TRANSLATIONS = {
'round.players': 'Игроки',
'round.jackpot': 'Джекпот',
'round.playersPending': '+{n} в ожидании',
'round.jackpotPending': '+{amount} в ожидании',
'round.status.open': 'открыт',
'round.status.closing': 'закрывается',
'round.status.drawing': 'идёт розыгрыш',
@@ -1038,6 +1050,8 @@ const TRANSLATIONS = {
'round.players': '参与人数',
'round.jackpot': '奖池',
'round.playersPending': '+{n} 待确认',
'round.jackpotPending': '+{amount} 待确认',
'round.status.open': '进行中',
'round.status.closing': '即将结束',
'round.status.drawing': '正在开奖',
+5 -1
View File
@@ -150,11 +150,15 @@
<div class="row-between" style="margin-top:10px" id="round-stats-row">
<div>
<div class="hint" style="margin-bottom:2px" data-i18n="round.players">Giocatori</div>
<span class="mono" id="round-players"></span>
<!-- The two -pending spans are filled from server data by renderRoundStats()
(B-65), so they carry no data-i18n: an element belongs to one
translation mechanism or the other, never both. -->
<span class="mono" id="round-players"></span> <span class="pending-suffix hidden" id="round-players-pending"></span>
</div>
<div style="text-align:right">
<div class="hint" style="margin-bottom:2px" data-i18n="round.jackpot">Jackpot</div>
<span class="mono" id="round-jackpot"></span> <span class="balance-unit">PLM</span>
<span class="pending-suffix hidden" id="round-jackpot-pending"></span>
</div>
</div>
+6
View File
@@ -215,6 +215,12 @@ button.link:hover, a.link:hover { filter: none; color: var(--color-foreground);
.balance-confirmed { color: var(--color-success); }
.balance-pending { color: var(--color-primary); }
/* The in-flight part of the round's own figures (B-65): the players/jackpot next
to it are the confirmed ones the draw and the payout will actually use, and this
is what has been bet but hasn't confirmed yet. Same amber as .balance-pending,
for the same "not settled" meaning. */
.pending-suffix { color: var(--color-primary); font-size: 0.8rem; font-weight: 600; }
.icon { width: 16px; height: 16px; flex-shrink: 0; }
.dash-panel { display: none; }