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>
11 KiB
Guida admin
Come gestire la configurazione operativa di PLM Lottery. Presuppone che il server sia già avviato — vedi running-the-server.md.
Accesso
Il pannello admin è su https://<host>/admin — non è collegato da
nessun link nell'interfaccia utente (né in entrata né in uscita): ci si
arriva solo conoscendo l'URL. La pagina mostra solo un campo token finché non
accedi: non è protetta da login personale, ma da un token condiviso
(ADMIN_TOKEN, definito in .env).
Incolla il valore di ADMIN_TOKEN e premi "Accedi" (o Invio): se il token è
valido, si apre la dashboard con una navbar in alto e carica automaticamente
tutte le sezioni — nessun bottone "Carica" separato. Il token resta in
sessionStorage (si perde chiudendo la tab/il browser); "Esci" torna alla
sola schermata di login.
Sezioni della dashboard
- Parametri — configurazione operativa (vedi tabella sotto)
- Utenti — elenco utenti, saldo, accesso alla chiave privata, reset password
- Round — storico round: stato, vincitore, importi, txid di payout
- Transazioni pendenti — bet/payout/prelievi non ancora confermati, candidati al fee-bump RBF
- Audit log — eventi registrati dal sistema (config cambiata, bet, payout, accessi a chiavi private, ecc.)
Parametri
Tutti i parametri operativi/di business sono nella sezione "Parametri",
salvati nel database — modificabili in qualsiasi momento, effetto immediato,
nessun riavvio del server necessario. Non esiste alcuna variabile
d'ambiente equivalente: .env contiene solo segreti e configurazione di
infrastruttura (chiave master, JWT, Electrum, token admin), non parametri di
business — quelli si toccano solo da qui.
| Campo | Significato |
|---|---|
| Fee address | L'indirizzo PLM su cui finisce il 30% di ogni round (fee). Obbligatorio: i payout non partono se questo campo è vuoto. |
| Bet amount (PLM) | Il costo fisso d'ingresso per round. È anche l'importo minimo prelevabile: un prelievo sotto questa soglia viene rifiutato (i depositi non hanno un controllo minimo lato server). |
| Durata round (secondi) | Quanto resta aperto un round prima di chiudersi ed estrarre il vincitore. Il taglio per le nuove giocate scatta esattamente allo scadere di questo tempo (verificato ad ogni bet, non dipende dal ciclo dello scheduler) — è un "semaforo giallo": nessuna nuova entrata, ma le bet già trasmesse prima dello scadere hanno comunque tempo di confermarsi prima che il round chiuda ed estragga. |
| Pausa tra un round e il successivo (secondi) | Cooldown dopo la chiusura di un round, prima che il successivo si apra — dà tempo ai giocatori di vedere l'esito. |
Durata e cooldown si applicano dal round successivo, non a quello già in corso: ogni round si porta dietro i valori con cui è stato aperto, così abbassare la durata mentre un round è a metà non lo chiude di colpo, e alzarla non sposta il countdown che i giocatori stanno già guardando. Gli altri parametri (bet amount, fee rate, RBF timeout) restano invece a effetto immediato.
| Durata animazione estrazione (secondi) | Tempo minimo per cui la dashboard di ogni utente mostra l'animazione "Estrazione in corso" dopo la chiusura del round, prima di rivelare il vincitore. È solo un minimo: il processo reale aspetta fino a 3 blocchi confermati in sequenza (ultima bet in sospeso, estrazione, payout — ~2 minuti l'uno), quindi l'animazione può durare più a lungo di questo valore, mai meno. | | Fee rate di rete (sat/vB) | Fee per byte usata per costruire bet, payout e prelievi. | | Timeout prima del fee-bump RBF (secondi) | Dopo quanto tempo senza conferma una transazione viene ritrasmessa con fee più alta. |
Tutti gli importi in PLM vengono convertiti in sats (1 PLM = 100.000.000 sats) solo nella chiamata API — il backend lavora sempre in sats.
Su un'istanza nuova (mai avviata), questi campi partono con dei default
hardcoded nel codice (RoundConfig in app/db/models.py: bet 10 PLM, round
10 minuti, cooldown 30s, animazione estrazione 20s, fee 1
sat/vB, RBF timeout 900s) — vanno
comunque rivisti e confermati dal pannello prima del primo utilizzo reale.
Manutenzione (pausa/ripresa lotteria)
In cima alla sezione "Parametri" c'è una card "Manutenzione" con un pulsante per fermare l'apertura di nuovi round — utile per intervenire sul server (aggiornamenti, riavvii) senza lasciare gli utenti a metà di un round o sorprenderli con un'interruzione improvvisa.
- "Interrompi dopo questo round": il round eventualmente in corso viene completato normalmente — chiude, estrae il vincitore da un blocco confermato, e paga il 70/30 come sempre. Solo l'apertura del round successivo viene sospesa. Gli utenti vedono un avviso di manutenzione sulla loro dashboard (e sulla home, anche da sloggati) finché la lotteria resta in pausa.
- "Riprendi lotteria": annulla la pausa — al prossimo giro dello scheduler (ogni 5 secondi) un nuovo round si apre normalmente (rispettando comunque il cooldown se il precedente si è appena chiuso).
Ogni pausa/ripresa viene registrata nell'audit log (lottery_paused /
lottery_resumed), ma — come per il resto del pannello — non registra
quale operatore l'ha premuta (token condiviso, vedi limiti noti in
CLAUDE.md).
Chi paga il fee-bump RBF? Quando una bet, un payout o un prelievo resta troppo a lungo senza conferma (oltre il "Timeout prima del fee-bump RBF"), il sistema lo ritrasmette con una fee più alta. Il costo aggiuntivo lo assorbe sempre chi ha originato la transazione, non il destinatario: per bet e prelievi è l'utente stesso (gli torna un resto più piccolo), per i payout è il pool (il resto che torna all'indirizzo pool si riduce) — la quota del vincitore e quella delle fee, già fissate, non vengono mai toccate. Se non c'è un resto abbastanza grande da assorbire l'aumento, il bump fallisce e resta un intervento manuale (vedi "Limiti noti").
Round
La sezione "Round" mostra lo storico (GET /admin/rounds, ultimi 50 per
default): id, stato, orario di apertura, vincitore (username), importo del
pool, importo vinto, importo di fee, txid del payout — tutti in PLM salvo il
txid.
Transazioni pendenti
GET /admin/pending-transactions elenca bet, payout e prelievi ancora senza
conferma: tipo, stato, txid corrente, fee rate usata, numero di tentativi
(si incrementa a ogni bump RBF) e orario dell'ultima trasmissione. Una riga
che resta qui a lungo, con attempt_count che sale, indica una transazione
in difficoltà — vedi "Limiti noti" sul fallback RBF.
Audit log
GET /admin/audit-log elenca gli ultimi 200 eventi registrati dal sistema
(tipo evento, payload JSON, utente/round coinvolti, timestamp): bet
piazzate, payout inviati, round chiusi, configurazione modificata, accessi
alle chiavi private, ecc. È il primo posto da controllare per ricostruire
cosa è successo dopo un problema.
Eventi a cui vale la pena prestare attenzione:
| Evento | Significato |
|---|---|
config_updated |
Un parametro è stato modificato; il payload contiene valore precedente e nuovo per ogni campo cambiato. |
bet_broadcast_failed / withdrawal_broadcast_failed |
La rete ha rifiutato la transazione. Non è stato speso nulla: gli UTXO sono stati liberati e il saldo dell'utente è tornato come prima. |
pending_tx_abandoned |
Una transazione trasmessa è scomparsa dalla catena e il sistema l'ha dichiarata persa: UTXO liberati, bet rimossa o prelievo segnato failed. Se capita spesso, la fee rate configurata è probabilmente troppo bassa. |
pending_tx_recovered |
Una transazione che si credeva incompleta è invece finita in catena (tipicamente dopo un riavvio a metà invio) e il sistema l'ha ripresa da sé. |
bug_report_status_changed |
Un admin ha cambiato lo stato di una segnalazione (payload: report_id, stato precedente e nuovo). Con un token admin unico e condiviso, questa riga è l'unica traccia di chi tocca le segnalazioni. |
payout_failed |
Il payout di un round non è partito. Il round resta in paying_out e richiede intervento manuale: non esiste un retry automatico. Controlla fee_address, il saldo dell'indirizzo pool e la connessione Electrum. |
Alternative all'interfaccia grafica
Le stesse operazioni si possono fare da terminale o da Swagger UI
(https://<host>/docs, sezione admin — disponibile solo se ENABLE_API_DOCS=true
è impostato in .env, disattivata di default perché espone l'intera API),
sempre passando ADMIN_TOKEN nell'header X-Admin-Token:
# leggere la configurazione
curl https://<host>/admin/config -H "X-Admin-Token: <ADMIN_TOKEN>"
# aggiornarla (importi in sats: 10 PLM = 1000000000; solo i campi passati vengono cambiati)
curl -X PUT https://<host>/admin/config \
-H "X-Admin-Token: <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"fee_address": "plm1q...", "bet_amount_sats": 1000000000, "round_duration_seconds": 600}'
I valori vengono validati: fee_address deve essere un indirizzo bech32 PLM
valido (plm1...) e i parametri numerici hanno limiti di buon senso
(fee_rate_sat_vb almeno 1, durata round almeno 30s, ecc.). Un valore fuori
range viene rifiutato con un errore 422 e la configurazione resta invariata.
Il controllo su fee_address è deliberatamente severo: un indirizzo di
un'altra catena (per esempio bc1...) sarebbe formalmente valido come witness
program, e il 30% di commissione di ogni round finirebbe su uno script di cui
nessuno ha la chiave.
Utenti, chiave privata e reset password
La card "Utenti" elenca id, username, indirizzo e saldo di ogni utente
registrato. Il bottone "Mostra" su ogni riga rivela la chiave privata (WIF)
di quell'utente, dietro conferma esplicita — serve per interventi manuali
(es. restituire fondi bloccati). Ogni visualizzazione viene registrata
nell'audit log (admin_privkey_accessed). Questo non introduce una nuova
falla: il server è già custodial, la chiave master da cui derivano tutte le
chiavi utente vive sul server — questo pannello espone solo qualcosa che
l'operatore può già fare via script.
Il bottone "Reset" nella colonna "Password" genera una nuova password
casuale per l'utente e sovrascrive quella esistente — mostrata una sola
volta nel pannello, così puoi comunicarla a chi ti ha chiesto aiuto perché
l'ha dimenticata. Non è un "recupero": le password sono salvate solo come
hash Argon2 (app/auth/security.py), quindi quella vecchia non è mai
recuperabile né per l'admin né per il codice stesso — l'unica opzione è
sempre sostituirla con una nuova. Anche questa azione è audit-loggata
(admin_password_reset) e non esiste alcun flusso self-service equivalente
per l'utente: solo un admin col token può farlo.
Limiti noti
- Il token è unico e condiviso: non c'è identità per singolo admin né audit
di chi ha cambiato cosa (oltre alla tabella
audit_loggenerica). - Nessun rate limiting sugli endpoint admin (né su registrazione/bet/ prelievo utente).
- Se un payout fallisce (es. Electrum disconnesso, UTXO insufficienti), il
round resta bloccato in
paying_outsenza retry automatico — richiede intervento manuale.
Per l'elenco completo dei gap noti vedi la sezione "Known gaps / TODO" in
CLAUDE.md.