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-22 15:39:47 +02:00
< link rel = "icon" type = "image/svg+xml" href = "/logo.svg" >
2026-07-22 23:20:40 +02:00
< link rel = "stylesheet" href = "/admin.css" >
2026-07-21 10:36:55 +02:00
</ 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 >
2026-07-22 10:16:51 +02:00
< 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 >
2026-07-21 15:06:38 +02:00
</ 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-22 10:36:36 +02:00
< 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 >
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" >
</ 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 >
2026-07-22 12:00:09 +02:00
< 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 >
2026-07-21 15:06:38 +02:00
< div class = "warning-banner" >
2026-07-22 12:00:09 +02:00
⚠ 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.
2026-07-21 15:06:38 +02:00
</ div >
< div class = "card" >
< div class = "table-wrap" >
< table >
< thead >
2026-07-22 12:00:09 +02:00
< tr >< th > ID</ th >< th > Username</ th >< th > Indirizzo</ th >< th > Saldo (PLM)</ th >< th > Registrato</ th >< th > Chiave</ th >< th > Password</ th ></ tr >
2026-07-21 15:06:38 +02:00
</ 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' );
}
2026-07-22 10:16:51 +02:00
const DRAWING_STATUSES = [ 'closing' , 'drawing' , 'paying_out' ];
const CHAIN_STATUS_LABELS = {
waiting : 'In attesa del prossimo round' ,
open : 'Round aperto' ,
drawing : 'Estrazione in corso' ,
};
let chainStatusInterval = null ;
async function refreshChainStatus () {
try {
const res = await fetch ( '/rounds/current' );
const data = await res . json ();
let statusKey ;
if ( ! data . round_id ) statusKey = 'waiting' ;
else if ( DRAWING_STATUSES . includes ( data . status )) statusKey = 'drawing' ;
else statusKey = 'open' ;
document . getElementById ( 'chain-status-dot' ). className = 'status-dot status-' + statusKey ;
document . getElementById ( 'chain-status-label' ). textContent = CHAIN_STATUS_LABELS [ statusKey ];
document . getElementById ( 'chain-block' ). textContent =
'Blocco ' + ( data . chain_tip_height != null ? '#' + data . chain_tip_height : '—' );
} catch ( e ) {
// leave the last-known status on screen rather than blanking it out
}
}
function startChainStatusPolling () {
refreshChainStatus ();
clearInterval ( chainStatusInterval );
chainStatusInterval = setInterval ( refreshChainStatus , 15000 );
}
function stopChainStatusPolling () {
clearInterval ( chainStatusInterval );
chainStatusInterval = null ;
}
2026-07-21 15:06:38 +02:00
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' );
2026-07-22 10:16:51 +02:00
startChainStatusPolling ();
2026-07-21 14:28:29 +02:00
}
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 () {
2026-07-22 10:16:51 +02:00
stopChainStatusPolling ();
2026-07-21 14:28:29 +02:00
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-fee-rate' ). value = data . fee_rate_sat_vb ;
document . getElementById ( 'admin-rbf-timeout' ). value = data . rbf_timeout_seconds ;
2026-07-22 10:36:36 +02:00
renderMaintenanceState ( data . paused );
2026-07-21 14:28:29 +02:00
} catch ( e ) {
toast ( 'Errore nel caricamento configurazione: ' + e . message , 'error' );
}
}
2026-07-22 10:36:36 +02:00
function renderMaintenanceState ( paused ) {
const dot = document . getElementById ( 'maintenance-dot' );
const label = document . getElementById ( 'maintenance-status-label' );
const btn = document . getElementById ( 'maintenance-btn' );
btn . dataset . paused = paused ? '1' : '0' ;
if ( paused ) {
dot . className = 'status-dot status-paused' ;
label . textContent = 'In pausa: nessun nuovo round verrà aperto' ;
btn . textContent = 'Riprendi lotteria' ;
btn . classList . remove ( 'btn-stop' );
} else {
dot . className = 'status-dot status-open' ;
label . textContent = 'Attiva: i round si susseguono normalmente' ;
btn . textContent = 'Interrompi dopo questo round' ;
btn . classList . add ( 'btn-stop' );
}
}
async function toggleMaintenance () {
const btn = document . getElementById ( 'maintenance-btn' );
const isPaused = btn . dataset . paused === '1' ;
const path = isPaused ? '/admin/resume' : '/admin/pause' ;
if ( ! isPaused && ! window . confirm (
"Nessun nuovo round verrà aperto dopo quello in corso, fino a quando non riprendi la lotteria. " +
"Il round attuale (se presente) verrà comunque completato e il vincitore pagato. Continuare?"
)) {
return ;
}
btn . disabled = true ;
try {
const data = await callAdmin ( 'POST' , path , {});
renderMaintenanceState ( data . paused );
toast ( data . paused ? 'Lotteria in pausa.' : 'Lotteria ripresa.' , 'success' );
refreshChainStatus ();
} catch ( e ) {
toast ( 'Errore: ' + e . message , 'error' );
} finally {
btn . disabled = false ;
}
}
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 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
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>
2026-07-22 12:00:09 +02:00
<td>
<button class="secondary" style="width:auto;margin-top:0;min-height:30px;padding:4px 10px;font-size:0.78rem" onclick="resetUserPassword( ${ u . id } , this)">Reset</button>
<div class="privkey-box hidden" id="newpass- ${ u . id } "></div>
</td>
2026-07-21 14:28:29 +02:00
</tr>
2026-07-22 12:00:09 +02:00
` ). join ( '' ) || '<tr><td colspan="7" 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-22 12:00:09 +02:00
async function resetUserPassword ( userId , button ) {
if ( ! window . confirm (
"Verrà generata una nuova password casuale per questo utente, che non potrà più accedere con quella vecchia. " +
"L'azione viene registrata nell'audit log. Continuare?"
)) {
return ;
}
const box = document . getElementById ( 'newpass-' + userId );
await withLoading ( button , '…' , async () => {
try {
const data = await callAdmin ( 'POST' , '/admin/users/' + userId + '/reset-password' );
box . textContent = 'Nuova password per ' + data . username + ': ' + data . new_password ;
box . classList . remove ( 'hidden' );
toast ( 'Password reimpostata.' , 'success' );
} catch ( e ) {
toast ( 'Errore: ' + e . message , 'error' );
}
});
}
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 ();
});
2026-07-22 12:00:09 +02:00
function initAuthState () {
adminToken = sessionStorage . getItem ( 'plm_admin_token' );
if ( ! adminToken ) {
document . getElementById ( 'dashboard-section' ). classList . add ( 'hidden' );
document . getElementById ( 'login-section' ). classList . remove ( 'hidden' );
return ;
}
2026-07-21 14:28:29 +02:00
callAdmin ( 'GET' , '/admin/config' )
. then (() => { showDashboard (); return loadDashboard (); })
2026-07-22 12:00:09 +02:00
. catch (() => adminLogout ());
2026-07-21 14:28:29 +02:00
}
2026-07-22 12:00:09 +02:00
// Bfcache can restore a frozen snapshot of this page (DOM/JS state as it was
// before navigating away) without re-running any of this script — so a stale
// view could survive across back/forward navigation, e.g. showing a dashboard
// for a token that's since been rotated or explicitly logged out of. Cache-
// Control: no-store on this response should already prevent that, but
// re-validate here too as a safety net for browsers that ignore it.
window . addEventListener ( 'pageshow' , ( event ) => {
if ( event . persisted ) initAuthState ();
});
initAuthState ();
2026-07-21 10:36:55 +02:00
</ script >
</ body >
</ html >