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>
This commit is contained in:
@@ -2,6 +2,7 @@ import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import app.bets.confirmation # noqa: F401 (registers the "bet" confirmation handler)
|
||||
@@ -64,4 +65,9 @@ async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.get("/admin", include_in_schema=False)
|
||||
async def admin_panel() -> FileResponse:
|
||||
return FileResponse("app/static/admin.html")
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory="app/static", html=True), name="static")
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PLM Lottery - Admin</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; }
|
||||
pre { background: #f5f5f5; padding: 10px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>PLM Lottery - Pannello Admin</h1>
|
||||
|
||||
<section>
|
||||
<p style="font-size:0.85rem;color:#555;margin-top:0">
|
||||
Configurazione operativa (indirizzo fee, importo bet), salvata nel database e modificabile in
|
||||
qualsiasi momento senza riavviare il server. Serve il token admin (variabile <code>ADMIN_TOKEN</code>
|
||||
nel file <code>.env</code> del server).
|
||||
</p>
|
||||
|
||||
<label>Admin token</label>
|
||||
<input id="admin-token" type="password" placeholder="valore di ADMIN_TOKEN">
|
||||
|
||||
<button onclick="adminLoad()">Carica configurazione attuale</button>
|
||||
|
||||
<div id="admin-form" class="hidden">
|
||||
<label>Fee address (dove finisce il 30% di ogni round)</label>
|
||||
<input id="admin-fee-address" placeholder="plm1q...">
|
||||
<label>Bet amount (sats, 1 PLM = 100000000 sats)</label>
|
||||
<input id="admin-bet-amount" placeholder="es. 1000000000 per 10 PLM">
|
||||
<button onclick="adminSave()">Salva</button>
|
||||
<div id="admin-status" style="margin-top:8px;font-size:0.9rem"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3>Log</h3>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script>
|
||||
function log(obj) {
|
||||
const el = document.getElementById('log');
|
||||
el.textContent = JSON.stringify(obj, null, 2) + "\n\n" + el.textContent;
|
||||
}
|
||||
|
||||
async function callAdmin(method, path, body) {
|
||||
const adminToken = document.getElementById('admin-token').value;
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken };
|
||||
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 adminStatus(msg, ok) {
|
||||
const el = document.getElementById('admin-status');
|
||||
el.textContent = msg;
|
||||
el.style.color = ok ? '#2a7a2a' : '#a02020';
|
||||
}
|
||||
|
||||
async function adminLoad() {
|
||||
try {
|
||||
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;
|
||||
document.getElementById('admin-form').classList.remove('hidden');
|
||||
adminStatus('Configurazione caricata.', true);
|
||||
} catch (e) {
|
||||
adminStatus('Errore: token admin non valido o server non raggiungibile.', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function adminSave() {
|
||||
const feeAddress = document.getElementById('admin-fee-address').value;
|
||||
const betAmount = parseInt(document.getElementById('admin-bet-amount').value, 10);
|
||||
try {
|
||||
await callAdmin('PUT', '/admin/config', { fee_address: feeAddress, bet_amount_sats: betAmount });
|
||||
adminStatus('Salvato.', true);
|
||||
} catch (e) {
|
||||
adminStatus('Errore nel salvataggio: ' + e.message, false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -63,29 +63,6 @@
|
||||
<button onclick="withdraw()">Preleva</button>
|
||||
</section>
|
||||
|
||||
<section id="admin-section">
|
||||
<h3>Admin</h3>
|
||||
<p style="font-size:0.85rem;color:#555;margin-top:-8px">
|
||||
Configurazione operativa (indirizzo fee, importo bet), salvata nel database e modificabile in
|
||||
qualsiasi momento senza riavviare il server. Serve il token admin (variabile <code>ADMIN_TOKEN</code>
|
||||
nel file <code>.env</code> del server).
|
||||
</p>
|
||||
|
||||
<label>Admin token</label>
|
||||
<input id="admin-token" type="password" placeholder="valore di ADMIN_TOKEN">
|
||||
|
||||
<button onclick="adminLoad()">Carica configurazione attuale</button>
|
||||
|
||||
<div id="admin-form" class="hidden">
|
||||
<label>Fee address (dove finisce il 30% di ogni round)</label>
|
||||
<input id="admin-fee-address" placeholder="plm1q...">
|
||||
<label>Bet amount (sats, 1 PLM = 100000000 sats)</label>
|
||||
<input id="admin-bet-amount" placeholder="es. 1000000000 per 10 PLM">
|
||||
<button onclick="adminSave()">Salva</button>
|
||||
<div id="admin-status" style="margin-top:8px;font-size:0.9rem"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3>Log</h3>
|
||||
<pre id="log"></pre>
|
||||
|
||||
@@ -166,45 +143,6 @@ async function withdraw() {
|
||||
}
|
||||
|
||||
if (token) showDashboard();
|
||||
|
||||
async function callAdmin(method, path, body) {
|
||||
const adminToken = document.getElementById('admin-token').value;
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Admin-Token': adminToken };
|
||||
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 adminStatus(msg, ok) {
|
||||
const el = document.getElementById('admin-status');
|
||||
el.textContent = msg;
|
||||
el.style.color = ok ? '#2a7a2a' : '#a02020';
|
||||
}
|
||||
|
||||
async function adminLoad() {
|
||||
try {
|
||||
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;
|
||||
document.getElementById('admin-form').classList.remove('hidden');
|
||||
adminStatus('Configurazione caricata.', true);
|
||||
} catch (e) {
|
||||
adminStatus('Errore: token admin non valido o server non raggiungibile.', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function adminSave() {
|
||||
const feeAddress = document.getElementById('admin-fee-address').value;
|
||||
const betAmount = parseInt(document.getElementById('admin-bet-amount').value, 10);
|
||||
try {
|
||||
await callAdmin('PUT', '/admin/config', { fee_address: feeAddress, bet_amount_sats: betAmount });
|
||||
adminStatus('Salvato.', true);
|
||||
} catch (e) {
|
||||
adminStatus('Errore nel salvataggio: ' + e.message, false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user