Commit Graph
53 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5 bae08f2759 Extract inline JS from index.html/admin.html into app.js/admin.js
Mirrors the earlier CSS extraction (style.css/admin.css) — app/static/ is
now split cleanly by file type (markup, styles, script) instead of mixing
JS inline in the HTML. No behavior change: the script content moved
verbatim, referenced via <script src>. FastAPI's existing StaticFiles mount
serves the new files automatically, same as the CSS files already do.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:05:38 +02:00
davideandClaude Sonnet 5 aae0961c94 Bring docs in sync with recent features (pending balance, SSE, per-player reveal)
CLAUDE.md: bumped the stale test count (54 -> 76), added "Balance display"
and "Real-time updates (SSE)" sections, and rewrote the DRAW section's
frontend-reveal paragraph to describe the actual current behavior (dual
status/result boxes gated by user_played, closes_at-anchored reveal delay,
localStorage persistence, the last-round-result backstop) instead of the
older single-box design. Refined the "no history endpoints" known gap now
that GET /users/me/last-round-result exists (still not general history).

README.md: same test count fix, expanded coverage list.

docs/: fixed a pre-existing broken link in setup.md (admin-guide.md ->
guida-admin.md), added a note in running-the-server.md that editing the
bind-mounted Caddyfile needs an explicit `docker compose restart caddy`
(discovered while adding the SSE Caddy config in a prior change), and
rewrote guida-utente.md's draw/reveal section plus the balance/withdrawal
sections to match what the UI actually does now. guida-admin.md was
reviewed but needed no changes.

app/static/style.css: dropped `.toast.info`, dead since the toast-based
loss notification it styled was replaced by the persistent result box.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:05:27 +02:00
davideandClaude Sonnet 5 dda5bd14e1 Wire up the SSE push channel in both frontend dashboards
app/static/index.html: opens an EventSource against /rounds/stream (no auth
needed, see the previous commit) alongside the existing polling loops. On an
"update" notification, immediately re-runs the same refreshes polling would
eventually do (refreshRound/refreshMe/checkLastRoundResult when logged in,
refreshChainStatusOnly when logged out). Also reacts to the browser's "open"
event, which fires on the initial connection and on every automatic
reconnect — this re-syncs right away instead of leaving the page on stale
state until the next event or poll tick, which matters most right after a
dropped connection comes back.

app/static/admin.html: same channel, refreshing the chain-status bar and
whichever admin section is currently open (Utenti/Round/Transazioni
pendenti/Audit log) instead of requiring a manual tab switch to see new data.

Polling intervals are untouched in both pages — this is purely additive, so
a blocked/dropped SSE connection just degrades to the pre-existing behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 10:52:50 +02:00
davideandClaude Sonnet 5 f229f91632 Add server-push (SSE) notifications for round/bet/balance state changes
Frontend dashboards previously found out about state changes only on their
next poll tick (up to 15s, or 3s during a draw) — this adds a push channel
so updates land as soon as they happen instead.

- app/rounds/events.py: a small in-process pub/sub (RoundEventBroadcaster).
  The message carries no payload — it's just a "something changed, go
  refetch" signal, so it needs no auth and no knowledge of who's allowed to
  see what; personalization stays entirely in the existing REST endpoints.
- GET /rounds/stream: an SSE endpoint exposing that channel, with keep-alive
  comments so it survives idle periods behind a reverse proxy, and a
  defensive MAX_SUBSCRIBERS cap (well above the ~100 concurrent users
  expected) — past it, the endpoint returns 503 instead of opening a stream,
  and callers just keep working off polling.
- publish() calls added at every point that actually changes what a
  dashboard would want to know: new round opened, round status transitions
  (closing/drawing/paying_out/closed), a bet or withdrawal broadcast, any
  pending tx confirming (bet/withdrawal/payout), a deposit credited, and a
  new block tip arriving (the exact moment the "drawing" phase is waiting on).
- Caddyfile: excludes /rounds/stream from gzip encoding, since compression
  would buffer output and defeat the point of a live stream.

