Refuse to open a round that could not pay its winner (B-66)
fee_address has no column default, because an operator has to supply their own — and the payout pays the 30% commission to it, so build_payout_transaction cannot even be built without one. A fresh instance nonetheless opened rounds happily: each took bets, confirmed them, and only then discovered it was unpayable, wedging in "paying_out" and retrying every 60s with money already in the pool. One manual recovery per round, until somebody noticed. open_new_round_if_needed now checks rounds_can_open(config) alongside `paused`: no payout address, no round. Nothing has moved yet at that point, which is the whole difference. Same scope as pausing — a round already in progress still closes, draws and pays out, since clearing the address mid-round is exactly the operator slip that must not strand a live round. Surfaced rather than silent, in the two places that matter: lottery_configured on GET /rounds/current, which makes / show a *different* banner from the maintenance one (telling a player "come back later" would be false — nothing is coming until setup finishes), and a warning at the top of /admin's Parametri card, the one screen that can fix it. rounds_can_open is where any future would-make-a-round-unpayable prerequisite belongs, instead of being discovered at payout time. The test churn is the finding restated: 26 tests expected a round to open on an instance with no payout address. Their fixtures now seed one, so each goes back to testing what it says — several would otherwise have passed for the wrong reason, returning None because of the missing address rather than because of the cooldown or pause under test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-1
@@ -235,7 +235,18 @@ function renderChainStatusBar() {
|
||||
label.textContent = t(CHAIN_STATUS_KEYS[labelKey]);
|
||||
block.textContent = t('chain.block', { n: data.chain_tip_height != null ? '#' + data.chain_tip_height : '—' });
|
||||
|
||||
document.getElementById('maintenance-banner').classList.toggle('hidden', !data.lottery_paused);
|
||||
// Two separate reasons no round will open, and telling them apart matters to the
|
||||
// reader: a pause ends when the operator resumes, while an unconfigured instance
|
||||
// (B-66) won't produce a round at all until it's set up — "come back later" would
|
||||
// be a lie. `=== false` so an older server that doesn't send the field at all
|
||||
// can't flash the banner. A pause takes precedence: it's the deliberate action.
|
||||
const notConfigured = data.lottery_configured === false;
|
||||
const noRoundsComing = !!data.lottery_paused || notConfigured;
|
||||
document.getElementById('maintenance-banner').classList.toggle('hidden', !noRoundsComing);
|
||||
if (noRoundsComing) {
|
||||
document.getElementById('maintenance-banner-text').textContent =
|
||||
data.lottery_paused ? t('maintenance.banner') : t('maintenance.notConfigured');
|
||||
}
|
||||
}
|
||||
|
||||
// After a couple of consecutive failed polls (network blip, server restart,
|
||||
|
||||
Reference in New Issue
Block a user