2026-07-21 10:36:55 +02:00
<!doctype html>
< html lang = "it" >
< head >
< meta charset = "utf-8" >
2026-07-21 10:46:55 +02:00
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > PLM Lottery — Admin</ title >
2026-07-21 10:36:55 +02:00
< style >
2026-07-21 10:46:55 +02:00
@ import url ( 'https://fonts.googleapis.com/css2?family=Fira+Code:wght@500;600&family=Fira+Sans:wght@400;500;600;700&display=swap' ) ;
: root {
--color-background : #F8FAFC ;
--color-surface : #FFFFFF ;
--color-foreground : #0F172A ;
--color-muted-foreground : #64748B ;
--color-border : #E2E8F0 ;
--color-primary : #F59E0B ;
--color-on-primary : #0F172A ;
--color-destructive : #DC2626 ;
--color-destructive-bg : #FEF2F2 ;
--color-success : #16A34A ;
--color-success-bg : #F0FDF4 ;
--color-ring : #F59E0B ;
--radius : 12 px ;
}
* { box-sizing : border-box ; }
body {
font-family : 'Fira Sans' , system-ui , sans-serif ;
background : var ( -- color - background );
color : var ( -- color - foreground );
2026-07-21 15:06:38 +02:00
margin : 0 ;
2026-07-21 10:46:55 +02:00
line-height : 1.5 ;
}
. mono { font-family : 'Fira Code' , monospace ; }
2026-07-21 15:06:38 +02:00
/* --- login screen: narrow, centered --- */
# login-section { max-width : 420 px ; margin : 15 vh auto 0 ; padding : 0 20 px ; }
# login-section header { margin-bottom : 24 px ; }
# login-section header h1 { font-size : 1.375 rem ; font-weight : 700 ; margin : 0 ; letter-spacing : -0.01 em ; }
# login-section header p { color : var ( -- color - muted - foreground ); font-size : 0.9 rem ; margin : 4 px 0 0 ; }
/* --- dashboard: navbar + content --- */
. navbar {
display : flex ; align-items : center ; gap : 4 px ; flex-wrap : wrap ;
background : var ( -- color - surface ); border-bottom : 1 px solid var ( -- color - border );
padding : 0 20 px ; position : sticky ; top : 0 ; z-index : 10 ;
}
. navbar . brand { font-weight : 700 ; font-size : 1.05 rem ; padding : 14 px 16 px 14 px 0 ; white-space : nowrap ; }
. navbar . nav-tab {
padding : 16 px 14 px ; font-size : 0.9 rem ; font-weight : 600 ; cursor : pointer ;
color : var ( -- color - muted - foreground ); border-bottom : 2 px solid transparent ;
margin-bottom : -1 px ; transition : color 150 ms , border-color 150 ms ; white-space : nowrap ;
}
. navbar . nav-tab . active { color : var ( -- color - foreground ); border-bottom-color : var ( -- color - primary ); }
. navbar . nav-tab : hover { color : var ( -- color - foreground ); }
. navbar . spacer { flex : 1 ; }
main { max-width : 960 px ; margin : 0 auto ; padding : 24 px 20 px 80 px ; }
. view { display : none ; }
. view . active { display : block ; }
h2 . section-title { font-size : 1.15 rem ; font-weight : 700 ; margin : 0 0 4 px ; }
2026-07-21 10:46:55 +02:00
. card {
background : var ( -- color - surface );
border : 1 px solid var ( -- color - border );
border-radius : var ( -- radius );
padding : 20 px ;
margin-bottom : 16 px ;
}
. card . hint { color : var ( -- color - muted - foreground ); font-size : 0.85 rem ; margin : 0 0 14 px ; }
2026-07-21 15:06:38 +02:00
. grid-2 { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 220 px , 1 fr )); gap : 0 20 px ; }
2026-07-21 10:46:55 +02:00
label { display : block ; font-size : 0.85 rem ; font-weight : 500 ; color : var ( -- color - muted - foreground ); margin-top : 12 px ; margin-bottom : 6 px ; }
label : first-child { margin-top : 0 ; }
input {
width : 100 % ; padding : 10 px 12 px ; font-size : 0.95 rem ; font-family : inherit ;
border : 1 px solid var ( -- color - border ); border-radius : 8 px ; background : var ( -- color - surface );
color : var ( -- color - foreground ); transition : border-color 150 ms , box-shadow 150 ms ;
}
input : focus {
outline : none ; border-color : var ( -- color - ring );
box-shadow : 0 0 0 3 px color-mix ( in srgb , var ( -- color - ring ) 25 % , transparent );
}
button {
display : inline-flex ; align-items : center ; justify-content : center ; gap : 6 px ;
min-height : 44 px ; padding : 0 18 px ; margin-top : 16 px ; width : 100 % ;
font-family : inherit ; font-size : 0.95 rem ; font-weight : 600 ;
background : var ( -- color - primary ); color : var ( -- color - on - primary );
border : none ; border-radius : 8 px ; cursor : pointer ;
transition : filter 150 ms , transform 150 ms ;
}
button : hover { filter : brightness ( 0.94 ); }
button : active { transform : scale ( 0.98 ); }
button : disabled { opacity : 0.6 ; cursor : default ; }
button : focus-visible { outline : 2 px solid var ( -- color - ring ); outline-offset : 2 px ; }
button . secondary {
2026-07-21 15:06:38 +02:00
width : auto ; margin-top : 0 ; min-height : 36 px ; padding : 0 14 px ;
2026-07-21 10:46:55 +02:00
background : var ( -- color - background ); color : var ( -- color - foreground );
border : 1 px solid var ( -- color - border );
}
. hidden { display : none !important ; }
2026-07-21 14:22:03 +02:00
table { width : 100 % ; border-collapse : collapse ; font-size : 0.85 rem ; }
th , td { text-align : left ; padding : 8 px 6 px ; border-bottom : 1 px solid var ( -- color - border ); vertical-align : top ; }
th { color : var ( -- color - muted - foreground ); font-weight : 500 ; }
2026-07-21 15:06:38 +02:00
td . addr , td . txid { font-family : 'Fira Code' , monospace ; word-break : break-all ; max-width : 200 px ; }
. table-wrap { overflow-x : auto ; }
. badge {
display : inline-block ; font-size : 0.72 rem ; font-weight : 600 ; padding : 2 px 8 px ;
border-radius : 999 px ; background : var ( -- color - background ); border : 1 px solid var ( -- color - border );
}
. badge . status-open , . badge . status-confirmed , . badge . status-closed { background : var ( -- color - success - bg ); color : var ( -- color - success ); border-color : var ( -- color - success ); }
. badge . status-pending , . badge . status-drawing , . badge . status-paying_out , . badge . status-closing , . badge . status-broadcast {
background : #FEF3C7 ; color : #92400E ; border-color : #F59E0B ;
}
2026-07-21 14:22:03 +02:00
button . reveal {
width : auto ; margin-top : 0 ; padding : 4 px 10 px ; min-height : 30 px ; font-size : 0.78 rem ;
background : var ( -- color - destructive - bg ); color : var ( -- color - destructive ); border : 1 px solid var ( -- color - destructive );
}
. privkey-box {
margin-top : 6 px ; padding : 8 px ; border-radius : 6 px ; font-size : 0.78 rem ;
background : var ( -- color - destructive - bg ); border : 1 px solid var ( -- color - destructive );
word-break : break-all ; font-family : 'Fira Code' , monospace ; color : var ( -- color - foreground );
}
. warning-banner {
background : var ( -- color - destructive - bg ); border : 1 px solid var ( -- color - destructive ); color : var ( -- color - destructive );
border-radius : 8 px ; padding : 10 px 12 px ; font-size : 0.8 rem ; margin-bottom : 14 px ; font-weight : 500 ;
}
2026-07-21 15:06:38 +02:00
pre . payload {
background : var ( -- color - background ); border : 1 px solid var ( -- color - border ); border-radius : 6 px ;
padding : 6 px 8 px ; font-size : 0.75 rem ; margin : 0 ; white-space : pre-wrap ; word-break : break-all ;
}
2026-07-21 10:46:55 +02:00
# toast-container {
position : fixed ; bottom : 20 px ; left : 50 % ; transform : translateX ( -50 % );
display : flex ; flex-direction : column ; gap : 8 px ; z-index : 100 ; width : calc ( 100 % - 40 px ); max-width : 440 px ;
}
. toast {
padding : 12 px 14 px ; border-radius : 8 px ; font-size : 0.85 rem ; font-weight : 500 ;
box-shadow : 0 4 px 12 px rgba ( 15 , 23 , 42 , 0.12 );
animation : toast-in 200 ms ease-out ;
}
. toast . success { background : var ( -- color - success - bg ); color : var ( -- color - success ); }
. toast . error { background : var ( -- color - destructive - bg ); color : var ( -- color - destructive ); }
@ keyframes toast-in { from { opacity : 0 ; transform : translateY ( 8 px ); } to { opacity : 1 ; transform : translateY ( 0 ); } }
@ media ( prefers-reduced-motion : reduce ) {
* { animation : none !important ; transition : none !important ; }
}
2026-07-21 10:36:55 +02:00
</ style >
</ head >
< body >
2026-07-21 14:28:29 +02:00
< section id = "login-section" >
< header >
< h1 > PLM Lottery — Admin</ h1 >
< p > Accesso riservato</ p >
</ header >
2026-07-21 10:36:55 +02:00
2026-07-21 14:28:29 +02:00
< 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 >
2026-07-21 10:36:55 +02:00
2026-07-21 15:06:38 +02:00
< 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 = "spacer" ></ span >
< button class = "secondary" style = "margin:8px 0" onclick = "adminLogout()" > Esci</ button >
</ nav >
2026-07-21 10:36:55 +02:00
2026-07-21 15:06:38 +02:00
< main >
2026-07-21 10:36:55 +02:00
2026-07-21 15:06:38 +02:00
< 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 >
2026-07-21 10:36:55 +02:00
2026-07-21 15:06:38 +02:00
< div class = "card" >
< 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" >
< label for = "admin-min-amount" > Importo minimo deposito/prelievo (PLM)</ label >
< input id = "admin-min-amount" inputmode = "decimal" placeholder = "es. 1" >
</ 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" >
2026-07-21 16:03:50 +02:00
< label for = "admin-draw-animation" > Durata animazione estrazione (secondi)</ label >
< input id = "admin-draw-animation" inputmode = "numeric" placeholder = "es. 20" >
2026-07-21 15:06:38 +02:00
< 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 >
2026-07-21 14:28:29 +02:00
</ div >
2026-07-21 15:06:38 +02:00
< div class = "view" id = "view-utenti" >
< h2 class = "section-title" > Utenti</ h2 >
< p class = "hint" > Elenco utenti registrati, con saldo interno e accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati).</ p >
< div class = "warning-banner" >
⚠ La chiave privata dà accesso completo ai fondi dell'utente. Ogni volta che la visualizzi viene registrata nell'audit log del server. Non condividerla, non salvarla altrove.
</ 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 ></ tr >
</ thead >
< tbody id = "users-tbody" ></ tbody >
</ table >
</ div >
</ div >
2026-07-21 14:28:29 +02:00
</ div >
2026-07-21 15:06:38 +02:00
< 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 >
</ main >
</ div >
2026-07-21 14:22:03 +02:00
2026-07-21 10:46:55 +02:00
< div id = "toast-container" aria-live = "polite" ></ div >
2026-07-21 10:36:55 +02:00
< script >
2026-07-21 10:46:55 +02:00
const SATS_PER_PLM = 100000000 ;
2026-07-21 14:28:29 +02:00
let adminToken = sessionStorage . getItem ( 'plm_admin_token' );
2026-07-21 10:46:55 +02:00
function toast ( message , type ) {
const container = document . getElementById ( 'toast-container' );
const el = document . createElement ( 'div' );
el . className = 'toast ' + type ;
el . textContent = message ;
container . appendChild ( el );
setTimeout (() => el . remove (), 4000 );
}
async function withLoading ( button , label , fn ) {
const original = button . textContent ;
button . disabled = true ;
button . textContent = label ;
try {
await fn ();
} finally {
button . disabled = false ;
button . textContent = original ;
}
2026-07-21 10:36:55 +02:00
}
async function callAdmin ( method , path , body ) {
const headers = { 'Content-Type' : 'application/json' , 'X-Admin-Token' : adminToken };
const res = await fetch ( path , { method , headers , body : body ? JSON . stringify ( body ) : undefined });
const data = await res . json (). catch (() => ({}));
if ( ! res . ok ) throw new Error ( data . detail || res . statusText );
return data ;
}
2026-07-21 15:06:38 +02:00
function escapeHtml ( s ) {
return String ( s ). replace ( /[&<>"']/g , ( c ) => ({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' }[ c ]));
}
function badge ( status ) {
return `<span class="badge status- ${ escapeHtml ( status ) } "> ${ escapeHtml ( status ) } </span>` ;
}
function fmtDate ( iso ) {
if ( ! iso ) return '—' ;
return new Date ( iso ). toLocaleString ( 'it-IT' );
}
const VIEWS = [ 'parametri' , 'utenti' , 'round' , 'pending' , 'audit' ];
function switchView ( name ) {
for ( const key of VIEWS ) {
document . getElementById ( 'nav-' + key ). classList . toggle ( 'active' , key === name );
document . getElementById ( 'view-' + key ). classList . toggle ( 'active' , key === name );
}
const loaders = { utenti : loadUsers , round : loadRounds , pending : loadPending , audit : loadAuditLog };
if ( loaders [ name ]) loaders [ name ]();
}
2026-07-21 14:28:29 +02:00
function showDashboard () {
document . getElementById ( 'login-section' ). classList . add ( 'hidden' );
document . getElementById ( 'dashboard-section' ). classList . remove ( 'hidden' );
}
async function loadDashboard () {
2026-07-21 15:06:38 +02:00
await Promise . all ([ adminLoadConfig (), loadUsers (), loadRounds (), loadPending (), loadAuditLog ()]);
2026-07-21 14:28:29 +02:00
}
async function adminLogin () {
const btn = document . getElementById ( 'login-btn' );
adminToken = document . getElementById ( 'admin-token' ). value ;
await withLoading ( btn , 'Verifica…' , async () => {
2026-07-21 10:46:55 +02:00
try {
2026-07-21 14:28:29 +02:00
await callAdmin ( 'GET' , '/admin/config' );
sessionStorage . setItem ( 'plm_admin_token' , adminToken );
showDashboard ();
await loadDashboard ();
2026-07-21 10:46:55 +02:00
} catch ( e ) {
2026-07-21 14:28:29 +02:00
adminToken = null ;
toast ( 'Token non valido.' , 'error' );
2026-07-21 10:46:55 +02:00
}
});
2026-07-21 10:36:55 +02:00
}
2026-07-21 14:28:29 +02:00
function adminLogout () {
sessionStorage . removeItem ( 'plm_admin_token' );
adminToken = null ;
document . getElementById ( 'admin-token' ). value = '' ;
document . getElementById ( 'dashboard-section' ). classList . add ( 'hidden' );
document . getElementById ( 'login-section' ). classList . remove ( 'hidden' );
}
async function adminLoadConfig () {
try {
const data = await callAdmin ( 'GET' , '/admin/config' );
document . getElementById ( 'admin-fee-address' ). value = data . fee_address ;
document . getElementById ( 'admin-bet-amount' ). value = data . bet_amount_sats / SATS_PER_PLM ;
2026-07-21 15:06:38 +02:00
document . getElementById ( 'admin-round-duration' ). value = data . round_duration_seconds ;
document . getElementById ( 'admin-round-cooldown' ). value = data . round_cooldown_seconds ;
2026-07-21 16:03:50 +02:00
document . getElementById ( 'admin-draw-animation' ). value = data . draw_animation_seconds ;
2026-07-21 15:06:38 +02:00
document . getElementById ( 'admin-min-amount' ). value = data . min_amount_sats / SATS_PER_PLM ;
document . getElementById ( 'admin-fee-rate' ). value = data . fee_rate_sat_vb ;
document . getElementById ( 'admin-rbf-timeout' ). value = data . rbf_timeout_seconds ;
2026-07-21 14:28:29 +02:00
} catch ( e ) {
toast ( 'Errore nel caricamento configurazione: ' + e . message , 'error' );
}
}
2026-07-21 10:36:55 +02:00
async function adminSave () {
2026-07-21 10:46:55 +02:00
const btn = document . getElementById ( 'save-btn' );
2026-07-21 10:36:55 +02:00
const feeAddress = document . getElementById ( 'admin-fee-address' ). value ;
2026-07-21 10:38:26 +02:00
const betAmountPlm = parseFloat ( document . getElementById ( 'admin-bet-amount' ). value );
2026-07-21 15:06:38 +02:00
const minAmountPlm = parseFloat ( document . getElementById ( 'admin-min-amount' ). value );
const body = {
fee_address : feeAddress ,
bet_amount_sats : Math . round ( betAmountPlm * SATS_PER_PLM ),
round_duration_seconds : parseInt ( document . getElementById ( 'admin-round-duration' ). value , 10 ),
round_cooldown_seconds : parseInt ( document . getElementById ( 'admin-round-cooldown' ). value , 10 ),
2026-07-21 16:03:50 +02:00
draw_animation_seconds : parseInt ( document . getElementById ( 'admin-draw-animation' ). value , 10 ),
2026-07-21 15:06:38 +02:00
min_amount_sats : Math . round ( minAmountPlm * SATS_PER_PLM ),
fee_rate_sat_vb : parseInt ( document . getElementById ( 'admin-fee-rate' ). value , 10 ),
rbf_timeout_seconds : parseInt ( document . getElementById ( 'admin-rbf-timeout' ). value , 10 ),
};
2026-07-21 10:46:55 +02:00
await withLoading ( btn , 'Salvataggio…' , async () => {
try {
2026-07-21 15:06:38 +02:00
await callAdmin ( 'PUT' , '/admin/config' , body );
2026-07-21 10:46:55 +02:00
toast ( 'Configurazione salvata.' , 'success' );
} catch ( e ) {
toast ( 'Errore nel salvataggio: ' + e . message , 'error' );
}
});
2026-07-21 10:36:55 +02:00
}
2026-07-21 14:22:03 +02:00
async function loadUsers () {
2026-07-21 14:28:29 +02:00
try {
const users = await callAdmin ( 'GET' , '/admin/users' );
const tbody = document . getElementById ( 'users-tbody' );
tbody . innerHTML = users . map (( u ) => `
<tr>
<td> ${ u . id } </td>
<td> ${ escapeHtml ( u . username ) } </td>
<td class="addr"> ${ escapeHtml ( u . address ) } </td>
<td> ${ u . balance_sats / SATS_PER_PLM } </td>
2026-07-21 15:06:38 +02:00
<td> ${ fmtDate ( u . created_at ) } </td>
2026-07-21 14:28:29 +02:00
<td>
<button class="reveal" onclick="revealPrivkey( ${ u . id } , this)">Mostra</button>
<div class="privkey-box hidden" id="privkey- ${ u . id } "></div>
</td>
</tr>
2026-07-21 15:06:38 +02:00
` ). join ( '' ) || '<tr><td colspan="6" class="hint">Nessun utente registrato.</td></tr>' ;
2026-07-21 14:28:29 +02:00
} catch ( e ) {
toast ( 'Errore nel caricamento utenti: ' + e . message , 'error' );
}
2026-07-21 14:22:03 +02:00
}
async function revealPrivkey ( userId , button ) {
const box = document . getElementById ( 'privkey-' + userId );
if ( ! box . classList . contains ( 'hidden' )) {
box . classList . add ( 'hidden' );
box . textContent = '' ;
button . textContent = 'Mostra' ;
return ;
}
if ( ! window . confirm ( 'Stai per visualizzare la chiave privata di questo utente. L\'accesso verrà registrato nell\'audit log. Continuare?' )) {
return ;
}
await withLoading ( button , '…' , async () => {
try {
const data = await callAdmin ( 'GET' , '/admin/users/' + userId + '/privkey' );
box . textContent = data . wif ;
box . classList . remove ( 'hidden' );
button . textContent = 'Nascondi' ;
} catch ( e ) {
toast ( 'Errore: ' + e . message , 'error' );
}
});
}
2026-07-21 14:28:29 +02:00
2026-07-21 15:06:38 +02:00
async function loadRounds () {
try {
const rounds = await callAdmin ( 'GET' , '/admin/rounds' );
const tbody = document . getElementById ( 'rounds-tbody' );
tbody . innerHTML = rounds . map (( r ) => `
<tr>
<td> ${ r . id } </td>
<td> ${ badge ( r . status ) } </td>
<td> ${ fmtDate ( r . opened_at ) } </td>
<td> ${ r . winner_username ? escapeHtml ( r . winner_username ) : '—' } </td>
<td> ${ r . pool_amount_sats != null ? r . pool_amount_sats / SATS_PER_PLM : '—' } </td>
<td> ${ r . winner_amount_sats != null ? r . winner_amount_sats / SATS_PER_PLM : '—' } </td>
<td> ${ r . fee_amount_sats != null ? r . fee_amount_sats / SATS_PER_PLM : '—' } </td>
<td class="txid"> ${ r . payout_txid ? escapeHtml ( r . payout_txid ) : '—' } </td>
</tr>
` ). join ( '' ) || '<tr><td colspan="8" class="hint">Nessun round ancora.</td></tr>' ;
} catch ( e ) {
toast ( 'Errore nel caricamento round: ' + e . message , 'error' );
}
}
async function loadPending () {
try {
const items = await callAdmin ( 'GET' , '/admin/pending-transactions' );
const tbody = document . getElementById ( 'pending-tbody' );
tbody . innerHTML = items . map (( p ) => `
<tr>
<td> ${ p . id } </td>
<td> ${ escapeHtml ( p . kind ) } </td>
<td> ${ badge ( p . status ) } </td>
<td class="txid"> ${ escapeHtml ( p . current_txid ) } </td>
<td> ${ p . fee_rate_sat_vb } sat/vB</td>
<td> ${ p . attempt_count } </td>
<td> ${ fmtDate ( p . broadcast_at ) } </td>
</tr>
` ). join ( '' ) || '<tr><td colspan="7" class="hint">Nessuna transazione pendente.</td></tr>' ;
} catch ( e ) {
toast ( 'Errore nel caricamento transazioni pendenti: ' + e . message , 'error' );
}
}
async function loadAuditLog () {
try {
const entries = await callAdmin ( 'GET' , '/admin/audit-log' );
const tbody = document . getElementById ( 'audit-tbody' );
tbody . innerHTML = entries . map (( e ) => `
<tr>
<td> ${ e . id } </td>
<td> ${ escapeHtml ( e . event_type ) } </td>
<td><pre class="payload"> ${ escapeHtml ( JSON . stringify ( e . payload )) } </pre></td>
<td> ${ e . user_id ?? '—' } </td>
<td> ${ e . round_id ?? '—' } </td>
<td> ${ fmtDate ( e . created_at ) } </td>
</tr>
` ). join ( '' ) || '<tr><td colspan="6" class="hint">Nessun evento registrato.</td></tr>' ;
} catch ( e ) {
toast ( 'Errore nel caricamento audit log: ' + e . message , 'error' );
}
}
2026-07-21 14:28:29 +02:00
document . getElementById ( 'admin-token' ). addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Enter' ) adminLogin ();
});
if ( adminToken ) {
callAdmin ( 'GET' , '/admin/config' )
. then (() => { showDashboard (); return loadDashboard (); })
. catch (() => { sessionStorage . removeItem ( 'plm_admin_token' ); adminToken = null ; });
}
2026-07-21 10:36:55 +02:00
</ script >
</ body >
</ html >