Single-process only by design for now (no cross-worker fan-out) and the
notification is a generic broadcast rather than a per-user channel — both
are deliberate scope decisions for the current ~100-user, single-container
deployment, not oversights. Polling is left fully in place as a fallback;
this is purely additive.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 10:52:07 +02:00
davideandClaude Sonnet 5 6a857f0e07 Show pending-inclusive balance and per-player round outcome reliably
Balance display: place_bet/request_withdrawal spend whole UTXOs and mark
them spent at broadcast time, well before confirmation, so the confirmed-only
balance could drop by far more than the amount actually moving. Add
compute_pending_balance() (app/wallet/balance.py) to fold the unconfirmed
change from in-flight bet/withdrawal PendingTransactions back in; GET
/users/me now returns pending_balance_sats + has_pending, and the frontend
shows it colored green (settled) or amber (still pending) instead of the
confirmed-only figure.

Round outcome display: the win/lose reveal and the "pagamento al vincitore
in corso" status were fighting over the same UI slot, and the reveal broke
across a page refresh. Now:
- The round-status box (generic phase progress) and the personal win/lose
  box are independent and can both be visible at once.
- The win/lose box only renders for users who actually played in that round
  (new user_played field on GET /rounds/current, via a new optional-auth
  dependency so the endpoint stays usable logged-out).
- The reveal delay is anchored to the round's server-provided closes_at
  instead of a client-side "first seen" timestamp, so repeated reloads can't
  reset it, and the revealed result is persisted in localStorage so it
  survives a refresh even after the round has fully closed.
- GET /users/me/last-round-result is a durable DB-backed backstop for
  players who miss the live window entirely (backgrounded tab, offline).

Also hardens the frontend polling loop: call() now times out instead of
hanging forever, and a session-epoch counter stops an in-flight request from
a previous login from resurrecting a duplicate poll loop after logout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 10:09:12 +02:00
davide f822911128 Fix frontend/server round-state desync bugs
- Add a request timeout (AbortController) to the frontend's call() helper, so a
  hung server request no longer freezes the entire polling chain silently.
- Add GET /users/me/last-round-result: a durable, DB-backed fallback for the
  round outcome, since /rounds/current drops winner_user_id the instant a
  round flips from "paying_out" to "closed" — a backgrounded tab or a missed
  poll could otherwise mean a player never learns whether they won.
- Refresh the balance display when a win is revealed (live or via the new
  backstop), instead of leaving the pre-payout balance on screen.
- Guard refreshRound() with a session-epoch counter so an in-flight request
  from a previous login can't re-arm the poll loop after logout, which
  previously produced a duplicate "zombie" polling chain.
