Almaatla commited on
Commit
90a557c
·
verified ·
1 Parent(s): ed43504

Update admin.html

Browse files
Files changed (1) hide show
  1. admin.html +373 -209
admin.html CHANGED
@@ -6,255 +6,419 @@
6
  <title>MPTrading Admin</title>
7
  <style>
8
  :root{
9
- --bg:#0b1020; --panel:#111a33; --panel2:#0f1730; --text:#e7eaf3;
10
- --muted:#aab2d5; --line:#233055; --green:#22c55e; --red:#ef4444; --blue:#60a5fa;
 
 
 
 
 
 
 
11
  }
12
  *{ box-sizing:border-box; }
13
- body{ margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); }
14
- 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); }
15
- header .title{ font-weight:700; letter-spacing:0.3px; }
16
- main{ padding:12px; display:grid; grid-template-columns: 1.2fr 1fr; gap:12px; height: calc(100vh - 52px); }
17
- .card{ background:var(--panel); border:1px solid var(--line); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; min-height:0; }
18
- .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); }
19
- .content{ padding:12px; overflow:auto; min-height:0; }
20
-
21
- label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 6px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  input, textarea, button{
23
- font:inherit; border-radius:10px; border:1px solid var(--line);
24
- background:#0c1430; color:var(--text); padding:10px 12px;
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
- textarea{ width:100%; min-height:260px; resize:vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size:12px; }
27
  input{ width:100%; }
28
  .row{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
29
  .row3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; }
30
- .actions{ display:flex; gap:10px; flex-wrap:wrap; margin-top:10px; }
 
 
 
 
 
 
31
  button{ cursor:pointer; }
32
  button.primary:hover{ background:rgba(96,165,250,0.12); }
33
  button.danger{ border-color:rgba(239,68,68,0.5); }
34
  button.danger:hover{ background:rgba(239,68,68,0.12); }
35
-
36
- table{ width:100%; border-collapse:collapse; font-size:13px; }
37
- th, td{ text-align:left; padding:8px 6px; border-bottom:1px solid rgba(35,48,85,0.6); }
 
 
 
 
 
 
 
 
38
  th{ color:var(--muted); font-weight:600; }
39
- td.mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
40
- .small{ font-size:12px; color:var(--muted); }
 
41
  .ok{ color:var(--green); }
42
  .err{ color:var(--red); }
43
- code{ color:#c7d2fe; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </style>
45
  </head>
46
-
47
  <body>
48
- <header>
49
- <div class="title">MPTrading Admin</div>
50
- <div class="small">Endpoints: <code>/admin/load_scenario</code>, <code>/admin/add_event</code>, <code>/admin/state</code></div>
51
- </header>
52
-
53
- <main>
54
- <section class="card">
55
- <h2>Scenario loader</h2>
56
- <div class="content">
57
- <label>Admin token (sent as <code>X-ADMIN-TOKEN</code>)</label>
58
- <input id="token" type="password" placeholder="Enter ADMIN_TOKEN" />
59
-
60
- <label>Scenario JSON</label>
61
- <textarea id="scenario"></textarea>
62
-
63
- <div class="actions">
64
- <button class="primary" id="loadBtn">Load scenario</button>
65
- <button class="danger" id="clearBtn">Clear events</button>
66
- <button id="refreshBtn">Refresh state</button>
67
- </div>
68
 
69
- <div id="msg" class="small" style="margin-top:10px;"></div>
 
 
 
 
 
 
70
 
71
- <div class="small" style="margin-top:10px;">
72
- Scenario format:
73
- <pre class="small" style="white-space:pre-wrap;margin:8px 0 0;color:var(--muted);">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  {
75
  "name": "FOMC week",
76
  "startDay": 0,
77
  "basePrice": 100.0,
78
  "defaultVolatility": 0.8,
79
  "events": [
80
- { "day": 20, "shockPct": 5.0, "volatility": 1.4, "news": "Rumor of rate cut" }
81
  ]
82
  }
83
- </pre>
84
- </div>
85
- </div>
86
- </section>
87
-
88
- <section class="card">
89
- <h2>Add event & schedule</h2>
90
- <div class="content">
91
- <div class="row3">
92
- <div>
93
- <label>Offset (ticks ahead)</label>
94
- <input id="offset" type="number" value="10" min="0" step="1" />
95
  </div>
96
- <div>
97
- <label>Shock % (e.g. 5 or -3)</label>
98
- <input id="shockPct" type="number" value="5" step="0.1" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
- <div>
101
- <label>Volatility (optional)</label>
102
- <input id="vol" type="number" placeholder="leave blank" step="0.1" />
 
 
 
103
  </div>
104
- </div>
105
 
106
- <label>News (optional)</label>
107
- <input id="news" type="text" placeholder="Headline to broadcast at that tick" />
108
 
109
- <div class="actions">
110
- <button class="primary" id="addBtn">Add event</button>
 
 
 
 
 
 
 
 
 
 
 
111
  </div>
 
 
112
 
113
- <div style="height:12px"></div>
 
114
 
115
- <div class="small">
116
- Current day: <span class="mono" id="curDay">--</span> · Current volatility: <span class="mono" id="curVol">--</span>
117
- </div>
118
 
119
- <div style="height:10px"></div>
120
-
121
- <table>
122
- <thead>
123
- <tr>
124
- <th>Day</th>
125
- <th>Shock%</th>
126
- <th>Vol</th>
127
- <th>News</th>
128
- </tr>
129
- </thead>
130
- <tbody id="eventsBody">
131
- <tr><td colspan="4" class="small">No events loaded.</td></tr>
132
- </tbody>
133
- </table>
134
- </div>
135
- </section>
136
- </main>
137
-
138
- <script>
139
- const $ = (id) => document.getElementById(id);
140
-
141
- function setMsg(text, ok=true){
142
- $("msg").textContent = text;
143
- $("msg").className = "small " + (ok ? "ok" : "err");
144
- }
145
-
146
- function headers(){
147
- const t = $("token").value || "";
148
- return { "Content-Type": "application/json", "X-ADMIN-TOKEN": t };
149
- }
150
-
151
- async function apiGet(path){
152
- const r = await fetch(path, { method: "GET", headers: headers() });
153
- const txt = await r.text();
154
- let data = null;
155
- try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
156
- if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
157
- return data;
158
- }
159
-
160
- async function apiPost(path, body){
161
- const r = await fetch(path, { method: "POST", headers: headers(), body: JSON.stringify(body || {}) });
162
- const txt = await r.text();
163
- let data = null;
164
- try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
165
- if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
166
- return data;
167
- }
168
-
169
- function renderEvents(events){
170
- const tb = $("eventsBody");
171
- tb.innerHTML = "";
172
- if (!events || !events.length){
173
- tb.innerHTML = `<tr><td colspan="4" class="small">No events loaded.</td></tr>`;
174
- return;
175
  }
176
- for (const e of events){
177
- const tr = document.createElement("tr");
178
- tr.innerHTML = `
179
- <td class="mono">${e.day}</td>
180
- <td class="mono">${Number(e.shockPct || 0).toFixed(2)}</td>
181
- <td class="mono">${(e.volatility === null || e.volatility === undefined) ? "" : Number(e.volatility).toFixed(2)}</td>
182
- <td>${escapeHtml(e.news || "")}</td>
183
- `;
184
- tb.appendChild(tr);
185
  }
186
- }
187
-
188
- function escapeHtml(s){
189
- return String(s).replace(/[&<>"']/g, (c) => ({
190
- "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
191
- }[c]));
192
- }
193
-
194
- async function refresh(){
195
- const st = await apiGet("/admin/state");
196
- $("curDay").textContent = st.day;
197
- $("curVol").textContent = st.currentVolatility;
198
- renderEvents(st.events || []);
199
- }
200
-
201
- // Default scenario template in textarea
202
- $("scenario").value = JSON.stringify({
203
- name: "Example scenario",
204
- startDay: 0,
205
- basePrice: 100.0,
206
- defaultVolatility: 0.8,
207
- events: [
208
- { day: 20, shockPct: 5.0, volatility: 1.4, news: "Rumor of rate cut" },
209
- { day: 30, shockPct: -3.0, volatility: 2.0, news: "Unexpected hike" }
210
- ]
211
- }, null, 2);
212
-
213
- $("refreshBtn").addEventListener("click", async () => {
214
- try { await refresh(); setMsg("State refreshed."); }
215
- catch(e){ setMsg(e.message, false); }
216
- });
217
-
218
- $("clearBtn").addEventListener("click", async () => {
219
- try { await apiPost("/admin/clear_events", {}); await refresh(); setMsg("Events cleared."); }
220
- catch(e){ setMsg(e.message, false); }
221
- });
222
-
223
- $("loadBtn").addEventListener("click", async () => {
224
- try {
225
- const obj = JSON.parse($("scenario").value);
226
- const res = await apiPost("/admin/load_scenario", obj);
227
- await refresh();
228
- setMsg(`Scenario loaded. startDay=${res.startDay}, eventsLoaded=${res.eventsLoaded}`);
229
- } catch(e) {
230
- setMsg(e.message, false);
231
  }
232
- });
233
-
234
- $("addBtn").addEventListener("click", async () => {
235
- try {
236
- const offset = Number($("offset").value);
237
- const shockPct = Number($("shockPct").value);
238
- const volRaw = ($("vol").value || "").trim();
239
- const volatility = volRaw === "" ? null : Number(volRaw);
240
- const news = ($("news").value || "").trim();
241
-
242
- const body = { offset, shockPct };
243
- if (volatility !== null && Number.isFinite(volatility)) body.volatility = volatility;
244
- if (news) body.news = news;
245
-
246
- await apiPost("/admin/add_event", body);
247
- await refresh();
248
- setMsg("Event added.");
249
- } catch(e) {
250
- setMsg(e.message, false);
251
  }
252
- });
253
 
254
- // Initial
255
- refresh().catch(() => {
256
- // likely missing token; leave quiet
257
- });
258
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  </body>
260
  </html>
 
6
  <title>MPTrading Admin</title>
7
  <style>
8
  :root{
9
+ --bg:#0b1020;
10
+ --panel:#111a33;
11
+ --panel2:#0f1730;
12
+ --text:#e7eaf3;
13
+ --muted:#aab2d5;
14
+ --line:#233055;
15
+ --green:#22c55e;
16
+ --red:#ef4444;
17
+ --blue:#60a5fa;
18
  }
19
  *{ box-sizing:border-box; }
20
+ body{
21
+ margin:0;
22
+ font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
23
+ background:var(--bg);
24
+ color:var(--text);
25
+ }
26
+ header{
27
+ height:52px;
28
+ display:flex;
29
+ align-items:center;
30
+ justify-content:space-between;
31
+ padding:0 14px;
32
+ border-bottom:1px solid var(--line);
33
+ background:rgba(0,0,0,0.15);
34
+ gap:12px;
35
+ }
36
+ header .title{
37
+ font-weight:700;
38
+ letter-spacing:0.3px;
39
+ white-space:nowrap;
40
+ }
41
+ .small{ font-size:12px; color:var(--muted); }
42
+ code{ color:#c7d2fe; }
43
+ main{
44
+ padding:12px;
45
+ display:grid;
46
+ grid-template-columns: 1.2fr 1fr;
47
+ gap:12px;
48
+ height: calc(100vh - 52px);
49
+ }
50
+ @media (max-width: 1000px){
51
+ main{ grid-template-columns: 1fr; height:auto; }
52
+ }
53
+ .card{
54
+ background:var(--panel);
55
+ border:1px solid var(--line);
56
+ border-radius:10px;
57
+ overflow:hidden;
58
+ display:flex;
59
+ flex-direction:column;
60
+ min-height:0;
61
+ }
62
+ .card h2{
63
+ margin:0;
64
+ padding:10px 12px;
65
+ font-size:13px;
66
+ color:var(--muted);
67
+ text-transform:uppercase;
68
+ letter-spacing:0.08em;
69
+ border-bottom:1px solid var(--line);
70
+ background:var(--panel2);
71
+ }
72
+ .content{
73
+ padding:12px;
74
+ overflow:auto;
75
+ min-height:0;
76
+ }
77
+ label{
78
+ display:block;
79
+ font-size:12px;
80
+ color:var(--muted);
81
+ margin:10px 0 6px;
82
+ }
83
  input, textarea, button{
84
+ font:inherit;
85
+ border-radius:10px;
86
+ border:1px solid var(--line);
87
+ background:#0c1430;
88
+ color:var(--text);
89
+ padding:10px 12px;
90
+ }
91
+ textarea{
92
+ width:100%;
93
+ min-height:260px;
94
+ resize:vertical;
95
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
96
+ font-size:12px;
97
+ line-height:1.35;
98
  }
 
99
  input{ width:100%; }
100
  .row{ display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
101
  .row3{ display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px; }
102
+ .actions{
103
+ display:flex;
104
+ gap:10px;
105
+ flex-wrap:wrap;
106
+ margin-top:10px;
107
+ align-items:center;
108
+ }
109
  button{ cursor:pointer; }
110
  button.primary:hover{ background:rgba(96,165,250,0.12); }
111
  button.danger{ border-color:rgba(239,68,68,0.5); }
112
  button.danger:hover{ background:rgba(239,68,68,0.12); }
113
+ table{
114
+ width:100%;
115
+ border-collapse:collapse;
116
+ font-size:13px;
117
+ }
118
+ th, td{
119
+ text-align:left;
120
+ padding:8px 6px;
121
+ border-bottom:1px solid rgba(35,48,85,0.6);
122
+ vertical-align:top;
123
+ }
124
  th{ color:var(--muted); font-weight:600; }
125
+ td.mono{
126
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
127
+ }
128
  .ok{ color:var(--green); }
129
  .err{ color:var(--red); }
130
+ .hint{
131
+ font-size:12px;
132
+ color:var(--muted);
133
+ margin-top:6px;
134
+ line-height:1.35;
135
+ }
136
+ .pill{
137
+ display:inline-block;
138
+ padding:2px 8px;
139
+ border-radius:999px;
140
+ border:1px solid var(--line);
141
+ font-size:12px;
142
+ color:var(--muted);
143
+ background:rgba(255,255,255,0.02);
144
+ }
145
  </style>
146
  </head>
 
147
  <body>
148
+ <header>
149
+ <div class="title">MPTrading Admin</div>
150
+ <div class="small">
151
+ Endpoints:
152
+ <code>/admin/state</code>, <code>/admin/load_scenario</code>, <code>/admin/add_event</code>, <code>/admin/clear_events</code>
153
+ </div>
154
+ </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ <main>
157
+ <!-- LEFT: Scenario loader -->
158
+ <section class="card">
159
+ <h2>Scenario loader</h2>
160
+ <div class="content">
161
+ <label>Admin token (sent as <code>X-ADMIN-TOKEN</code>)</label>
162
+ <input id="token" type="password" placeholder="Enter ADMIN_TOKEN" autocomplete="off" />
163
 
164
+ <div class="row">
165
+ <div>
166
+ <label>Market length override (optional)</label>
167
+ <input id="marketLength" type="number" min="600" step="1" placeholder="Leave blank = auto" />
168
+ <div class="hint">
169
+ Blank = auto-size to at least <span class="pill">600</span> and at least the highest scenario day + 1.
170
+ Set a number to force a specific length (minimum 600).
171
+ </div>
172
+ </div>
173
+ <div>
174
+ <label>Server state</label>
175
+ <div class="small">
176
+ Day: <span class="mono" id="curDay">--</span><br/>
177
+ Vol: <span class="mono" id="curVol">--</span><br/>
178
+ Market length: <span class="mono" id="curMktLen">--</span><br/>
179
+ Tick rate: <span class="mono" id="curTickRate">--</span>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <label>Scenario JSON</label>
185
+ <textarea id="scenario" spellcheck="false"></textarea>
186
+
187
+ <div class="actions">
188
+ <button class="primary" id="loadBtn">Load scenario</button>
189
+ <button class="danger" id="clearBtn">Clear events</button>
190
+ <button id="refreshBtn">Refresh state</button>
191
+ <span id="msg" class="small"></span>
192
+ </div>
193
+
194
+ <div class="small" style="margin-top:12px;">
195
+ Scenario format:
196
+ <pre class="small" style="white-space:pre-wrap;margin:8px 0 0;color:var(--muted);">
197
  {
198
  "name": "FOMC week",
199
  "startDay": 0,
200
  "basePrice": 100.0,
201
  "defaultVolatility": 0.8,
202
  "events": [
203
+ {"day": 20, "shockPct": 5.0, "volatility": 1.4, "news": "Rumor of rate cut"}
204
  ]
205
  }
206
+ </pre>
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
+ </div>
209
+ </section>
210
+
211
+ <!-- RIGHT: Add event schedule -->
212
+ <section class="card">
213
+ <h2>Add event schedule</h2>
214
+ <div class="content">
215
+ <div class="row3">
216
+ <div>
217
+ <label>Offset ticks ahead</label>
218
+ <input id="offset" type="number" value="10" min="0" step="1" />
219
+ </div>
220
+ <div>
221
+ <label>Shock % (e.g. 5 or -3)</label>
222
+ <input id="shockPct" type="number" value="5" step="0.1" />
223
+ </div>
224
+ <div>
225
+ <label>Volatility (optional)</label>
226
+ <input id="vol" type="number" placeholder="leave blank" step="0.1" />
227
+ </div>
228
  </div>
229
+
230
+ <label>News (optional)</label>
231
+ <input id="news" type="text" placeholder="Headline to broadcast at that tick" />
232
+
233
+ <div class="actions">
234
+ <button class="primary" id="addBtn">Add event</button>
235
  </div>
 
236
 
237
+ <div style="height:12px;"></div>
 
238
 
239
+ <table>
240
+ <thead>
241
+ <tr>
242
+ <th>Day</th>
243
+ <th>Shock</th>
244
+ <th>Vol</th>
245
+ <th>News</th>
246
+ </tr>
247
+ </thead>
248
+ <tbody id="eventsBody">
249
+ <tr><td colspan="4" class="small">No events loaded.</td></tr>
250
+ </tbody>
251
+ </table>
252
  </div>
253
+ </section>
254
+ </main>
255
 
256
+ <script>
257
+ const id = (x) => document.getElementById(x);
258
 
259
+ const token = id("token");
260
+ const scenario = id("scenario");
261
+ const marketLength = id("marketLength");
262
 
263
+ const curDay = id("curDay");
264
+ const curVol = id("curVol");
265
+ const curMktLen = id("curMktLen");
266
+ const curTickRate = id("curTickRate");
267
+
268
+ const msg = id("msg");
269
+ const eventsBody = id("eventsBody");
270
+
271
+ const loadBtn = id("loadBtn");
272
+ const clearBtn = id("clearBtn");
273
+ const refreshBtn = id("refreshBtn");
274
+ const addBtn = id("addBtn");
275
+
276
+ const offset = id("offset");
277
+ const shockPct = id("shockPct");
278
+ const vol = id("vol");
279
+ const news = id("news");
280
+
281
+ function setMsg(text, ok = true){
282
+ msg.textContent = text;
283
+ msg.className = ok ? "small ok" : "small err";
284
+ }
285
+
286
+ function headers(){
287
+ const t = token.value;
288
+ return {
289
+ "Content-Type": "application/json",
290
+ "X-ADMIN-TOKEN": t
291
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
+
294
+ async function apiGet(path){
295
+ const r = await fetch(path, { method: "GET", headers: headers() });
296
+ const txt = await r.text();
297
+ let data = null;
298
+ try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
299
+ if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
300
+ return data;
 
301
  }
302
+
303
+ async function apiPost(path, body){
304
+ const r = await fetch(path, { method: "POST", headers: headers(), body: JSON.stringify(body) });
305
+ const txt = await r.text();
306
+ let data = null;
307
+ try { data = JSON.parse(txt); } catch { data = { raw: txt }; }
308
+ if (!r.ok) throw new Error((data && data.detail) ? data.detail : ("HTTP " + r.status));
309
+ return data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
+
312
+ function escapeHtml(s){
313
+ return String(s).replace(/[&<>"']/g, (c) => ({
314
+ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"
315
+ }[c]));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
 
317
 
318
+ function renderEvents(events){
319
+ const tb = eventsBody;
320
+ tb.innerHTML = "";
321
+ if (!events || !events.length){
322
+ tb.innerHTML = '<tr><td colspan="4" class="small">No events loaded.</td></tr>';
323
+ return;
324
+ }
325
+ for (const e of events){
326
+ const tr = document.createElement("tr");
327
+ const shock = Number(e.shockPct || 0).toFixed(2);
328
+ const v = (e.volatility === null || e.volatility === undefined) ? "" : Number(e.volatility).toFixed(2);
329
+ tr.innerHTML = `
330
+ <td class="mono">${Number(e.day)}</td>
331
+ <td class="mono">${shock}</td>
332
+ <td class="mono">${v}</td>
333
+ <td>${escapeHtml(e.news || "")}</td>
334
+ `;
335
+ tb.appendChild(tr);
336
+ }
337
+ }
338
+
339
+ async function refresh(){
340
+ const st = await apiGet("/admin/state");
341
+ curDay.textContent = st.day;
342
+ curVol.textContent = st.currentVolatility;
343
+ curMktLen.textContent = st.marketLength;
344
+ curTickRate.textContent = st.tickRate;
345
+ renderEvents(st.events);
346
+ }
347
+
348
+ // Default scenario template
349
+ scenario.value = JSON.stringify({
350
+ name: "Example scenario",
351
+ startDay: 0,
352
+ basePrice: 100.0,
353
+ defaultVolatility: 0.8,
354
+ events: [
355
+ { day: 20, shockPct: 5.0, volatility: 1.4, news: "Rumor of rate cut" },
356
+ { day: 30, shockPct: -3.0, volatility: 2.0, news: "Unexpected hike" }
357
+ ]
358
+ }, null, 2);
359
+
360
+ refreshBtn.addEventListener("click", async () => {
361
+ try{
362
+ await refresh();
363
+ setMsg("State refreshed.");
364
+ }catch(e){
365
+ setMsg(e.message, false);
366
+ }
367
+ });
368
+
369
+ clearBtn.addEventListener("click", async () => {
370
+ try{
371
+ await apiPost("/admin/clear_events", {});
372
+ await refresh();
373
+ setMsg("Events cleared.");
374
+ }catch(e){
375
+ setMsg(e.message, false);
376
+ }
377
+ });
378
+
379
+ loadBtn.addEventListener("click", async () => {
380
+ try{
381
+ const obj = JSON.parse(scenario.value);
382
+
383
+ const ml = marketLength.value.trim();
384
+ if (ml){
385
+ obj.marketLength = Number(ml);
386
+ } else {
387
+ delete obj.marketLength; // ensure auto mode
388
+ }
389
+
390
+ const res = await apiPost("/admin/load_scenario", obj);
391
+ await refresh();
392
+ setMsg(`Scenario loaded. startDay=${res.startDay}, eventsLoaded=${res.eventsLoaded}, marketLength=${res.marketLength} (${res.marketLengthMode})`);
393
+ }catch(e){
394
+ setMsg(e.message, false);
395
+ }
396
+ });
397
+
398
+ addBtn.addEventListener("click", async () => {
399
+ try{
400
+ const off = Number(offset.value);
401
+ const sh = Number(shockPct.value);
402
+
403
+ const volRaw = vol.value.trim();
404
+ const volatility = volRaw ? Number(volRaw) : null;
405
+
406
+ const n = news.value.trim();
407
+
408
+ const body = { offset: off, shockPct: sh };
409
+ if (volatility !== null && Number.isFinite(volatility)) body.volatility = volatility;
410
+ if (n) body.news = n;
411
+
412
+ await apiPost("/admin/add_event", body);
413
+ await refresh();
414
+ setMsg("Event added.");
415
+ }catch(e){
416
+ setMsg(e.message, false);
417
+ }
418
+ });
419
+
420
+ // Initial refresh; if token missing it will fail silently
421
+ refresh().catch(() => {});
422
+ </script>
423
  </body>
424
  </html>