Files
plm-lottery/app/static/index.html
T
davideandClaude Opus 5 23d58796b6 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>
2026-08-04 14:13:00 +02:00

269 lines
16 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PLM Lottery</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<nav class="hidden" id="app-navbar" data-i18n-aria-label="nav.ariaSections" aria-label="Sezioni">
<div class="app-navbar-top">
<div class="app-navbar-top-inner">
<span class="brand">
<img class="brand-mark" src="/logo.svg" alt="">
PLM Lottery
</span>
<div class="app-navbar-account">
<span class="navbar-username" id="dash-username"></span>
<span class="navbar-balance mono">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg>
<span id="navbar-balance">— PLM</span>
</span>
<a class="link icon-link" href="/guida" target="_blank" rel="noopener" data-i18n-title="nav.guideTitle" title="Guida" data-i18n-aria-label="nav.guideAria" aria-label="Apri la guida utente">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 2-3 4"/><path d="M12 17h.01"/></svg>
</a>
<a class="link icon-link" href="/report-bug" target="_blank" rel="noopener" data-i18n-title="nav.bugReport" title="Segnala un bug" data-i18n-aria-label="nav.bugReport" aria-label="Segnala un bug">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v5"/><path d="M12 16h.01"/></svg>
</a>
<button class="link icon-link" onclick="logout()" data-i18n-title="nav.logoutTitle" title="Esci" data-i18n-aria-label="nav.logoutAria" aria-label="Esci dall'account">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg>
</button>
</div>
</div>
</div>
<div class="app-navbar-tabs">
<button class="navbar-tab active" id="nav-deposit" onclick="switchPanel('deposit')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg>
<span data-i18n="nav.deposit">Deposito</span>
</button>
<button class="navbar-tab" id="nav-bet" onclick="switchPanel('bet')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h.01M12 12h.01M18 12h.01"/></svg>
<span data-i18n="nav.bet">Bet</span>
</button>
<button class="navbar-tab" id="nav-withdraw" onclick="switchPanel('withdraw')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
<span data-i18n="nav.withdraw">Prelievo</span>
</button>
<button class="navbar-tab" id="nav-profile" onclick="switchPanel('profile')">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span data-i18n="nav.profile">Profilo</span>
</button>
</div>
</nav>
<div class="app-shell">
<div class="chain-bar" id="chain-bar">
<span class="chain-status-pill">
<span class="status-dot" id="chain-status-dot"></span>
<!-- No data-i18n on this one or on #draw-label below: both are written by
app.js from live state, and letting applyStaticTranslations() also own
them made a language switch flash (or, here, assert) a stale value. -->
<span id="chain-status-label">Connecting…</span>
</span>
<span class="chain-bar-right">
<span class="chain-block mono" id="chain-block"></span>
<!-- Deliberately here and not in the navbar: the navbar is hidden until login,
which would leave the landing page and the login form untranslatable for
anyone who can't read the browser-detected default. -->
<select id="lang-switcher" class="lang-switcher" onchange="setLanguage(this.value)" aria-label="Language">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ru">Русский</option>
<option value="zh">中文</option>
</select>
</span>
</div>
<div class="maintenance-banner hidden" id="maintenance-banner">
<span>⚠️</span>
<!-- Filled by renderChainStatusBar(): the text depends on *why* no round will
open — a deliberate pause, or an instance the operator hasn't finished
configuring (B-66) — so it renders through t() and carries no data-i18n. -->
<span id="maintenance-banner-text"></span>
</div>
<section id="landing-hero" class="hero">
<h1>PLM Lottery</h1>
<p class="lead" data-i18n="hero.lead">Deposita PLM, entra nel round con una quota fissa, e se viene estratto il tuo numero vinci il montepremi.</p>
<div class="hero-steps">
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16"/></svg></div>
<div class="step-title" data-i18n="hero.step1.title">1. Deposita</div>
<div class="step-hint" data-i18n="hero.step1.hint">Ricevi un indirizzo PLM personale, tuo per sempre</div>
</div>
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 12h.01M12 12h.01M18 12h.01"/></svg></div>
<div class="step-title" data-i18n="hero.step2.title">2. Gioca</div>
<div class="step-hint" data-i18n="hero.step2.hint">Una bet a quota fissa per entrare nel round corrente</div>
</div>
<div class="hero-step">
<div class="step-icon"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21h8M12 17v4M7 4h10v4a5 5 0 0 1-10 0V4Z"/><path d="M7 5H4a1 1 0 0 0-1 1v1a4 4 0 0 0 4 4M17 5h3a1 1 0 0 1 1 1v1a4 4 0 0 1-4 4"/></svg></div>
<div class="step-title" data-i18n="hero.step3.title">3. Vinci</div>
<div class="step-hint" data-i18n="hero.step3.hint">Estrazione dal blocco, montepremi accreditato subito</div>
</div>
</div>
<div class="trust-row">
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg><span data-i18n="trust.fixedRate">Quota fissa dichiarata</span></span>
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg><span data-i18n="trust.blockHash">Estrazione da hash di blocco</span></span>
<span class="trust-pill"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg><span data-i18n="trust.freeWithdraw">Prelievo libero in ogni momento</span></span>
</div>
</section>
<section id="auth-section" class="card">
<div class="tabs">
<div class="tab active" id="tab-login" onclick="switchTab('login')" data-i18n="auth.tabLogin">Login</div>
<div class="tab" id="tab-register" onclick="switchTab('register')" data-i18n="auth.tabRegister">Registrati</div>
</div>
<div class="tab-panel active" id="panel-login">
<label for="login-username" data-i18n="auth.username">Username</label>
<input id="login-username" autocomplete="username">
<label for="login-password" data-i18n="auth.password">Password</label>
<input id="login-password" type="password" autocomplete="current-password">
<button onclick="login()" id="login-btn" data-i18n="auth.loginBtn">Accedi</button>
</div>
<div class="tab-panel" id="panel-register">
<label for="reg-username" data-i18n="auth.username">Username</label>
<input id="reg-username" autocomplete="username" minlength="3" maxlength="32" pattern="[A-Za-z0-9_.\-]+" required>
<label for="reg-password" data-i18n="auth.password">Password</label>
<input id="reg-password" type="password" autocomplete="new-password" minlength="8" required>
<label for="reg-password-confirm" data-i18n="auth.passwordConfirm">Conferma password</label>
<input id="reg-password-confirm" type="password" autocomplete="new-password" minlength="8" required>
<button onclick="register()" id="register-btn" data-i18n="auth.registerBtn">Crea account</button>
</div>
</section>
<section id="dashboard-section" class="hidden">
<div class="card round-card" id="round-card">
<div class="row-between" id="round-normal-row">
<h2 id="round-title">Round —</h2>
<span class="mono" id="round-timer" style="font-size:1.1rem;font-weight:700">--:--</span>
</div>
<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>
<!-- 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>
<div class="draw-state" id="draw-state">
<div class="draw-spinner"></div>
<div class="draw-label" id="draw-label">Drawing the winner…</div>
</div>
<div class="hidden" id="draw-result"></div>
</div>
<div class="dash-panel active" id="panel-deposit">
<div class="card">
<h2 data-i18n="deposit.balanceTitle">Saldo interno</h2>
<p class="hint" data-i18n="deposit.balanceHint">Aggiornato dopo 1 conferma sulla rete</p>
<div class="row-between">
<div><span class="balance-value mono" id="dash-balance"></span> <span class="balance-unit">PLM</span></div>
<button class="secondary" onclick="refreshMe()" id="refresh-btn" data-i18n-aria-label="deposit.refreshAria" aria-label="Aggiorna saldo">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36M21 3v6h-6"/></svg>
<span data-i18n="deposit.refreshBtn">Aggiorna</span>
</button>
</div>
</div>
<div class="card">
<h2 data-i18n="deposit.addressTitle">Indirizzo di deposito</h2>
<p class="hint" data-i18n="deposit.addressHint">È anche l'indirizzo su cui ricevi eventuali vincite</p>
<div class="address-box">
<span class="mono" id="dash-address"></span>
<button class="secondary" onclick="copyAddress()" data-i18n-aria-label="deposit.copyAria" aria-label="Copia indirizzo">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
<div class="qr-box">
<img id="dash-qr" data-i18n-alt="deposit.qrAlt" alt="QR code dell'indirizzo di deposito">
</div>
</div>
</div>
<div class="dash-panel" id="panel-bet">
<div class="card">
<h2 data-i18n="bet.title">Bet</h2>
<p class="hint" data-i18n="bet.hint">Ingresso fisso al round corrente</p>
<!-- No data-i18n here: the label carries the live bet amount, which is
admin-configurable, so it's rendered by renderBetButton() in app.js. -->
<button onclick="placeBet()" id="bet-btn">Place bet</button>
</div>
</div>
<div class="dash-panel" id="panel-withdraw">
<div class="card">
<h2 data-i18n="withdraw.title">Withdrawal</h2>
<p class="hint" data-i18n="withdraw.hint">Invia fondi a un indirizzo PLM esterno</p>
<label for="wd-address" data-i18n="withdraw.addressLabel">Indirizzo esterno</label>
<input id="wd-address" class="mono" placeholder="plm1q...">
<p class="hint" data-i18n-html="withdraw.addressHint">Solo indirizzi P2WPKH bech32 (quelli che iniziano con <code>plm1q...</code>). Indirizzi legacy (<code>P...</code>) o P2SH non sono supportati.</p>
<label for="wd-amount" data-i18n="withdraw.amountLabel">Importo (PLM)</label>
<input id="wd-amount" inputmode="decimal" data-i18n-placeholder="withdraw.amountPlaceholder" placeholder="es. 2">
<label class="checkbox-row">
<input type="checkbox" id="wd-full-amount" onchange="toggleWithdrawFullAmount()">
<span data-i18n="withdraw.fullAmountPrefix">Preleva l'intero importo (</span><span class="mono" id="wd-full-amount-value"></span><span data-i18n="withdraw.fullAmountSuffix"> PLM)</span>
</label>
<button onclick="withdraw()" id="withdraw-btn" data-i18n="withdraw.button">Preleva</button>
</div>
</div>
<div class="dash-panel" id="panel-profile">
<div class="card">
<h2 data-i18n="profile.title">Profilo</h2>
<p class="hint" data-i18n="profile.hint">Le tue informazioni account</p>
<label data-i18n="profile.usernameLabel">Username</label>
<div class="address-box"><span id="profile-username"></span></div>
<label data-i18n="profile.addressLabel">Indirizzo di deposito</label>
<div class="address-box"><span class="mono" id="profile-address"></span></div>
<label data-i18n="profile.balanceLabel">Saldo interno</label>
<div class="address-box"><span class="mono" id="profile-balance"></span> <span class="balance-unit">PLM</span></div>
<label data-i18n="profile.createdLabel">Utente dal</label>
<div class="address-box"><span id="profile-created-at"></span></div>
</div>
<div class="card">
<h2 data-i18n="settings.title">Impostazioni</h2>
<p class="hint" data-i18n="settings.hint">Cambia la password del tuo account</p>
<label for="settings-current-password" data-i18n="settings.currentPasswordLabel">Password attuale</label>
<input id="settings-current-password" type="password" autocomplete="current-password">
<label for="settings-new-password" data-i18n="settings.newPasswordLabel">Nuova password</label>
<input id="settings-new-password" type="password" autocomplete="new-password">
<label for="settings-new-password-confirm" data-i18n="settings.newPasswordConfirmLabel">Conferma nuova password</label>
<input id="settings-new-password-confirm" type="password" autocomplete="new-password">
<button onclick="changePassword()" id="change-password-btn" data-i18n="settings.updateBtn">Aggiorna password</button>
</div>
</div>
</section>
</div>
<div id="toast-container" aria-live="polite"></div>
<script src="/i18n.js"></script>
<script src="/app.js"></script>
</body>
</html>