MPTrading / admin.html
Almaatla's picture
Update admin.html
90a557c verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>MPTrading Admin</title>
<style>
:root{
--bg:#0b1020;
--panel:#111a33;
--panel2:#0f1730;
--text:#e7eaf3;
--muted:#aab2d5;
--line:#233055;
--green:#22c55e;
--red:#ef4444;
--blue:#60a5fa;
}
*{ box-sizing:border-box; }
body{
margin:0;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
background:var(--bg);
color:var(--text);
}
header{
height:52px;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 14px;
border-bottom:1px solid var(--line);
background:rgba(0,0,0,0.15);
gap:12px;
}
header .title{
font-weight:700;
letter-spacing:0.3px;
white-space:nowrap;
}
.small{ font-size:12px; color:var(--muted); }
code{ color:#c7d2fe; }
main{
padding:12px;
display:grid;
grid-template-columns: 1.2fr 1fr;
gap:12px;
height: calc(100vh - 52px);
}
@media (max-width: 1000px){
main{ grid-template-columns: 1fr; height:auto; }
}
.card{
background:var(--panel);
border:1px solid var(--line);
border-radius:10px;
overflow:hidden;
display:flex;
flex-direction:column;
min-height:0;
}
.card h2{
margin:0;
padding:10px 12px;
font-size:13px;
color:var(--muted);
text-transform:uppercase;
letter-spacing:0.08em;
border-bottom:1px solid var(--line);
background:var(--panel2);
}
.content{
padding:12px;
overflow:auto;
min-height:0;
}
label{
display:block;
font-size:12px;
color:var(--muted);
margin:10px 0 6px;
}
input, textarea, button{
font:inherit;
border-radius:10px;
border:1px solid var(--line);
background:#0c1430;
color:var(--text);
padding:10px 12px;
}
textarea{
width:100%;
min-height:260px;
resize:vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size:12px;
line-height:1.35;
}
input{ width:100%; }
.row{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
.row3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; }
.actions{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-top:10px;
align-items:center;
}
button{ cursor:pointer; }
button.primary:hover{ background:rgba(96,165,250,0.12); }
button.danger{ border-color:rgba(239,68,68,0.5); }
button.danger:hover{ background:rgba(239,68,68,0.12); }
table{
width:100%;
border-collapse:collapse;
font-size:13px;
}
th, td{
text-align:left;
padding:8px 6px;
border-bottom:1px solid rgba(35,48,85,0.6);
vertical-align:top;
}
th{ color:var(--muted); font-weight:600; }
td.mono{
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
.ok{ color:var(--green); }
.err{ color:var(--red); }
.hint{
font-size:12px;
color:var(--muted);
margin-top:6px;
line-height:1.35;
}
.pill{
display:inline-block;
padding:2px 8px;
border-radius:999px;
border:1px solid var(--line);
font-size:12px;
color:var(--muted);
background:rgba(255,255,255,0.02);
}
</style>
</head>
<body>
<header>
<div class="title">MPTrading Admin</div>
<div class="small">
Endpoints:
<code>/admin/state</code>, <code>/admin/load_scenario</code>, <code>/admin/add_event</code>, <code>/admin/clear_events</code>
</div>
</header>
<main>
<!-- LEFT: Scenario loader -->
<section class="card">
<h2>Scenario loader</h2>
<div class="content">
<label>Admin token (sent as <code>X-ADMIN-TOKEN</code>)</label>
<input id="token" type="password" placeholder="Enter ADMIN_TOKEN" autocomplete="off" />
<div class="row">
<div>
<label>Market length override (optional)</label>
<input id="marketLength" type="number" min="600" step="1" placeholder="Leave blank = auto" />
<div class="hint">
Blank = auto-size to at least <span class="pill">600</span> and at least the highest scenario day + 1.
Set a number to force a specific length (minimum 600).
</div>
</div>
<div>
<label>Server state</label>
<div class="small">
Day: <span class="mono" id="curDay">--</span><br/>
Vol: <span class="mono" id="curVol">--</span><br/>
Market length: <span class="mono" id="curMktLen">--</span><br/>
Tick rate: <span class="mono" id="curTickRate">--</span>
</div>
</div>
</div>
<label>Scenario JSON</label>
<textarea id="scenario" spellcheck="false"></textarea>
<div class="actions">
<button class="primary" id="loadBtn">Load scenario</button>
<button class="danger" id="clearBtn">Clear events</button>
<button id="refreshBtn">Refresh state</button>
<span id="msg" class="small"></span>
</div>
<div class="small" style="margin-top:12px;">
Scenario format:
<pre class="small" style="white-space:pre-wrap;margin:8px 0 0;color:var(--muted);">
{
"name": "FOMC week",
"startDay": 0,
"basePrice": 100.0,
"defaultVolatility": 0.8,
"events": [
{"day": 20, "shockPct": 5.0, "volatility": 1.4, "news": "Rumor of rate cut"}
]
}
</pre>
</div>
</div>
</section>
<!-- RIGHT: Add event schedule -->
<section class="card">
<h2>Add event schedule</h2>
<div class="content">
<div class="row3">
<div>
<label>Offset ticks ahead</label>
<input id="offset" type="number" value="10" min="0" step="1" />
</div>
<div>
<label>Shock % (e.g. 5 or -3)</label>
<input id="shockPct" type="number" value="5" step="0.1" />
</div>
<div>
<label>Volatility (optional)</label>
<input id="vol" type="number" placeholder="leave blank" step="0.1" />
</div>
</div>
<label>News (optional)</label>
<input id="news" type="text" placeholder="Headline to broadcast at that tick" />
<div class="actions">
<button class="primary" id="addBtn">Add event</button>
</div>
<div style="height:12px;"></div>
<table>
<thead>
<tr>
<th>Day</th>
<th>Shock</th>
<th>Vol</th>
<th>News</th>
</tr>
</thead>
<tbody id="eventsBody">
<tr><td colspan="4" class="small">No events loaded.</td></tr>
</tbody>
</table>
</div>
</section>
</main>
<script>
const id = (x) => document.getElementById(x);
const token = id("token");
const scenario = id("scenario");
const marketLength = id("marketLength");
const curDay = id("curDay");
const curVol = id("curVol");
const curMktLen = id("curMktLen");
const curTickRate = id("curTickRate");
const msg = id("msg");
const eventsBody = id("eventsBody");
const loadBtn = id("loadBtn");
const clearBtn = id("clearBtn");
const refreshBtn = id("refreshBtn");
const addBtn = id("addBtn");
const offset = id("offset");
const shockPct = id("shockPct");
const vol = id("vol");
const news = id("news");
function setMsg(text, ok = true){
msg.textContent = text;
msg.className = ok ? "small ok" : "small err";
}
function headers(){
const t = token.value;
return {
"Content-Type": "application/json",
"X-ADMIN-TOKEN": t
};
}
async function apiGet(path){
const r = await fetch(path, { method: "GET", headers: headers() });
const txt = await r.text();
let data = null;
try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
return data;
}
async function apiPost(path, body){
const r = await fetch(path, { method: "POST", headers: headers(), body: JSON.stringify(body) });
const txt = await r.text();
let data = null;
try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
return data;
}
function escapeHtml(s){
return String(s).replace(/[&<>"']/g, (c) => ({
"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
}[c]));
}
function renderEvents(events){
const tb = eventsBody;
tb.innerHTML = "";
if (!events || !events.length){
tb.innerHTML = '<tr><td colspan="4" class="small">No events loaded.</td></tr>';
return;
}
for (const e of events){
const tr = document.createElement("tr");
const shock = Number(e.shockPct || 0).toFixed(2);
const v = (e.volatility === null || e.volatility === undefined) ? "" : Number(e.volatility).toFixed(2);
tr.innerHTML = `
<td class="mono">${Number(e.day)}</td>
<td class="mono">${shock}</td>
<td class="mono">${v}</td>
<td>${escapeHtml(e.news || "")}</td>
`;
tb.appendChild(tr);
}
}
async function refresh(){
const st = await apiGet("/admin/state");
curDay.textContent = st.day;
curVol.textContent = st.currentVolatility;
curMktLen.textContent = st.marketLength;
curTickRate.textContent = st.tickRate;
renderEvents(st.events);
}
// Default scenario template
scenario.value = JSON.stringify({
name: "Example scenario",
startDay: 0,
basePrice: 100.0,
defaultVolatility: 0.8,
events: [
{ day: 20, shockPct: 5.0, volatility: 1.4, news: "Rumor of rate cut" },
{ day: 30, shockPct: -3.0, volatility: 2.0, news: "Unexpected hike" }
]
}, null, 2);
refreshBtn.addEventListener("click", async () => {
try{
await refresh();
setMsg("State refreshed.");
}catch(e){
setMsg(e.message, false);
}
});
clearBtn.addEventListener("click", async () => {
try{
await apiPost("/admin/clear_events", {});
await refresh();
setMsg("Events cleared.");
}catch(e){
setMsg(e.message, false);
}
});
loadBtn.addEventListener("click", async () => {
try{
const obj = JSON.parse(scenario.value);
const ml = marketLength.value.trim();
if (ml){
obj.marketLength = Number(ml);
} else {
delete obj.marketLength; // ensure auto mode
}
const res = await apiPost("/admin/load_scenario", obj);
await refresh();
setMsg(`Scenario loaded. startDay=${res.startDay}, eventsLoaded=${res.eventsLoaded}, marketLength=${res.marketLength} (${res.marketLengthMode})`);
}catch(e){
setMsg(e.message, false);
}
});
addBtn.addEventListener("click", async () => {
try{
const off = Number(offset.value);
const sh = Number(shockPct.value);
const volRaw = vol.value.trim();
const volatility = volRaw ? Number(volRaw) : null;
const n = news.value.trim();
const body = { offset: off, shockPct: sh };
if (volatility !== null && Number.isFinite(volatility)) body.volatility = volatility;
if (n) body.news = n;
await apiPost("/admin/add_event", body);
await refresh();
setMsg("Event added.");
}catch(e){
setMsg(e.message, false);
}
});
// Initial refresh; if token missing it will fail silently
refresh().catch(() => {});
</script>
</body>
</html>