+
Parametri
+
Configurazione operativa, salvata nel database — modificabile in qualsiasi momento senza riavviare il server.
-
-
Utenti
-
Elenco utenti registrati, con saldo interno e accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati).
-
-
- ⚠ 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.
+
-
-
-
- ID Username Indirizzo Saldo (PLM) Chiave
-
-
-
+
+
Utenti
+
Elenco utenti registrati, con saldo interno e accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati).
+
+
+ ⚠ 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.
+
+
+
+
+
+
+ ID Username Indirizzo Saldo (PLM) Registrato Chiave
+
+
+
+
+
-
-
+
+
+
Round
+
Ultimi round: stato, vincitore, importi e transazione di payout.
+
+
+
+
+
+ ID Stato Apertura Vincitore Pool (PLM) Vincita (PLM) Fee (PLM) Payout txid
+
+
+
+
+
+
+
+
+
Transazioni pendenti
+
Bet, payout e prelievi non ancora confermati — candidati al fee-bump RBF se scade il timeout.
+
+
+
+
+
+ ID Tipo Stato Txid Fee rate Tentativi Trasmessa
+
+
+
+
+
+
+
+
+
Audit log
+
Ultimi eventi registrati dal sistema (config, bet, payout, accessi a chiavi private, ecc.).
+
+
+
+
+
+ ID Evento Dettagli Utente Round Quando
+
+
+
+
+
+
+
+
+
@@ -213,13 +327,37 @@ async function callAdmin(method, path, body) {
return data;
}
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+}
+
+function badge(status) {
+ return `
${escapeHtml(status)} `;
+}
+
+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]();
+}
+
function showDashboard() {
document.getElementById('login-section').classList.add('hidden');
document.getElementById('dashboard-section').classList.remove('hidden');
}
async function loadDashboard() {
- await Promise.all([adminLoadConfig(), loadUsers()]);
+ await Promise.all([adminLoadConfig(), loadUsers(), loadRounds(), loadPending(), loadAuditLog()]);
}
async function adminLogin() {
@@ -251,6 +389,11 @@ async function adminLoadConfig() {
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;
+ document.getElementById('admin-round-duration').value = data.round_duration_seconds;
+ document.getElementById('admin-round-cooldown').value = data.round_cooldown_seconds;
+ 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;
} catch (e) {
toast('Errore nel caricamento configurazione: ' + e.message, 'error');
}
@@ -260,10 +403,19 @@ async function adminSave() {
const btn = document.getElementById('save-btn');
const feeAddress = document.getElementById('admin-fee-address').value;
const betAmountPlm = parseFloat(document.getElementById('admin-bet-amount').value);
- const betAmountSats = Math.round(betAmountPlm * SATS_PER_PLM);
+ 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),
+ 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),
+ };
await withLoading(btn, 'Salvataggio…', async () => {
try {
- await callAdmin('PUT', '/admin/config', { fee_address: feeAddress, bet_amount_sats: betAmountSats });
+ await callAdmin('PUT', '/admin/config', body);
toast('Configurazione salvata.', 'success');
} catch (e) {
toast('Errore nel salvataggio: ' + e.message, 'error');
@@ -271,10 +423,6 @@ async function adminSave() {
});
}
-function escapeHtml(s) {
- return s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
-}
-
async function loadUsers() {
try {
const users = await callAdmin('GET', '/admin/users');
@@ -285,12 +433,13 @@ async function loadUsers() {
${escapeHtml(u.username)}
${escapeHtml(u.address)}
${u.balance_sats / SATS_PER_PLM}
+
${fmtDate(u.created_at)}
Mostra
- `).join('');
+ `).join('') || '
Nessun utente registrato. ';
} catch (e) {
toast('Errore nel caricamento utenti: ' + e.message, 'error');
}
@@ -319,6 +468,66 @@ async function revealPrivkey(userId, button) {
});
}
+async function loadRounds() {
+ try {
+ const rounds = await callAdmin('GET', '/admin/rounds');
+ const tbody = document.getElementById('rounds-tbody');
+ tbody.innerHTML = rounds.map((r) => `
+
+ ${r.id}
+ ${badge(r.status)}
+ ${fmtDate(r.opened_at)}
+ ${r.winner_username ? escapeHtml(r.winner_username) : '—'}
+ ${r.pool_amount_sats != null ? r.pool_amount_sats / SATS_PER_PLM : '—'}
+ ${r.winner_amount_sats != null ? r.winner_amount_sats / SATS_PER_PLM : '—'}
+ ${r.fee_amount_sats != null ? r.fee_amount_sats / SATS_PER_PLM : '—'}
+ ${r.payout_txid ? escapeHtml(r.payout_txid) : '—'}
+
+ `).join('') || '
Nessun round ancora. ';
+ } 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) => `
+
+ ${p.id}
+ ${escapeHtml(p.kind)}
+ ${badge(p.status)}
+ ${escapeHtml(p.current_txid)}
+ ${p.fee_rate_sat_vb} sat/vB
+ ${p.attempt_count}
+ ${fmtDate(p.broadcast_at)}
+
+ `).join('') || '
Nessuna transazione pendente. ';
+ } 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) => `
+
+ ${e.id}
+ ${escapeHtml(e.event_type)}
+ ${escapeHtml(JSON.stringify(e.payload))}
+ ${e.user_id ?? '—'}
+ ${e.round_id ?? '—'}
+ ${fmtDate(e.created_at)}
+
+ `).join('') || '
Nessun evento registrato. ';
+ } catch (e) {
+ toast('Errore nel caricamento audit log: ' + e.message, 'error');
+ }
+}
+
document.getElementById('admin-token').addEventListener('keydown', (e) => {
if (e.key === 'Enter') adminLogin();
});