Snapshot a round's timing when it opens (B-61)

round_duration_seconds was read live on every scheduler tick and every bet
check, with the deadline computed as opened_at + duration. Lowering it from 600
to 60 while a round was 300s in closed that round instantly; raising it moved
the closes_at clients were already counting down to. round_cooldown_seconds had
the same property for the gap after a close. B-11 fixed this class of problem
for the advertised jackpot; the timing fields were left live.

Round now carries duration_seconds and cooldown_seconds, set from the config
when it opens. round_deadline() is the single place the deadline is computed —
the scheduler, place_bet's two checks and /rounds/current's closes_at all go
through it — and the cooldown is read off the round that just closed, so the gap
a round announced is the gap that's honoured. The config row becomes what the
*next* round opens with.

The migration backfills from the live config rather than leaving the column
defaults: an instance running 300s rounds would otherwise see the round
currently in progress jump to 600s the moment this lands, which is precisely the
retroactive change being fixed. Verified against a scratch DB with a non-default
config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 23:25:56 +02:00
co-authored by Claude Opus 5
parent 77e07e87dc
commit 37cc5eeeb5
11 changed files with 169 additions and 37 deletions
+4 -2
View File
@@ -15,7 +15,7 @@ from app.db.models import RoundParticipant, User
from app.db.session import get_session
from app.rounds.config import get_round_config
from app.rounds.events import EVICTED, RoundEventCapacityError, broadcaster
from app.rounds.service import get_active_round, winner_share
from app.rounds.service import get_active_round, round_deadline, winner_share
router = APIRouter(prefix="/rounds", tags=["rounds"])
@@ -142,7 +142,9 @@ async def current_round(
)
) or 0
opened_at = round_.opened_at.replace(tzinfo=timezone.utc)
closes_at = opened_at + timedelta(seconds=config.round_duration_seconds)
# B-61: from the round's own duration — the countdown clients are watching must
# not jump because an operator edited the config mid-round.
closes_at = round_deadline(round_)
# Lets the frontend show the personalized win/lose reveal only to players in
# this round — everyone else (not logged in, or logged in but didn't bet)