2026-07-23 08:53:23 +02:00
davide ad71000777 Show a distinct message per round phase instead of one generic spinner
The draw-state panel and the top chain-status bar previously showed the
same "Estrazione in corso" text for closing/drawing/paying_out alike. Now
each phase gets its own copy (closing: waiting for last bet confirmation;
drawing: waiting for the draw block; paying_out: winner drawn from block
#N, payout in flight), using the round data already returned by the API.

Also drops the now-redundant pastDeadline fast-poll branch: status flips
to "closing" the instant the timer expires (see the scheduler change),
so isDrawing alone already covers that window.
2026-07-22 23:21:04 +02:00
davide 877a219aa4 Distinguish round-closing sub-phases and anchor cooldown to payout confirmation
Flip status to "closing" the instant the round timer expires, rather than
leaving it "open" (invisible) while in-flight bets confirm — /rounds/current
now surfaces this wait as its own phase instead of collapsing it into
"drawing". _tick() is updated to keep re-checking pending bets while status
is "closing" instead of short-circuiting on the old "status != open" guard.

Also re-stamp closed_at at actual payout confirmation time (not at the
earlier "closing" transition), since round_cooldown_seconds counts from
closed_at — with a short cooldown (e.g. 20s) and ~2 block-time draw+payout
wait, the old anchor meant the cooldown had already elapsed by the time the
round actually closed, making it a no-op.

Expose draw_block_height/draw_block_hash on GET /rounds/current so clients
can show which block the winner was drawn from.
2026-07-22 23:20:55 +02:00
davide 3d6f4a98c4 Extract inline CSS from index.html/admin.html into style.css/admin.css
Separates presentation from markup for easier maintenance — no visual
or behavioral change, same rules now loaded via <link rel="stylesheet">.
2026-07-22 23:20:40 +02:00
davideandClaude Sonnet 5 7b3555f8eb Tie the withdrawal minimum to the bet amount instead of a separate field
RoundConfig.min_amount_sats was an independently-configurable floor that
could drift out of sync with bet_amount_sats for no real reason (deposits
never had a server-side minimum anyway). Drop the field and enforce
amount_sats >= config.bet_amount_sats directly in request_withdrawal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 19:25:15 +02:00
davideandClaude Sonnet 5 9e7b726df7 Use the official PLM logo in the navbar and as favicon
Adds app/static/logo.svg — the official Palladium logo (fetched from the
palladium-coin/palladium-web-site repo), reprocessed for crisp display at
small sizes: the white/blue boundary is sharpened (steep contrast curve
instead of the soft anti-aliasing baked into the low-res source) and the
outer circle uses an analytically computed alpha edge instead of a
rasterized ellipse, so it stays perfectly round with no pixel staircase at
any size. Shape and colors are otherwise identical to the source artwork.

Wired up as the navbar brand mark (replacing the plain amber "P" square) in
index.html, and as the favicon for both index.html and admin.html.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 15:39:47 +02:00
davideandClaude Sonnet 5 5f62a8315e Add a "withdraw full balance" checkbox to the withdrawal form
Checking it disables the amount field, autofills it with the current
balance, and keeps it in sync if the balance changes while checked. The
actual withdrawal request uses the raw balance_sats value directly instead
of round-tripping through the PLM input field, avoiding float-rounding
drift when withdrawing the exact full balance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:59:02 +02:00
davideandClaude Sonnet 5 80c413ad3d Show the net prize as the jackpot instead of the gross pool
GET /rounds/current's jackpot_sats was the full pool (participant_count *
bet_amount_sats), which overstates what the winner actually receives once
the 30% commission is taken out at payout time. Apply the same 70% split
rounds/scheduler.py uses so the number shown during the round already
matches the real payout, without surfacing the fee itself in the UI/docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:53:09 +02:00
davideandClaude Sonnet 5 1acb6c3b16 Redesign the user dashboard with a modern fintech-style layout
Restructures app/static/index.html's visual design without touching any JS
logic or element IDs:

- Typography switched to IBM Plex Sans (financial/trustworthy pairing),
  Fira Code kept for addresses and numeric mono values.
- Design tokens extended with elevation shadows, an inset-surface color,
  and a scaled border-radius system; cards/buttons get subtle depth instead
  of flat borders.
- Navigation goes adaptive: a native-app-style fixed bottom tab bar on
  mobile (thumb-reachable), promoted back to an inline tab strip once the
  viewport is wide enough (>=720px) to fit one comfortably under the header.
- Top bar decluttered: balance shown as a highlighted pill (icon + value),
  Guida/Segnala un bug/Esci condensed into icon buttons with tooltips and
  aria-labels instead of full-text links.
- Round-status card gets a subtle gradient "hero" treatment to read as the
  central live widget of the dashboard.
- Content width now adapts from 480px (mobile) to 620px (>=720px) instead
  of staying phone-narrow on desktop; safe-area insets reserved for the
  fixed bottom bar and toast stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:50:17 +02:00
davideandClaude Sonnet 5 2b645f5e37 Show the user's balance in the navbar
Adds a navbar-balance span next to the username, kept in sync by the
existing refreshMe() calls (login, after a bet, after a withdrawal, manual
refresh) — no new endpoint needed, /users/me already returns balance_sats.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:36:30 +02:00
davideandClaude Sonnet 5 c4f6f2eed7 Add a help link and bug-report button to the user navbar
Serves docs/guida-utente.md as plain text at GET /guida (no markdown
rendering, kept simple) and links it from the navbar next to a "Segnala un
bug" button. The bug-report link is a placeholder GitHub issues URL
(REPLACE_ME) until a real repo exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:34:03 +02:00
davideandClaude Sonnet 5 67f70465e9 Warn users that withdrawals only support P2WPKH bech32 addresses
embit's address_to_scriptpubkey only knows Bitcoin's network prefixes, not
PLM's — so a legacy P2PKH address (prefix 55, "P...") silently produces a
None scriptpubkey instead of a clear rejection, and P2SH only works by
coincidence. Until the address decoding is fixed to use PLM's own network
prefixes, surface the current limitation to users in the withdrawal form
and the user guide instead of letting them lose funds to a broken address.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:29:05 +02:00
davideandClaude Sonnet 5 7dcf6d2756 Sync round countdown across clients and enforce the bet cutoff on deadline, not scheduler tick
The round timer relied on each client's own wall clock, so two browsers with
skewed local clocks showed different countdowns for the same round; the
server now also returns server_time so the frontend can correct for clock
skew. Also drop out-of-order /rounds/current responses (multiple independent
triggers could resolve late and revert the UI to a stale drawing/result
state) and prune per-round bookkeeping maps on round transitions.

