From 78109aa4c4d7ddac8b1535da972cc8112f56b737 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Wed, 22 Jul 2026 10:16:51 +0200 Subject: [PATCH] Add a marketing landing hero and a live chain/round status strip The user page's login screen was a bare test dashboard with no explanation of how the lottery works; it now leads with a hero (3-step explainer, trust pills) shown only while logged out, plus a bento-style nav and a glowing round card during the draw. Both the user and admin pages now show the current chain tip height and lottery status (open / drawing / waiting for next round) via a small status strip, polled from /rounds/current (extended with chain_tip_height sourced from ElectrumListener.tip_height). --- app/api/routes/rounds.py | 12 +- app/static/admin.html | 65 ++++++++++- app/static/index.html | 233 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 295 insertions(+), 15 deletions(-) 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