diff --git a/app/api/routes/rounds.py b/app/api/routes/rounds.py
index 41605e7..2b55a54 100644
--- a/app/api/routes/rounds.py
+++ b/app/api/routes/rounds.py
@@ -1,6 +1,6 @@
from datetime import timedelta, timezone
-from fastapi import APIRouter, Depends
+from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,16 +24,21 @@ class CurrentRoundResponse(BaseModel):
draw_animation_seconds: int
winner_user_id: int | None = None
winner_amount_sats: int | None = None
+ chain_tip_height: int | None = None
@router.get("/current", response_model=CurrentRoundResponse)
-async def current_round(session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse:
+async def current_round(request: Request, session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse:
config = await get_round_config(session)
round_ = await get_active_round(session)
+ listener = request.app.state.electrum_listener
+ chain_tip_height = listener.tip_height or None
if round_ is None:
await session.commit()
return CurrentRoundResponse(
- bet_amount_sats=config.bet_amount_sats, draw_animation_seconds=config.draw_animation_seconds
+ bet_amount_sats=config.bet_amount_sats,
+ draw_animation_seconds=config.draw_animation_seconds,
+ chain_tip_height=chain_tip_height,
)
participant_count = await session.scalar(
@@ -54,4 +59,5 @@ async def current_round(session: AsyncSession = Depends(get_session)) -> Current
draw_animation_seconds=config.draw_animation_seconds,
winner_user_id=round_.winner_user_id,
winner_amount_sats=round_.winner_amount_sats,
+ chain_tip_height=chain_tip_height,
)
diff --git a/app/static/admin.html b/app/static/admin.html
index b2a25c5..c101cb5 100644
--- a/app/static/admin.html
+++ b/app/static/admin.html
@@ -57,6 +57,26 @@
.navbar .nav-tab:hover { color: var(--color-foreground); }
.navbar .spacer { flex: 1; }
+ .chain-status-pill { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; font-size: 0.82rem; white-space: nowrap; }
+ .status-dot {
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
+ background: var(--color-muted-foreground);
+ }
+ .status-dot.status-open {
+ background: var(--color-success);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 18%, transparent);
+ }
+ .status-dot.status-drawing {
+ background: var(--color-primary);
+ animation: status-dot-pulse 1400ms ease-in-out infinite;
+ }
+ .status-dot.status-waiting { background: var(--color-muted-foreground); }
+ @keyframes status-dot-pulse {
+ 0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 45%, transparent); }
+ 50% { box-shadow: 0 0 0 5px transparent; }
+ }
+ .chain-block { color: var(--color-muted-foreground); font-size: 0.82rem; white-space: nowrap; }
+
main { max-width: 960px; margin: 0 auto; padding: 24px 20px 80px; }
.view { display: none; }
@@ -187,7 +207,12 @@
Transazioni pendenti
Audit log
-
+
+
+ Connessione…
+
+ Blocco —
+
@@ -342,6 +367,42 @@ function fmtDate(iso) {
return new Date(iso).toLocaleString('it-IT');
}
+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;
+}
+
const VIEWS = ['parametri', 'utenti', 'round', 'pending', 'audit'];
function switchView(name) {
@@ -356,6 +417,7 @@ function switchView(name) {
function showDashboard() {
document.getElementById('login-section').classList.add('hidden');
document.getElementById('dashboard-section').classList.remove('hidden');
+ startChainStatusPolling();
}
async function loadDashboard() {
@@ -379,6 +441,7 @@ async function adminLogin() {
}
function adminLogout() {
+ stopChainStatusPolling();
sessionStorage.removeItem('plm_admin_token');
adminToken = null;
document.getElementById('admin-token').value = '';
diff --git a/app/static/index.html b/app/static/index.html
index efbb286..f1dd84c 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -3,7 +3,7 @@
-PLM Lottery — Test
+PLM Lottery