Separately, place_bet only checked status == "open", leaving a window (up to
the scheduler's 5s tick interval) after a round's timer hit zero where a new
bet could still be accepted. place_bet now checks the round's own deadline
directly (round_accepts_bets), acting as an immediate "yellow light" for new
entries while still letting already-broadcast bets confirm before the round
closes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:07:09 +02:00
davide f27fe6243c Harden session handling, add password reset/change, and firm up round polling
Session hardening: / and /admin now respond with Cache-Control: no-store, and
both pages re-derive their auth state on pageshow (event.persisted) as a
safety net against bfcache showing a stale logged-in/out view across
back/forward navigation. The user page also syncs logout across tabs via the
storage event, since localStorage is shared but in-memory JS state isn't.

Password recovery: admin gets a "Reset" button per user (POST
/admin/users/{id}/reset-password) that generates and sets a new password,
shown once — passwords are Argon2-hashed and can never be recovered, only
replaced. Users get self-service password change (POST
/users/me/change-password, requires the current password) under a new
Profilo tab, alongside read-only account info (username, address, balance,
join date).

Round display robustness: the user dashboard now refreshes immediately on
tab visibility change (background tabs get their timers throttled hard),
shows an explicit "connessione persa" state after repeated failed polls
instead of silently freezing on stale data, and polls faster both right when
the countdown hits zero and through the gap where the round is past its
deadline but still waiting for in-flight bets to confirm before the server
actually closes it.
2026-07-22 12:00:09 +02:00
davide 162a63d04a Add a maintenance pause/resume switch and a proper user navbar
RoundConfig gets a paused flag toggled via new POST /admin/pause and
/admin/resume endpoints (audit-logged, surfaced as a "Manutenzione" card in
the admin Parametri view). Pausing only stops the *next* round from opening
once the current one closes — rounds/service.py:open_new_round_if_needed
still lets an in-progress round finish, draw, and pay out its winner
normally. GET /rounds/current exposes lottery_paused so the user page shows
a maintenance banner (even while logged out) instead of silently going idle.

Also replaces the user dashboard's stacked account-bar card + bento-grid
menu with a single sticky navbar (identity row + Deposito/Bet/Prelievo
tabs), and moves the page content into a dedicated .app-shell container so
the navbar itself can span full width.
2026-07-22 10:36:36 +02:00
davide 78109aa4c4 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).
2026-07-22 10:16:51 +02:00
davideandClaude Sonnet 5 8627f3fa0c Persist "closing" as its own committed status
_close_and_draw set round_.status = "closing" but then immediately
overwrote it in memory with "closed" (no participants) or "drawing"
(with participants) before the first session.commit() — so "closing"
was never actually written to the database, only ever visible in the
ORM object's transient in-memory state. GET /rounds/current (and the
frontend's "in chiusura" label) could never observe it.

Committing right after setting "closing"/closed_at, before querying
participants, makes it a real, briefly-observable state like the
others in the round lifecycle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:22:29 +02:00
davideandClaude Sonnet 5 9e1d7da22d Add draw_animation_seconds field to the admin Parametri section
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:03:50 +02:00
davideandClaude Sonnet 5 7e4b603ae3 Show a draw animation and win/lose reveal on the user dashboard
When the round leaves "open" (closing/drawing/paying_out), the round
card swaps its timer for a spinning "Estrazione del vincitore in
corso…" state instead — bets are already rejected server-side once the
round isn't open, this just reflects that visually. Once winner_user_id
is set AND at least draw_animation_seconds has elapsed since the round
started closing (client-tracked per round_id), it reveals "🎉 Hai
vinto! +N PLM" (compared against the user's own id from /users/me) or
"Non hai vinto questa volta.", with a success toast on a win. The
result stays on screen through the cooldown gap and only clears once a
genuinely new round opens (tracked via activeResultRoundId), not the
instant the old round has no active status.

