diff --git a/app/static/admin.html b/app/static/admin.html index 5041033..fef3aa1 100644 --- a/app/static/admin.html +++ b/app/static/admin.html @@ -29,17 +29,40 @@ font-family: 'Fira Sans', system-ui, sans-serif; background: var(--color-background); color: var(--color-foreground); - max-width: 480px; - margin: 0 auto; - padding: 32px 20px 80px; + margin: 0; line-height: 1.5; } .mono { font-family: 'Fira Code', monospace; } - header { margin-bottom: 24px; } - header h1 { font-size: 1.375rem; font-weight: 700; margin: 0; letter-spacing: -0.01em; } - header p { color: var(--color-muted-foreground); font-size: 0.9rem; margin: 4px 0 0; } + /* --- login screen: narrow, centered --- */ + #login-section { max-width: 420px; margin: 15vh auto 0; padding: 0 20px; } + #login-section header { margin-bottom: 24px; } + #login-section header h1 { font-size: 1.375rem; font-weight: 700; margin: 0; letter-spacing: -0.01em; } + #login-section header p { color: var(--color-muted-foreground); font-size: 0.9rem; margin: 4px 0 0; } + + /* --- dashboard: navbar + content --- */ + .navbar { + display: flex; align-items: center; gap: 4px; flex-wrap: wrap; + background: var(--color-surface); border-bottom: 1px solid var(--color-border); + padding: 0 20px; position: sticky; top: 0; z-index: 10; + } + .navbar .brand { font-weight: 700; font-size: 1.05rem; padding: 14px 16px 14px 0; white-space: nowrap; } + .navbar .nav-tab { + padding: 16px 14px; font-size: 0.9rem; font-weight: 600; cursor: pointer; + color: var(--color-muted-foreground); border-bottom: 2px solid transparent; + margin-bottom: -1px; transition: color 150ms, border-color 150ms; 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: 960px; margin: 0 auto; padding: 24px 20px 80px; } + + .view { display: none; } + .view.active { display: block; } + + h2.section-title { font-size: 1.15rem; font-weight: 700; margin: 0 0 4px; } .card { background: var(--color-surface); @@ -51,6 +74,8 @@ .card .hint { color: var(--color-muted-foreground); font-size: 0.85rem; margin: 0 0 14px; } + .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0 20px; } + label { display: block; font-size: 0.85rem; font-weight: 500; color: var(--color-muted-foreground); margin-top: 12px; margin-bottom: 6px; } label:first-child { margin-top: 0; } @@ -78,6 +103,7 @@ button:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; } button.secondary { + width: auto; margin-top: 0; min-height: 36px; padding: 0 14px; background: var(--color-background); color: var(--color-foreground); border: 1px solid var(--color-border); } @@ -87,7 +113,17 @@ table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--color-border); vertical-align: top; } th { color: var(--color-muted-foreground); font-weight: 500; } - td.addr { font-family: 'Fira Code', monospace; word-break: break-all; max-width: 160px; } + td.addr, td.txid { font-family: 'Fira Code', monospace; word-break: break-all; max-width: 200px; } + .table-wrap { overflow-x: auto; } + + .badge { + display: inline-block; font-size: 0.72rem; font-weight: 600; padding: 2px 8px; + border-radius: 999px; background: var(--color-background); border: 1px 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; + } button.reveal { width: auto; margin-top: 0; padding: 4px 10px; min-height: 30px; font-size: 0.78rem; @@ -104,6 +140,11 @@ border-radius: 8px; padding: 10px 12px; font-size: 0.8rem; margin-bottom: 14px; font-weight: 500; } + pre.payload { + background: var(--color-background); border: 1px solid var(--color-border); border-radius: 6px; + padding: 6px 8px; font-size: 0.75rem; margin: 0; white-space: pre-wrap; word-break: break-all; + } + #toast-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 100; width: calc(100% - 40px); max-width: 440px; @@ -137,46 +178,119 @@ - + +
+

Round

+

Ultimi round: stato, vincitore, importi e transazione di payout.

+ +
+
+ + + + + +
IDStatoAperturaVincitorePool (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.

+ +
+
+ + + + + +
IDTipoStatoTxidFee rateTentativiTrasmessa
+
+
+
+ +
+

Audit log

+

Ultimi eventi registrati dal sistema (config, bet, payout, accessi a chiavi private, ecc.).

+ +
+
+ + + + + +
IDEventoDettagliUtenteRoundQuando
+
+
+
+ + +
@@ -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)} - `).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(); });