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>
This commit is contained in:
@@ -2,6 +2,7 @@ import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import app.bets.confirmation # noqa: F401 (registers the "bet" confirmation handler)
|
||||
import app.rounds.confirmation # noqa: F401 (registers the "payout" confirmation handler)
|
||||
@@ -61,3 +62,6 @@ app.include_router(admin_router)
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory="app/static", html=True), name="static")
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PLM Lottery - Test UI</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 16px; color: #222; }
|
||||
h1 { font-size: 1.4rem; }
|
||||
section { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
label { display: block; margin-top: 8px; font-size: 0.9rem; color: #555; }
|
||||
input { width: 100%; padding: 6px; margin-top: 2px; box-sizing: border-box; }
|
||||
button { margin-top: 12px; padding: 8px 14px; cursor: pointer; }
|
||||
.row { display: flex; gap: 8px; }
|
||||
.row > div { flex: 1; }
|
||||
pre { background: #f5f5f5; padding: 10px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
.hidden { display: none; }
|
||||
.addr { font-family: monospace; background: #f0f0f0; padding: 4px 6px; border-radius: 4px; }
|
||||
.balance { font-size: 1.6rem; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>PLM Lottery - Test UI</h1>
|
||||
|
||||
<section id="auth-section">
|
||||
<div class="row">
|
||||
<div>
|
||||
<h3>Registrati</h3>
|
||||
<label>Username</label>
|
||||
<input id="reg-username">
|
||||
<label>Password</label>
|
||||
<input id="reg-password" type="password">
|
||||
<button onclick="register()">Registra</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Login</h3>
|
||||
<label>Username</label>
|
||||
<input id="login-username">
|
||||
<label>Password</label>
|
||||
<input id="login-password" type="password">
|
||||
<button onclick="login()">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="dashboard-section" class="hidden">
|
||||
<h3>Account: <span id="dash-username"></span> <button onclick="logout()" style="float:right">Logout</button></h3>
|
||||
<div>Indirizzo di deposito / vincite:</div>
|
||||
<div class="addr" id="dash-address"></div>
|
||||
<div style="margin-top:12px">Saldo interno:</div>
|
||||
<div class="balance"><span id="dash-balance">-</span> sats <button onclick="refreshMe()">Aggiorna</button></div>
|
||||
|
||||
<hr>
|
||||
<h3>Bet</h3>
|
||||
<button onclick="placeBet()">Piazza bet (10 PLM)</button>
|
||||
|
||||
<hr>
|
||||
<h3>Withdrawal</h3>
|
||||
<label>Indirizzo esterno</label>
|
||||
<input id="wd-address" placeholder="plm1q...">
|
||||
<label>Importo (sats, 1 PLM = 100000000 sats)</label>
|
||||
<input id="wd-amount" placeholder="es. 200000000 per 2 PLM">
|
||||
<button onclick="withdraw()">Preleva</button>
|
||||
</section>
|
||||
|
||||
<h3>Log</h3>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script>
|
||||
let token = localStorage.getItem('plm_token');
|
||||
let username = localStorage.getItem('plm_username');
|
||||
let address = localStorage.getItem('plm_address');
|
||||
|
||||
function log(obj) {
|
||||
const el = document.getElementById('log');
|
||||
el.textContent = JSON.stringify(obj, null, 2) + "\n\n" + el.textContent;
|
||||
}
|
||||
|
||||
async function call(method, path, body) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
const res = await fetch(path, { method, headers, body: body ? JSON.stringify(body) : undefined });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
log({ request: method + ' ' + path, status: res.status, response: data });
|
||||
if (!res.ok) throw new Error(data.detail || res.statusText);
|
||||
return data;
|
||||
}
|
||||
|
||||
function showDashboard() {
|
||||
document.getElementById('auth-section').classList.add('hidden');
|
||||
document.getElementById('dashboard-section').classList.remove('hidden');
|
||||
document.getElementById('dash-username').textContent = username;
|
||||
document.getElementById('dash-address').textContent = address;
|
||||
refreshMe();
|
||||
}
|
||||
|
||||
async function register() {
|
||||
const u = document.getElementById('reg-username').value;
|
||||
const p = document.getElementById('reg-password').value;
|
||||
const data = await call('POST', '/auth/register', { username: u, password: p });
|
||||
token = data.access_token; username = u; address = data.address;
|
||||
localStorage.setItem('plm_token', token);
|
||||
localStorage.setItem('plm_username', username);
|
||||
localStorage.setItem('plm_address', address);
|
||||
showDashboard();
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const u = document.getElementById('login-username').value;
|
||||
const p = document.getElementById('login-password').value;
|
||||
const data = await call('POST', '/auth/login', { username: u, password: p });
|
||||
token = data.access_token; username = u; address = data.address;
|
||||
localStorage.setItem('plm_token', token);
|
||||
localStorage.setItem('plm_username', username);
|
||||
localStorage.setItem('plm_address', address);
|
||||
showDashboard();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.clear();
|
||||
token = username = address = null;
|
||||
document.getElementById('dashboard-section').classList.add('hidden');
|
||||
document.getElementById('auth-section').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function refreshMe() {
|
||||
try {
|
||||
const data = await call('GET', '/users/me');
|
||||
document.getElementById('dash-balance').textContent = data.balance_sats;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function placeBet() {
|
||||
try { await call('POST', '/bets', {}); } catch (e) {}
|
||||
refreshMe();
|
||||
}
|
||||
|
||||
async function withdraw() {
|
||||
const ext = document.getElementById('wd-address').value;
|
||||
const amt = parseInt(document.getElementById('wd-amount').value, 10);
|
||||
try { await call('POST', '/withdrawals', { external_address: ext, amount_sats: amt }); } catch (e) {}
|
||||
refreshMe();
|
||||
}
|
||||
|
||||
if (token) showDashboard();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user