Polling is now dynamic (setTimeout-chained, not setInterval): 3s while
the round is closing/drawing/paying_out for a responsive reveal, 15s
while open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:03:42 +02:00
davideandClaude Sonnet 5 669c3fb714 Add draw_animation_seconds config and expose winner info during a round
RoundConfig gains draw_animation_seconds (default 20) — the minimum
time the frontend's "estrazione in corso" animation plays before
revealing a winner. It's purely a UI cue: the real draw still waits for
a confirmed block for its entropy (rounds/scheduler.py), which usually
takes much longer than this value, so it only ever extends the
animation, never truncates the real wait.

GET /rounds/current now also returns draw_animation_seconds,
winner_user_id and winner_amount_sats (all populated once the
scheduler sets them on the round, i.e. from "paying_out" onward) so a
client can determine and reveal the outcome. GET /users/me now returns
the user's own id, needed client-side to compare against winner_user_id.

Admin config CRUD refactored to a shared field tuple (_CONFIG_FIELDS)
instead of repeating the same 7-then-8 field list three times.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:04:02 +02:00
davideandClaude Sonnet 5 d4aadf6b40 Rebuild the admin panel as a full dashboard with a navbar
Replaces the single scrolling page with a sticky top navbar and five
sections: Parametri (all seven RoundConfig fields, now including round
duration/cooldown/min amount/fee rate/RBF timeout alongside fee address
and bet amount), Utenti (unchanged), Round (history: status, winner,
pool/winner/fee amounts, payout txid), Transazioni pendenti (in-flight
bet/payout/withdrawal txs), and Audit log (recent system events with
parsed payload). Everything loads on login; switching a nav tab
refreshes that section's data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:06:38 +02:00
davideandClaude Sonnet 5 ed16e4d50b Add admin endpoints for round history, pending transactions, audit log
GET /admin/rounds: recent rounds with status, winner (joined username),
pool/winner/fee amounts, payout txid. GET /admin/pending-transactions:
in-flight bet/payout/withdrawal txs (RBF candidates). GET /admin/audit-log:
recent audit_log entries with parsed payload. All gated by the existing
require_admin dependency, feeding the new dashboard sections.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:05:49 +02:00
davideandClaude Sonnet 5 30bde96b6e Move all business/round parameters into DB config, out of env entirely
RoundConfig gains round_duration_seconds, round_cooldown_seconds,
min_amount_sats, fee_rate_sat_vb and rbf_timeout_seconds (plus a
hardcoded default for the pre-existing bet_amount_sats) as column
defaults on the model itself — get_round_config no longer seeds from
Settings at all. Every call site that read these from settings
(scheduler, bets, withdrawals, rounds service/route, RBF bumper) now
reads the DB-backed RoundConfig instead.

app/config.py now holds only true env-driven infra/secrets (database
URL, Electrum connection, master key, JWT, admin token) — no business
parameter has an env var anymore, matching an explicit decision to drop
the "seed from settings" indirection entirely rather than keep a env
fallback nobody should rely on.

Migration backfills the existing round_config row via server_default
(matching the old settings defaults) then drops the default, so future
rows go through the ORM/model defaults instead of a stale constant.

