Files
plm-lottery/app/static/admin.html
T
davideandClaude Sonnet 5.5 db24287ea5 Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
The seed was the hash of the first block seen after the close, so a miner holding tickets
could withhold blocks until one made them win, the active Electrum server could pick which
real block was used by not announcing another (a failed corroboration even moved the draw to
the next block), and the participant order was the server-only `broadcast_at` with no
tie-break, so nobody could recompute the result.

- Open: every round gets a 32-byte `server_seed`, stored only Fernet-encrypted with the
  master-xprv key (`server_seed_enc`), with its published `server_seed_hash`;
  `draw_confirmations` is copied from round_config. The plaintext column is written only by
  the reveal.
- Entering `drawing` (one transaction, `draw_committed` audit row): participants ordered by
  bet txid, each `draw_position`, `participants_hash` (sha256 of `txid:vout:address\n`
  lines; new `bet_vout`), `draw_height = tip_at_close + 1`. A tip of 0 keeps the round
  `closing` (`draw_baseline_tip_unknown`, once per round). All persisted, so a resumed draw
  reads rather than recomputes (the `_tick` branch itself is B-81).
- `_complete_draw` / `_wait_for_draw_block` replace `_wait_for_next_block`: wait for
  `draw_confirmations` blocks from `draw_height`, then `ElectrumListener.agreed_block_hash`
  asks every server, the active one included, for the header at that exact height (valid
  work, >= 2 other answers, strict majority of all); no agreement means the same height is
  asked again and `draw_header_corroboration_failed` is written once per distinct reported
  hash. `draw_stalled` now counts from the last tip advance, or from when the quorum started
  to fail. Seed = sha256(server_seed || block_hash || participants_hash), index = seed mod n.
- The winner, the seed and the reveal (`server_seed`, `winner_drawn` payload) are written in
  one transaction; a round closing with no participants reveals too. A round opened before
  this change (no commitment) is given a secret as it enters `drawing`.
- `GET /rounds/{id}/proof` (public, 404 `round_not_found`, translated in all 7 languages) and
  the new fields on `GET /rounds/current`; `draw_confirmations` in `/admin/config` and the
  /admin form; `server_seed_hash`/`draw_height` in `/admin/rounds`, never the secret.
- Alembic d3e8b15a7c42 (plain add_column / create_index, expression indexes untouched).
- Docs: the B-73 `*Stato:*`/`*Oggi:*` lines are gone, guides and CLAUDE.md describe the new
  draw. Ambiguities resolved in api.md: the proof's participants also carry `amount_sats`
  (RND-37 lists it); `seed` is decimal in the proof and hex in the audit row, as specified.

Tests changed because the spec changed: ElectrumListener.corroborate_header (asked only the
other servers about the block the active one announced) is replaced by agreed_block_hash and
its quorum tests ported; draw_winner takes the secret and the list fingerprint; the four
_wait_for_next_block scheduler tests are replaced by _wait_for_draw_block ones (fixed height,
confirmations, same-height retry, stall clock). New: test_draw_proof.py recomputes the winner
independently from the public proof, checks the plaintext secret is nowhere in the database,
the API or the log before the reveal, and that a second pass over a `drawing` round yields
the same winner. Mutation-checked: ordering by broadcast_at, an early reveal and asking the
tip instead of the draw height each fail tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 16:29:09 +02:00

191 lines
8.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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-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>