Only the duration and the cooldown (and, since B-73, the draw confirmations) were copied onto the round when it opened. The cost of a bet and the fee address were read live, so an edit from /admin in the middle of a round gave the players of that round different stakes for the same chance and redirected the commission of the round in progress. - `rounds.bet_amount_sats`, `rounds.fee_address` and `rounds.max_round_pool_sats` are copied from round_config in the transaction that opens the round (Alembic a7d3e9f15b48: plain add_column, existing rounds backfilled with the current configuration, i.e. what they were using; the expression indexes untouched). `place_bet` charges the round's cost, `_trigger_payout` pays the round's fee address (an empty one on the row still stops the payout, as before), `GET /rounds/current` advertises the round's cost while one is active and the configured one otherwise. The minimum withdrawal and the fee rate stay live, by design (ADR-20, ADR-10). - `round_config.max_round_pool_sats` (0 = none, otherwise >= the bet cost and <= 4e15, in /admin and PUT/GET /admin/config): a bet is refused with `round_full` (params also carry `max_round_pool_sats`) once the sum of the round's participant rows, in-flight ones included, has reached the round's cap, before anything moves; the pool can exceed it by one bet at most. A cap under the bet cost is a 422 `max_round_pool_below_bet_amount`. - PUT /admin/config's payability check (RND-40) now also judges a fee-rate change against the bet cost the round in progress opened with, which can be lower than the configured one. - `error.round_full` no longer says "maximum of N players" (it also fires for the pool cap): the seven translations are generic. Ambiguity resolved: the spec said the cap is snapshotted (RND-05, BET-20, ARC) in some places and "applies immediately, also to the round in progress" in the round_config table of modello-dati; the snapshot wins, with a column on `rounds` (modello-dati/api/guide-admin brought in line). Tests changed because the spec changed: the payout fixtures seed the round's own fee address, and the old "fee address missing" payout test now empties the round's address (clearing it in the configuration mid-round must not stop the payout). New test_round_snapshot.py: the copy at open, edits reaching only the next round, every bet of a round paying the same stake whether the cost is raised or lowered meanwhile, the payout following the round's address (and surviving it being cleared in the configuration), the cap (exactly at the cap is full, in-flight bets count, no cap, a mid-round edit of the cap does not apply, the refused player's coins untouched), plus the route, the admin bounds and the two 422s, the fee-rate check against the round's own cost, and the migration backfill. Mutation-checked: charging the live cost, paying the live address, not copying the cap and `>` instead of `>=` each fail tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
195 lines
8.5 KiB
HTML
195 lines
8.5 KiB
HTML
<!doctype html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>PLM Lottery — Admin</title>
|
||
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
||
<link rel="stylesheet" href="/admin.css">
|
||
</head>
|
||
<body>
|
||
|
||
<section id="login-section">
|
||
<header>
|
||
<h1>PLM Lottery — Admin</h1>
|
||
<p>Accesso riservato</p>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<label for="admin-token">Admin token</label>
|
||
<input id="admin-token" type="password" placeholder="valore di ADMIN_TOKEN" autofocus>
|
||
<button onclick="adminLogin()" id="login-btn">Accedi</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="dashboard-section" class="hidden">
|
||
<nav class="navbar">
|
||
<span class="brand">PLM Lottery — Admin</span>
|
||
<span class="nav-tab active" id="nav-parametri" onclick="switchView('parametri')">Parametri</span>
|
||
<span class="nav-tab" id="nav-utenti" onclick="switchView('utenti')">Utenti</span>
|
||
<span class="nav-tab" id="nav-round" onclick="switchView('round')">Round</span>
|
||
<span class="nav-tab" id="nav-pending" onclick="switchView('pending')">Transazioni pendenti</span>
|
||
<span class="nav-tab" id="nav-audit" onclick="switchView('audit')">Audit log</span>
|
||
<span class="nav-tab" id="nav-bugreports" onclick="switchView('bugreports')">Segnalazioni bug</span>
|
||
<span class="spacer"></span>
|
||
<span class="chain-status-pill">
|
||
<span class="status-dot" id="chain-status-dot"></span>
|
||
<span id="chain-status-label">Connessione…</span>
|
||
</span>
|
||
<span class="chain-block mono" id="chain-block">Blocco —</span>
|
||
<button class="secondary" style="margin:8px 0 8px 14px" onclick="adminLogout()">Esci</button>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<div class="view active" id="view-parametri">
|
||
<h2 class="section-title">Parametri</h2>
|
||
<p class="hint">Configurazione operativa, salvata nel database — modificabile in qualsiasi momento senza riavviare il server.</p>
|
||
|
||
<div class="card" id="maintenance-card">
|
||
<h2>Manutenzione</h2>
|
||
<p class="hint" id="maintenance-hint">
|
||
Interrompe l'apertura di nuovi round dopo quello in corso, senza troncare il round attuale — chiusura,
|
||
estrazione e pagamento del vincitore avvengono normalmente. Gli utenti vedono un avviso di manutenzione.
|
||
</p>
|
||
<div class="row-between">
|
||
<span class="chain-status-pill">
|
||
<span class="status-dot" id="maintenance-dot"></span>
|
||
<span id="maintenance-status-label">—</span>
|
||
</span>
|
||
<button id="maintenance-btn" class="secondary" style="width:auto;margin-top:0" onclick="toggleMaintenance()">…</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<!-- B-66: no fee_address means no round can open at all (the payout pays the
|
||
30% commission to it, so it cannot even be built). Shown here because
|
||
this is the one screen that can fix it. -->
|
||
<div class="warning-banner hidden" id="admin-fee-address-warning">
|
||
⚠️ Nessun fee address configurato: finché resta vuoto <strong>non si aprirà nessun round</strong>
|
||
(il payout non sarebbe costruibile). Impostalo qui sotto e salva.
|
||
</div>
|
||
<div class="grid-2">
|
||
<div>
|
||
<label for="admin-fee-address">Fee address (dove finisce il 30% di ogni round)</label>
|
||
<input id="admin-fee-address" class="mono" placeholder="plm1q...">
|
||
<label for="admin-bet-amount">Bet amount (PLM)</label>
|
||
<input id="admin-bet-amount" inputmode="decimal" placeholder="es. 10">
|
||
</div>
|
||
<div>
|
||
<label for="admin-round-duration">Durata round (secondi)</label>
|
||
<input id="admin-round-duration" inputmode="numeric" placeholder="es. 600">
|
||
<label for="admin-round-cooldown">Pausa tra un round e il successivo (secondi)</label>
|
||
<input id="admin-round-cooldown" inputmode="numeric" placeholder="es. 30">
|
||
<label for="admin-draw-animation">Durata animazione estrazione (secondi)</label>
|
||
<input id="admin-draw-animation" inputmode="numeric" placeholder="es. 20">
|
||
<label for="admin-draw-confirmations">Conferme sul blocco di estrazione (1–20, dal round successivo)</label>
|
||
<input id="admin-draw-confirmations" inputmode="numeric" placeholder="es. 3">
|
||
<label for="admin-deposit-confirmations">Conferme per accreditare un deposito (1–100; le uscite coinbase ne richiedono comunque 120)</label>
|
||
<input id="admin-deposit-confirmations" inputmode="numeric" placeholder="es. 6">
|
||
<label for="admin-max-pool">Tetto al montepremi di un round (PLM, 0 = nessun tetto, dal round successivo)</label>
|
||
<input id="admin-max-pool" inputmode="decimal" placeholder="es. 0">
|
||
<label for="admin-fee-rate">Fee rate di rete (sat/vB)</label>
|
||
<input id="admin-fee-rate" inputmode="numeric" placeholder="es. 1">
|
||
<label for="admin-rbf-timeout">Timeout prima del fee-bump RBF (secondi)</label>
|
||
<input id="admin-rbf-timeout" inputmode="numeric" placeholder="es. 900">
|
||
</div>
|
||
</div>
|
||
<button onclick="adminSave()" id="save-btn">Salva</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-utenti">
|
||
<h2 class="section-title">Utenti</h2>
|
||
<p class="hint">Elenco utenti registrati, con saldo interno, accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati) e reset password per chi resta bloccato fuori dall'account.</p>
|
||
|
||
<div class="warning-banner">
|
||
⚠ La chiave privata dà accesso completo ai fondi dell'utente: ogni visualizzazione viene registrata nell'audit log, non condividerla né salvarla altrove. La password esistente di un utente non è mai recuperabile (è salvata solo come hash Argon2) — "Reset" ne genera una nuova al posto della vecchia, anche questo audit-loggato.
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Username</th><th>Indirizzo</th><th>Saldo (PLM)</th><th>Registrato</th><th>Chiave</th><th>Password</th></tr>
|
||
</thead>
|
||
<tbody id="users-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-round">
|
||
<h2 class="section-title">Round</h2>
|
||
<p class="hint">Ultimi round: stato, vincitore, importi e transazione di payout.</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Stato</th><th>Apertura</th><th>Vincitore</th><th>Pool (PLM)</th><th>Vincita (PLM)</th><th>Fee (PLM)</th><th>Payout txid</th></tr>
|
||
</thead>
|
||
<tbody id="rounds-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-pending">
|
||
<h2 class="section-title">Transazioni pendenti</h2>
|
||
<p class="hint">Bet, payout e prelievi non ancora confermati — candidati al fee-bump RBF se scade il timeout.</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Tipo</th><th>Stato</th><th>Txid</th><th>Fee rate</th><th>Tentativi</th><th>Trasmessa</th></tr>
|
||
</thead>
|
||
<tbody id="pending-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-audit">
|
||
<h2 class="section-title">Audit log</h2>
|
||
<p class="hint">Ultimi eventi registrati dal sistema (config, bet, payout, accessi a chiavi private, ecc.).</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Evento</th><th>Dettagli</th><th>Utente</th><th>Round</th><th>Quando</th></tr>
|
||
</thead>
|
||
<tbody id="audit-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-bugreports">
|
||
<h2 class="section-title">Segnalazioni bug</h2>
|
||
<p class="hint">Segnalazioni inviate dagli utenti tramite la pagina "Segnala un bug".</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Descrizione</th><th>Contatto</th><th>Utente</th><th>Quando</th><th>Stato</th></tr>
|
||
</thead>
|
||
<tbody id="bugreports-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<div id="toast-container" aria-live="polite"></div>
|
||
|
||
<script src="/admin.js"></script>
|
||
|
||
</body>
|
||
</html>
|