Tests updated: should_bump's timeout_seconds is now required (no
settings fallback); test_scheduler.py seeds a RoundConfig row directly
instead of monkeypatching settings; test_withdrawals.py and
test_rounds_service.py use local constants mirroring the model
defaults instead of reading them off settings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:05:40 +02:00
davideandClaude Sonnet 5 48a9eeb839 Gate the admin panel behind a real login screen
/admin now shows only a token field + "Accedi" button on first load —
config and users are structurally in the page but empty/hidden, no data
requested until the token is verified. On successful login (a GET
/admin/config that doesn't 403) it reveals the dashboard and loads
config + users automatically; no more separate "Carica configurazione"/
"Carica utenti" buttons. Token is kept in sessionStorage (cleared on
tab close) so a reload during the same session skips straight back to
the dashboard. Added a Logout button and Enter-to-submit on the token
field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:28:29 +02:00
davideandClaude Sonnet 5 f6035a888b Add password confirmation and an admin users/privkey panel
Registration now requires the password twice, rejected client-side on
mismatch before hitting the API. The admin page gets a Utenti card:
loads the user list (id, username, address, balance in PLM) and a
per-row "Mostra" button that reveals the private key after an explicit
confirm() — click again to hide it. A persistent warning banner notes
that every reveal is audit-logged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:22:03 +02:00
davideandClaude Sonnet 5 39c6ea1950 Add admin endpoints to list users and export a user's private key
GET /admin/users lists id/username/address/balance_sats/created_at.
GET /admin/users/{id}/privkey derives and returns that user's raw WIF
private key, for manual intervention (e.g. sweeping funds back if
something's stuck) — this is already a custodial system, the server
holds the master key everything is derived from, so this doesn't grant
a new capability, just exposes an existing one through the API. Every
access is audit-logged (admin_privkey_accessed).

Also fixes a pre-existing test-isolation bug in test_admin.py's client
fixture: app.db.session.get_session had `from app.db.base import
AsyncSessionLocal`, a one-time reference copy at first import — later
tests reassigning db_base.AsyncSessionLocal never reached it, so any
test mixing direct DB writes with router calls silently read/wrote
against a stale, possibly-disposed engine from whichever test ran
first. Fixed by also rebinding app.db.session.AsyncSessionLocal in the
fixture on every run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 14:21:55 +02:00
davideandClaude Sonnet 5 f21ecbd4ee Add a cooldown between rounds (ROUND_COOLDOWN_SECONDS)
open_new_round_if_needed now withholds opening the next round until
ROUND_COOLDOWN_SECONDS (default 30) have passed since the previous
round's closed_at, returning None in that window instead of a Round.
Without this, the next round opened within one scheduler tick (~5s) of
the previous payout confirming — not enough time for a player to
notice the round they were in actually resolved.

Callers updated: the scheduler treats None as "nothing to do this
tick", and place_bet raises a "try again shortly" BetError instead of
crashing on a None round.

Not in the original flowchart — a deliberate UX addition on top of it,
documented as such in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 11:29:59 +02:00
davideandClaude Sonnet 5 63bc1d911b Show round timer, players and jackpot in the dashboard
A card above the section menu polls GET /rounds/current every 15s and
ticks a mm:ss countdown to closes_at every second locally, showing
status (aperto/in chiusura/estrazione/pagamento), participant count
and jackpot in PLM. Refreshed immediately after placing a bet, and
timers are cleared on logout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:57:39 +02:00
davideandClaude Sonnet 5 7f2d0bcec6 Add a public current-round status endpoint
GET /rounds/current returns the active round's id/status, opened_at/
closes_at (derived from ROUND_DURATION_SECONDS), participant count and
jackpot (participant_count * bet_amount_sats). No active round still
returns bet_amount_sats so clients can render a fixed-entry hint either
way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:57:31 +02:00
davideandClaude Sonnet 5 7e3eec3e4e Turn the user dashboard into a menu-driven layout with a deposit QR
Deposito/Bet/Prelievo are now separate panels behind a top nav instead
of one long scroll of cards, and the Deposito panel renders the
deposit address as a QR code (via GET /qr/{address}) alongside the
existing copy-to-clipboard address box.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:52:06 +02:00
davideandClaude Sonnet 5 dd45ec75c6 Add a QR code endpoint for PLM addresses
GET /qr/{address} renders the address as a PNG QR code (qrcode[pil]),
gated by a bech32-shaped regex since it's otherwise unauthenticated —
the address itself isn't sensitive, but this keeps it from being used
as an arbitrary text-to-QR service.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:51:56 +02:00
davideandClaude Sonnet 5 8c659e2be5 Redesign the test/admin UIs and drop the on-page log panel
New visual system from the ui-ux-pro-max skill (gold/purple accents,
Fira Sans + Fira Code, light mode, WCAG AA contrast): card layout,
tabbed login/register, loading states on every button, and toast
feedback instead of a raw request/response JSON dump. The dump is gone
from both pages — diagnosing an error now means reading logs/app.log,
not staring at the page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:46:55 +02:00
davideandClaude Sonnet 5 7e5c372833 Log to a file instead of only stdout
All app and uvicorn logging now goes to logs/app.log (rotating,
10MB x5), and a catch-all exception handler logs full tracebacks there
before returning a generic 500 — so an error is traceable to its cause
without depending on how the process was launched. logs/ is gitignored,
like the other runtime artifacts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:46:47 +02:00
davideandClaude Sonnet 5 ddaca5520e Show and accept amounts in PLM in the test/admin UIs
Balance, withdrawal amount and bet_amount_sats are entered/displayed in
PLM in both static pages; conversion to sats happens client-side right
before the API call, since the backend contract stays sats-based.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:38:26 +02:00
davideandClaude Sonnet 5 a6dbe48457 Serve the admin panel at its own unlinked /admin page
Moves the fee_address/bet_amount_sats config form out of the main test
UI into a dedicated admin.html, served by a GET /admin route
(registered ahead of the StaticFiles mount so it doesn't shadow the
existing GET/PUT /admin/config API). Deliberately not linked from the
test UI in either direction: reachable only by knowing the URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:36:55 +02:00
davideandClaude Sonnet 5 41863691a0 Add admin panel to the test UI
Lets an operator load and update fee_address/bet_amount_sats from a
form (using the X-Admin-Token header) instead of curl/Swagger, with
inline status feedback and the same request/response log as the rest
of the page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:32:05 +02:00
davideandClaude Sonnet 5 ac5ee2ac2c Add static test UI for manual QA
Single-page vanilla HTML/JS frontend (register/login, balance,
place bet, withdraw) served by FastAPI at the same origin so it can
exercise the live API without CORS setup. Manual-testing aid only,
not part of the MVP spec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:27:05 +02:00
davideandClaude Sonnet 5 c45bf543c0 Wire up the FastAPI app and master-key bootstrap script
app/main.py assembles the lifespan-managed background tasks (Electrum
listener, round scheduler, confirmation poller, RBF bumper) and mounts
all routers behind a /health check. generate_master_key.py is the
one-time ops script that creates and Fernet-encrypts the server's
master xprv before first launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:42 +02:00
davideandClaude Sonnet 5 01331c1e4c Add admin config and audit log
Bearer-token-gated admin endpoints to read/update the DB-backed
operational config (fee_address, bet_amount_sats) without a redeploy,
plus a lightweight audit log writer for round/payout/config events.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:31 +02:00
davideandClaude Sonnet 5 8380b80d12 Add withdrawal flow
Builds and broadcasts a user->external-address PSBT with change back to
the user's own address, fee deducted from the withdrawn amount, and
registers the confirmation handler that marks the withdrawal confirmed.
Shares the per-user lock with bets so a build never races a spend from
the same UTXO set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:25 +02:00
davideandClaude Sonnet 5 5ce49d7b88 Add round lifecycle, draw algorithm and scheduler
Periodic scheduler (configurable round duration) that closes a round
only once all broadcast bets confirm, waits for the next block after
closing, draws a winner via block-hash-seeded modulo over participants
ordered by broadcast time, triggers the 70/30 payout, and only opens the
next round once that payout confirms. Draw logic is isolated in
draw.py as a deliberately simple, replaceable component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:18 +02:00
davideandClaude Sonnet 5 492fc29eca Add bet flow
Places the fixed-cost bet into the current round: builds and broadcasts
the user->pool PSBT with change back to the user's own address, enforces
at most one active bet per user, and registers the confirmation handler
that marks a bet confirmed and adds the participant to the round.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:11 +02:00
davideandClaude Sonnet 5 dce532f17e Add deposit crediting from confirmed UTXOs
Credits a user's internal balance once a UTXO on their deposit address
reaches 1 confirmation, keeping utxo_events as the source of truth and
cached_balance_sats as a derived read cache.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:26:03 +02:00
davideandClaude Sonnet 5 fc2aadbc7e Add transaction broadcast, confirmation polling and RBF fee-bump
Shared per-user locking to serialize bet/withdrawal PSBT builds
(tx/locks.py), a confirmation poller for pending outgoing transactions,
and the timeout->fee-bump->rebroadcast loop used by bets, payouts and
withdrawals alike (tx/broadcast.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:25:57 +02:00
davideandClaude Sonnet 5 107e592704 Add authentication and user profile endpoint
Argon2 password hashing, JWT session issuing/verification
(auth/security.py), register/login routes, the bearer-token
get_current_user dependency, and GET /users/me for address + balance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:25:49 +02:00