3VVM commited on
Commit
d21ee4b
·
verified ·
1 Parent(s): 3fb52e7

Upload 3 files

Browse files
Files changed (3) hide show
  1. frontend/app.js +548 -0
  2. frontend/index.html +144 -0
  3. frontend/styles.css +172 -0
frontend/app.js ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ let appState = null;
3
+ let sourceRaw = "";
4
+ let currentTf = 30;
5
+ let selectedSymbol = "frxEURUSD";
6
+
7
+ const tfList = [30, 60, 120, 180, 300];
8
+ const tfLabels = {30: "30s", 60: "1m", 120: "2m", 180: "3m", 300: "5m"};
9
+
10
+ const $ = (id) => document.getElementById(id);
11
+
12
+ function formatUTC(ts) {
13
+ if (ts === null || ts === undefined || Number.isNaN(Number(ts))) return "—";
14
+ const d = new Date(Number(ts) * 1000);
15
+ return d.toISOString().replace("T", " ").replace("Z", " UTC");
16
+ }
17
+
18
+ function formatNumber(v, digits = 6) {
19
+ if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
20
+ const n = Number(v);
21
+ if (!Number.isFinite(n)) return "—";
22
+ if (Math.abs(n) >= 1000) return n.toFixed(2);
23
+ if (Math.abs(n) >= 1) return n.toFixed(4);
24
+ return n.toFixed(digits);
25
+ }
26
+
27
+ function setPill(id, text, cls = "") {
28
+ const el = $(id);
29
+ if (!el) return;
30
+ el.textContent = text;
31
+ el.className = "status-pill " + (cls || "subtle");
32
+ }
33
+
34
+ function prettyJSON(obj) {
35
+ return JSON.stringify(obj, null, 2);
36
+ }
37
+
38
+ function safeObj(v, fallback = {}) {
39
+ return (v && typeof v === "object") ? v : fallback;
40
+ }
41
+
42
+ function renderCodeViewer(htmlText) {
43
+ const viewer = $("sourceViewer");
44
+ if (!viewer) return;
45
+ viewer.innerHTML = htmlText;
46
+ applyCodeSearch();
47
+ }
48
+
49
+ function applyCodeSearch() {
50
+ const query = ($("codeSearch")?.value || "").trim().toLowerCase();
51
+ const lines = document.querySelectorAll(".code-line");
52
+ let matches = 0;
53
+ let first = null;
54
+ lines.forEach((line) => {
55
+ const text = (line.dataset.text || "").toLowerCase();
56
+ const hit = !query || text.includes(query);
57
+ line.classList.toggle("hidden", !hit);
58
+ line.classList.toggle("match", !!query && hit);
59
+ if (hit && query) {
60
+ matches += 1;
61
+ if (!first) first = line;
62
+ }
63
+ });
64
+ const meta = $("codeMeta");
65
+ if (meta) {
66
+ meta.textContent = query ? `${matches} matching lines` : `${lines.length} lines loaded`;
67
+ }
68
+ if (query && first) {
69
+ first.scrollIntoView({behavior: "smooth", block: "center"});
70
+ }
71
+ }
72
+
73
+ function buildTfButtons() {
74
+ const wrap = $("tfButtons");
75
+ if (!wrap) return;
76
+ wrap.innerHTML = "";
77
+ tfList.forEach((tf) => {
78
+ const b = document.createElement("button");
79
+ b.className = "tf-btn" + (tf === currentTf ? " active" : "");
80
+ b.textContent = tfLabels[tf];
81
+ b.onclick = async () => {
82
+ currentTf = tf;
83
+ await fetch(`/api/config/timeframe/${tf}`, {method:"POST"});
84
+ buildTfButtons();
85
+ refreshState();
86
+ };
87
+ wrap.appendChild(b);
88
+ });
89
+ }
90
+
91
+ async function setSymbol(symbol) {
92
+ if (!symbol) return;
93
+ await fetch(`/api/config/symbol/${encodeURIComponent(symbol)}`, {method:"POST"});
94
+ selectedSymbol = symbol;
95
+ refreshState();
96
+ }
97
+
98
+ function populateSymbols(symbols) {
99
+ const sel = $("symbolSelect");
100
+ if (!sel) return;
101
+ sel.innerHTML = "";
102
+ (symbols || []).forEach((sym) => {
103
+ const opt = document.createElement("option");
104
+ opt.value = sym;
105
+ opt.textContent = sym;
106
+ sel.appendChild(opt);
107
+ });
108
+ if (symbols && symbols.length) {
109
+ sel.value = selectedSymbol;
110
+ if (!selectedSymbol || !symbols.includes(selectedSymbol)) {
111
+ selectedSymbol = symbols[0];
112
+ sel.value = selectedSymbol;
113
+ }
114
+ $("symbolInput").value = selectedSymbol;
115
+ }
116
+ }
117
+
118
+ function drawCandles(canvas, candles) {
119
+ if (!canvas) return;
120
+ const ctx = canvas.getContext("2d");
121
+ const w = canvas.width, h = canvas.height;
122
+ ctx.clearRect(0,0,w,h);
123
+
124
+ // background grid
125
+ ctx.strokeStyle = "rgba(120,140,180,0.16)";
126
+ ctx.lineWidth = 1;
127
+ for (let i = 0; i <= 8; i++) {
128
+ const y = Math.round((h - 24) * i / 8) + 12;
129
+ ctx.beginPath();
130
+ ctx.moveTo(8, y);
131
+ ctx.lineTo(w - 8, y);
132
+ ctx.stroke();
133
+ }
134
+
135
+ if (!candles || candles.length === 0) {
136
+ ctx.fillStyle = "#91a0be";
137
+ ctx.font = "18px monospace";
138
+ ctx.fillText("Waiting for candles…", 24, 38);
139
+ return;
140
+ }
141
+
142
+ const visible = candles.slice(-80);
143
+ const highs = visible.map(c => Number(c.high));
144
+ const lows = visible.map(c => Number(c.low));
145
+ const max = Math.max(...highs);
146
+ const min = Math.min(...lows);
147
+ const pad = (max - min) * 0.12 || (Math.abs(max) * 0.01) || 1;
148
+ const hi = max + pad;
149
+ const lo = min - pad;
150
+ const plotW = w - 36;
151
+ const plotH = h - 36;
152
+ const x0 = 18, y0 = 14;
153
+ const candleWidth = Math.max(4, Math.floor(plotW / visible.length * 0.68));
154
+ const gap = Math.max(2, Math.floor(plotW / visible.length * 0.28));
155
+
156
+ function yFor(price) {
157
+ return y0 + (hi - price) / (hi - lo) * plotH;
158
+ }
159
+
160
+ // price scale text
161
+ ctx.fillStyle = "#91a0be";
162
+ ctx.font = "12px monospace";
163
+ ctx.fillText(hi.toFixed(6), 18, 12);
164
+ ctx.fillText(lo.toFixed(6), 18, h - 8);
165
+
166
+ visible.forEach((c, i) => {
167
+ const o = Number(c.open), hgh = Number(c.high), l = Number(c.low), cl = Number(c.close);
168
+ const x = x0 + i * (candleWidth + gap);
169
+ const up = cl >= o;
170
+ const bodyTop = yFor(Math.max(o, cl));
171
+ const bodyBottom = yFor(Math.min(o, cl));
172
+ const wickTop = yFor(hgh);
173
+ const wickBottom = yFor(l);
174
+
175
+ ctx.strokeStyle = up ? "#58d68d" : "#ff7c7c";
176
+ ctx.beginPath();
177
+ ctx.moveTo(x + candleWidth/2, wickTop);
178
+ ctx.lineTo(x + candleWidth/2, wickBottom);
179
+ ctx.stroke();
180
+
181
+ ctx.fillStyle = up ? "rgba(88,214,141,0.85)" : "rgba(255,124,124,0.85)";
182
+ const bodyH = Math.max(1, bodyBottom - bodyTop);
183
+ ctx.fillRect(x, bodyTop, candleWidth, bodyH);
184
+ });
185
+
186
+ // chart label
187
+ const last = visible[visible.length - 1];
188
+ ctx.fillStyle = "#e6edf7";
189
+ ctx.font = "14px monospace";
190
+ ctx.fillText(`TF ${tfLabels[currentTf]} | O ${formatNumber(last.open)} H ${formatNumber(last.high)} L ${formatNumber(last.low)} C ${formatNumber(last.close)}`, 24, 24);
191
+ }
192
+
193
+ function drawSpark(canvas, ticks) {
194
+ if (!canvas) return;
195
+ const ctx = canvas.getContext("2d");
196
+ const w = canvas.width, h = canvas.height;
197
+ ctx.clearRect(0,0,w,h);
198
+ ctx.strokeStyle = "rgba(120,140,180,0.16)";
199
+ ctx.lineWidth = 1;
200
+ for (let i = 0; i <= 4; i++) {
201
+ const y = Math.round((h - 24) * i / 4) + 12;
202
+ ctx.beginPath();
203
+ ctx.moveTo(8, y);
204
+ ctx.lineTo(w - 8, y);
205
+ ctx.stroke();
206
+ }
207
+
208
+ if (!ticks || ticks.length < 2) {
209
+ ctx.fillStyle = "#91a0be";
210
+ ctx.font = "16px monospace";
211
+ ctx.fillText("Waiting for ticks…", 24, 32);
212
+ return;
213
+ }
214
+
215
+ const visible = ticks.slice(-120);
216
+ const prices = visible.map(t => Number(t.price));
217
+ const max = Math.max(...prices);
218
+ const min = Math.min(...prices);
219
+ const pad = (max - min) * 0.15 || (Math.abs(max) * 0.01) || 1;
220
+ const hi = max + pad;
221
+ const lo = min - pad;
222
+ const x0 = 18, y0 = 14;
223
+ const plotW = w - 36, plotH = h - 36;
224
+
225
+ function yFor(price) {
226
+ return y0 + (hi - price) / (hi - lo) * plotH;
227
+ }
228
+
229
+ ctx.strokeStyle = "#8dd5ff";
230
+ ctx.beginPath();
231
+ visible.forEach((t, i) => {
232
+ const x = x0 + (i / Math.max(1, visible.length - 1)) * plotW;
233
+ const y = yFor(Number(t.price));
234
+ if (i === 0) ctx.moveTo(x, y);
235
+ else ctx.lineTo(x, y);
236
+ });
237
+ ctx.stroke();
238
+
239
+ const last = visible[visible.length - 1];
240
+ ctx.fillStyle = "#e6edf7";
241
+ ctx.font = "14px monospace";
242
+ ctx.fillText(`Last ${formatNumber(last.price)} | Bid ${formatNumber(last.bid)} | Ask ${formatNumber(last.ask)} | Spread ${formatNumber(last.spread, 8)}`, 24, 24);
243
+ }
244
+
245
+ function cardClass(value, goodWhen = null, badWhen = null) {
246
+ const s = String(value ?? "").toLowerCase();
247
+ if (badWhen && badWhen.includes(s)) return "bad";
248
+ if (goodWhen && goodWhen.includes(s)) return "good";
249
+ if (["blocked", "invalidated", "expired", "error", "cold_start"].includes(s)) return "bad";
250
+ if (["confirmed", "normal", "strong", "warm"].includes(s)) return "good";
251
+ if (["degraded", "cautious", "candidate", "forming", "cooling", "demo"].includes(s)) return "warn";
252
+ return "";
253
+ }
254
+
255
+ function makeCard(label, value, cls = "") {
256
+ const div = document.createElement("div");
257
+ div.className = `status-card ${cls}`.trim();
258
+ div.innerHTML = `<div class="k">${label}</div><div class="v">${value}</div>`;
259
+ return div;
260
+ }
261
+
262
+ function renderStatusGrid(snapshot) {
263
+ const grid = $("statusGrid");
264
+ if (!grid) return;
265
+ const engine = safeObj(snapshot.engine);
266
+ const out = safeObj(engine.raw_output);
267
+ const conn = safeObj(snapshot.connection);
268
+ const tl = safeObj(snapshot.timeline);
269
+ const current = safeObj(tl.current);
270
+ const clk = safeObj(snapshot.clock);
271
+
272
+ const items = [
273
+ ["direction", out.direction ?? "—", cardClass(out.direction, ["buy","sell"])],
274
+ ["confidence", out.confidence !== undefined ? Number(out.confidence).toFixed(4) : "—", ""],
275
+ ["execution_suitability", out.execution_suitability ?? "—", cardClass(out.execution_suitability)],
276
+ ["market_state", out.market_state ?? "—", cardClass(out.market_state)],
277
+ ["regime_label", out.regime_label ?? "—", cardClass(out.regime_label)],
278
+ ["asset_mode", out.asset_mode ?? "—", cardClass(out.asset_mode)],
279
+ ["timeframe_alignment", out.timeframe_alignment !== undefined ? Number(out.timeframe_alignment).toFixed(4) : "—", ""],
280
+ ["manipulation_probability", out.manipulation_probability !== undefined ? Number(out.manipulation_probability).toFixed(4) : "—", ""],
281
+ ["liquidity_score", out.liquidity_score !== undefined ? Number(out.liquidity_score).toFixed(4) : "—", ""],
282
+ ["volatility_score", out.volatility_score !== undefined ? Number(out.volatility_score).toFixed(4) : "—", ""],
283
+ ["pressure_score", out.pressure_score !== undefined ? Number(out.pressure_score).toFixed(4) : "—", ""],
284
+ ["momentum_score", out.momentum_score !== undefined ? Number(out.momentum_score).toFixed(4) : "—", ""],
285
+ ["timing_score", out.timing_score !== undefined ? Number(out.timing_score).toFixed(4) : "—", ""],
286
+ ["signal_freshness", out.signal_freshness !== undefined ? Number(out.signal_freshness).toFixed(4) : "—", ""],
287
+ ["spread_health", out.spread_health !== undefined ? Number(out.spread_health).toFixed(4) : "—", ""],
288
+ ["data_quality", out.data_quality !== undefined ? Number(out.data_quality).toFixed(4) : "—", ""],
289
+ ["readability_score", out.readability_score !== undefined ? Number(out.readability_score).toFixed(4) : "—", ""],
290
+ ["blocked_flag", String(out.blocked_flag ?? false), cardClass(out.blocked_flag ? "blocked" : "ok")],
291
+ ["degraded_mode_flag", String(out.degraded_mode_flag ?? false), cardClass(out.degraded_mode_flag ? "degraded" : "ok")],
292
+ ["stale_signal_flag", String(out.stale_signal_flag ?? false), cardClass(out.stale_signal_flag ? "expired" : "ok")],
293
+ ["reason_summary", out.reason_summary ?? "—", ""],
294
+ ["signal_generated_at", current.generated_at ? formatUTC(current.generated_at) : "—", ""],
295
+ ["signal_expires_at", current.expires_at ? formatUTC(current.expires_at) : "—", ""],
296
+ ["signal_countdown", current.countdown !== undefined && current.countdown !== null ? `${Number(current.countdown).toFixed(1)}s` : "—", ""],
297
+ ["signal_age", current.age !== undefined ? `${Number(current.age).toFixed(1)}s` : "—", ""],
298
+ ["lifecycle_state", current.lifecycle_state ?? out.lifecycle_state ?? "—", cardClass(current.lifecycle_state ?? out.lifecycle_state ?? "")],
299
+ ["warm_up_fraction", out.warm_up_fraction !== undefined ? Number(out.warm_up_fraction).toFixed(4) : "—", ""],
300
+ ["operational_mode", out.operational_mode ?? conn.status ?? "—", cardClass(out.operational_mode ?? conn.status ?? "")],
301
+ ["base_timeframe", snapshot.base_ref?.timeframe_label ?? "—", ""],
302
+ ["selected_display_tf", conn.selected_display_tf_label ?? "—", ""],
303
+ ["tick_count", engine.tick_count ?? 0, ""],
304
+ ["is_warm", String(engine.is_warm ?? false), cardClass(engine.is_warm ? "warm" : "cold_start")],
305
+ ["live_mode", String(conn.live_mode ?? false), cardClass(conn.live_mode ? "confirmed" : "demo")],
306
+ ["demo_mode", String(conn.demo_mode ?? false), cardClass(conn.demo_mode ? "demo" : "ok")],
307
+ ["ws_status", conn.status ?? "—", cardClass(conn.status)],
308
+ ["last_error", conn.last_error ?? "—", ""],
309
+ ["server_time", clk.server_time ? formatUTC(clk.server_time) : "—", ""],
310
+ ];
311
+
312
+ grid.innerHTML = "";
313
+ items.forEach(([label, value, cls]) => grid.appendChild(makeCard(label, value, cls)));
314
+ }
315
+
316
+ function renderRawOutput(snapshot) {
317
+ const pre = $("rawOutput");
318
+ if (!pre) return;
319
+ const out = safeObj(snapshot.engine).raw_output || {};
320
+ pre.textContent = prettyJSON(out);
321
+ }
322
+
323
+ function renderHealth(snapshot) {
324
+ const pre = $("healthOutput");
325
+ if (!pre) return;
326
+ const health = safeObj(snapshot.engine).health || {};
327
+ pre.textContent = prettyJSON(health);
328
+ }
329
+
330
+ function renderDebug(snapshot) {
331
+ const wrap = $("debugTrace");
332
+ if (!wrap) return;
333
+ const dbg = safeObj(snapshot.engine).debug_trace || {};
334
+ const sections = [
335
+ ["candle", dbg.candle],
336
+ ["data_integrity", dbg.data_integrity],
337
+ ["market_state", dbg.market_state],
338
+ ["asset_profile", dbg.asset_profile],
339
+ ["session", dbg.session],
340
+ ["timeframe_fusion", dbg.timeframe_fusion],
341
+ ["events", dbg.events],
342
+ ["liquidity_pressure", dbg.liquidity_pressure],
343
+ ["technical_stack", dbg.technical_stack],
344
+ ["score", dbg.score],
345
+ ["lifecycle", dbg.lifecycle],
346
+ ["warmup", dbg.warmup],
347
+ ];
348
+ wrap.innerHTML = "";
349
+ sections.forEach(([name, value]) => {
350
+ const d = document.createElement("details");
351
+ d.open = false;
352
+ const summaryValue = value && typeof value === "object" ? JSON.stringify(Object.keys(value).slice(0, 5)) : String(value);
353
+ d.innerHTML = `<summary>${name}</summary><pre>${escapeHTML(JSON.stringify(value, null, 2))}</pre>`;
354
+ wrap.appendChild(d);
355
+ });
356
+ }
357
+
358
+ function escapeHTML(str) {
359
+ return String(str)
360
+ .replace(/&/g, "&amp;")
361
+ .replace(/</g, "&lt;")
362
+ .replace(/>/g, "&gt;");
363
+ }
364
+
365
+ function renderTimeline(snapshot) {
366
+ const wrap = $("signalTimeline");
367
+ if (!wrap) return;
368
+ const tl = safeObj(snapshot.timeline);
369
+ const rows = Array.isArray(tl.active_signals) ? tl.active_signals : [];
370
+ const current = tl.current;
371
+
372
+ const header = `<div class="signal-row head"><div>ID / Dir</div><div>Generated</div><div>Expires</div><div>Countdown / Age</div><div>State</div><div>Status</div></div>`;
373
+ const body = rows.map((s) => {
374
+ const countdown = (s.countdown === null || s.countdown === undefined) ? "—" : `${Number(s.countdown).toFixed(1)}s`;
375
+ const age = (s.age === null || s.age === undefined) ? "—" : `${Number(s.age).toFixed(1)}s`;
376
+ const status = s.invalidated ? "invalidated" : (s.stale ? "stale" : (s.active ? "active" : "closed"));
377
+ const statusClass = s.invalidated ? "expired" : (s.stale ? "stale" : (s.active ? "active" : ""));
378
+ return `
379
+ <div class="signal-row">
380
+ <div>#${s.signal_id} ${s.direction}</div>
381
+ <div>${formatUTC(s.generated_at)}</div>
382
+ <div>${formatUTC(s.expires_at)}</div>
383
+ <div>${countdown}<br><span class="meta">${age}</span></div>
384
+ <div>${s.lifecycle_state}</div>
385
+ <div class="${statusClass}">${status}</div>
386
+ </div>
387
+ `;
388
+ }).join("");
389
+
390
+ wrap.innerHTML = header + (body || `<div class="signal-row"><div>—</div><div>—</div><div>—</div><div>—</div><div>idle</div><div>inactive</div></div>`);
391
+ $("signalDirection").textContent = current?.direction ? `Direction: ${current.direction}` : "Direction: —";
392
+ $("signalLifecycle").textContent = current?.lifecycle_state ? `Lifecycle: ${current.lifecycle_state}` : "Lifecycle: —";
393
+ $("signalExpiryBucket").textContent = current?.expiry_bucket ? `Expiry bucket: ${current.expiry_bucket}` : "Expiry bucket: —";
394
+ $("signalFreshness").textContent = current?.confidence !== undefined ? `Confidence: ${Number(current.confidence).toFixed(4)}` : "Confidence: —";
395
+ $("signalState").textContent = current?.stale ? "Stale signal" : (current?.active ? "Active signal" : "Inactive");
396
+ }
397
+
398
+ function renderLogs(snapshot) {
399
+ const wrap = $("logList");
400
+ if (!wrap) return;
401
+ const logs = Array.isArray(snapshot.logs) ? snapshot.logs.slice(-24).reverse() : [];
402
+ wrap.innerHTML = logs.map((row) => `
403
+ <div class="log-item">
404
+ <div>${formatUTC(row.ts)}</div>
405
+ <div>${formatNumber(row.price)}</div>
406
+ <div>${row.market_state || ""}</div>
407
+ <div class="meta">dir ${row.direction || "—"} | conf ${row.confidence !== undefined ? Number(row.confidence).toFixed(4) : "—"} | ${row.execution_suitability || "—"} | ${row.lifecycle_state || "—"}</div>
408
+ <div class="reason">${escapeHTML(row.reason_summary || "")}</div>
409
+ </div>
410
+ `).join("");
411
+ }
412
+
413
+ function renderPrices(snapshot) {
414
+ const ticks = safeObj(snapshot.charts).price_stream || [];
415
+ const canvas = $("priceSpark");
416
+ drawSpark(canvas, ticks);
417
+ const list = $("tickList");
418
+ if (!list) return;
419
+ const rows = ticks.slice(-18).reverse().map(t => `
420
+ <div class="log-item">
421
+ <div>${formatUTC(t.ts)}</div>
422
+ <div>${formatNumber(t.price)}</div>
423
+ <div>${t.source || "tick"}</div>
424
+ <div class="meta">bid ${formatNumber(t.bid)} | ask ${formatNumber(t.ask)} | spread ${formatNumber(t.spread, 8)} | ${escapeHTML(t.symbol || "")}</div>
425
+ </div>
426
+ `).join("");
427
+ list.innerHTML = rows || `<div class="log-item"><div>Waiting for ticks…</div><div>—</div><div>—</div></div>`;
428
+ }
429
+
430
+ function renderChart(snapshot) {
431
+ const charts = safeObj(snapshot.charts);
432
+ const tf = String(charts.selected_timeframe || currentTf);
433
+ currentTf = Number(tf);
434
+ buildTfButtons();
435
+ const candles = safeObj(charts.timeframes)[String(currentTf)] || [];
436
+ drawCandles($("candleChart"), candles);
437
+ $("chartNote").textContent = `Selected: ${tfLabels[currentTf] || currentTf + "s"} | ${candles.length} candles | same live tick stream`;
438
+ }
439
+
440
+ function renderClock(snapshot) {
441
+ const clk = safeObj(snapshot.clock);
442
+ const conn = safeObj(snapshot.connection);
443
+ $("utcClock").textContent = formatUTC(clk.utc_now);
444
+ $("latestTick").textContent = clk.latest_tick_ts ? `${formatUTC(clk.latest_tick_ts)} (${Number(clk.latest_tick_age_sec || 0).toFixed(1)}s ago)` : "—";
445
+ $("baseRef").textContent = `${snapshot.base_ref?.timeframe_label || "30s"} | ${conn.selected_symbol || "—"} | ${conn.market_type || "unknown"}`;
446
+ const current = safeObj(snapshot.timeline).current;
447
+ $("signalAge").textContent = current?.age !== undefined && current?.age !== null ? `${Number(current.age).toFixed(1)}s` : "—";
448
+ $("signalCountdown").textContent = current?.countdown !== undefined && current?.countdown !== null ? `${Number(current.countdown).toFixed(1)}s` : "—";
449
+ }
450
+
451
+ function renderPills(snapshot) {
452
+ const conn = safeObj(snapshot.connection);
453
+ const engine = safeObj(snapshot.engine);
454
+ const out = safeObj(engine.raw_output);
455
+
456
+ setPill("connStatus", conn.status || "idle", cardClass(conn.status));
457
+ setPill("modeStatus", conn.demo_mode ? "demo mode" : (conn.live_mode ? "live mode" : (conn.status || "idle")), cardClass(conn.demo_mode ? "demo" : conn.status));
458
+ setPill("warmStatus", engine.is_warm ? "warm" : "cold start", engine.is_warm ? "good" : "warn");
459
+ }
460
+
461
+ function updateAll(snapshot) {
462
+ appState = snapshot;
463
+ selectedSymbol = safeObj(snapshot.connection).selected_symbol || selectedSymbol;
464
+ renderPills(snapshot);
465
+ renderClock(snapshot);
466
+ renderChart(snapshot);
467
+ renderPrices(snapshot);
468
+ renderStatusGrid(snapshot);
469
+ renderRawOutput(snapshot);
470
+ renderDebug(snapshot);
471
+ renderTimeline(snapshot);
472
+ renderLogs(snapshot);
473
+ renderHealth(snapshot);
474
+ }
475
+
476
+ async function refreshState() {
477
+ try {
478
+ const res = await fetch("/api/state", {cache: "no-store"});
479
+ const data = await res.json();
480
+ updateAll(data);
481
+ } catch (err) {
482
+ setPill("connStatus", "frontend offline", "bad");
483
+ }
484
+ }
485
+
486
+ async function loadSource() {
487
+ try {
488
+ const [rawRes, htmlRes] = await Promise.all([
489
+ fetch("/api/source", {cache: "no-store"}),
490
+ fetch("/api/source-html", {cache: "no-store"}),
491
+ ]);
492
+ const raw = await rawRes.json();
493
+ sourceRaw = raw.source || "";
494
+ $("codeMeta").textContent = `${raw.line_count || 0} lines loaded`;
495
+ const htmlText = await htmlRes.text();
496
+ renderCodeViewer(htmlText);
497
+ } catch (err) {
498
+ $("sourceViewer").textContent = "Source unavailable.";
499
+ }
500
+ }
501
+
502
+ async function loadSymbols() {
503
+ try {
504
+ const res = await fetch("/api/symbols", {cache: "no-store"});
505
+ const data = await res.json();
506
+ populateSymbols(data.symbols || []);
507
+ selectedSymbol = data.selected || selectedSymbol;
508
+ $("symbolInput").value = selectedSymbol;
509
+ if (data.selected) {
510
+ $("symbolSelect").value = data.selected;
511
+ }
512
+ } catch (err) {
513
+ populateSymbols(["frxEURUSD","frxGBPUSD","frxUSDJPY","cryBTCUSD","cryETHUSD"]);
514
+ }
515
+ }
516
+
517
+ function attachHandlers() {
518
+ $("symbolApply").onclick = async () => {
519
+ const sel = $("symbolSelect").value;
520
+ await setSymbol(sel);
521
+ };
522
+ $("symbolSet").onclick = async () => {
523
+ const sym = $("symbolInput").value.trim();
524
+ if (sym) await setSymbol(sym);
525
+ };
526
+ $("codeSearch").addEventListener("input", applyCodeSearch);
527
+ $("codeCopy").onclick = async () => {
528
+ try {
529
+ await navigator.clipboard.writeText(sourceRaw || "");
530
+ $("codeCopy").textContent = "Copied";
531
+ setTimeout(() => $("codeCopy").textContent = "Copy", 1200);
532
+ } catch (err) {
533
+ $("codeCopy").textContent = "Copy failed";
534
+ setTimeout(() => $("codeCopy").textContent = "Copy", 1200);
535
+ }
536
+ };
537
+ }
538
+
539
+ async function boot() {
540
+ buildTfButtons();
541
+ attachHandlers();
542
+ await loadSymbols();
543
+ await loadSource();
544
+ await refreshState();
545
+ setInterval(refreshState, 1000);
546
+ }
547
+
548
+ window.addEventListener("DOMContentLoaded", boot);
frontend/index.html ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>MAYTHOS Live Space</title>
7
+ <link rel="stylesheet" href="/static/styles.css" />
8
+ </head>
9
+ <body>
10
+ <div class="app-shell">
11
+ <header class="topbar">
12
+ <div>
13
+ <div class="eyebrow">MAYTHOS Docker Space</div>
14
+ <h1>Real-time Engine Console</h1>
15
+ <p class="subtitle">Live Deriv stream, local candle aggregation, engine internals, debug trace, lifecycle, and health.</p>
16
+ </div>
17
+ <div class="top-actions">
18
+ <div class="status-pill" id="connStatus">connecting</div>
19
+ <div class="status-pill subtle" id="modeStatus">booting</div>
20
+ <div class="status-pill subtle" id="warmStatus">cold</div>
21
+ </div>
22
+ </header>
23
+
24
+ <section class="toolbar card">
25
+ <div class="toolbar-row">
26
+ <label class="field">
27
+ <span>Symbol</span>
28
+ <div class="inline">
29
+ <select id="symbolSelect"></select>
30
+ <button id="symbolApply" class="btn">Apply</button>
31
+ </div>
32
+ </label>
33
+ <label class="field">
34
+ <span>Custom symbol</span>
35
+ <div class="inline">
36
+ <input id="symbolInput" placeholder="frxEURUSD / cryBTCUSD" />
37
+ <button id="symbolSet" class="btn secondary">Set</button>
38
+ </div>
39
+ </label>
40
+ <label class="field">
41
+ <span>Display timeframe</span>
42
+ <div class="inline tf-buttons" id="tfButtons"></div>
43
+ </label>
44
+ </div>
45
+ <div class="toolbar-row compact">
46
+ <div class="mini-stat"><span>Base ref</span><strong id="baseRef">—</strong></div>
47
+ <div class="mini-stat"><span>Latest tick</span><strong id="latestTick">—</strong></div>
48
+ <div class="mini-stat"><span>Signal age</span><strong id="signalAge">—</strong></div>
49
+ <div class="mini-stat"><span>Expiry countdown</span><strong id="signalCountdown">—</strong></div>
50
+ <div class="mini-stat"><span>UTC clock</span><strong id="utcClock">—</strong></div>
51
+ </div>
52
+ </section>
53
+
54
+ <main class="grid">
55
+ <section class="panel card chart-panel">
56
+ <div class="panel-head">
57
+ <h2>Live candle chart</h2>
58
+ <div class="panel-note" id="chartNote">30s / 1m / 2m / 3m / 5m from the same tick feed</div>
59
+ </div>
60
+ <canvas id="candleChart" width="1200" height="420"></canvas>
61
+ <div class="signal-strip">
62
+ <div class="signal-chip" id="signalDirection">—</div>
63
+ <div class="signal-chip" id="signalLifecycle">—</div>
64
+ <div class="signal-chip" id="signalExpiryBucket">—</div>
65
+ <div class="signal-chip" id="signalFreshness">—</div>
66
+ <div class="signal-chip" id="signalState">—</div>
67
+ </div>
68
+ </section>
69
+
70
+ <section class="panel card price-panel">
71
+ <div class="panel-head">
72
+ <h2>Live price stream</h2>
73
+ <div class="panel-note">Ticks, spread, and local runtime status</div>
74
+ </div>
75
+ <canvas id="priceSpark" width="1200" height="180"></canvas>
76
+ <div class="ticker-list" id="tickList"></div>
77
+ </section>
78
+
79
+ <section class="panel card status-panel">
80
+ <div class="panel-head">
81
+ <h2>Status cards</h2>
82
+ <div class="panel-note">Raw engine output values and derived signal timeline</div>
83
+ </div>
84
+ <div class="status-grid" id="statusGrid"></div>
85
+ </section>
86
+
87
+ <section class="panel card engine-panel">
88
+ <div class="panel-head">
89
+ <h2>Raw engine output</h2>
90
+ <div class="panel-note">Full dict returned by MAYTHOS.tick()</div>
91
+ </div>
92
+ <pre class="json-box" id="rawOutput"></pre>
93
+ </section>
94
+
95
+ <section class="panel card debug-panel">
96
+ <div class="panel-head">
97
+ <h2>Nested debug trace</h2>
98
+ <div class="panel-note">candle, data_integrity, market_state, asset_profile, session, timeframe_fusion, events, liquidity_pressure, technical_stack, score, lifecycle, warmup</div>
99
+ </div>
100
+ <div id="debugTrace" class="debug-trace"></div>
101
+ </section>
102
+
103
+ <section class="panel card lifecycle-panel">
104
+ <div class="panel-head">
105
+ <h2>Signal lifecycle & expiry</h2>
106
+ <div class="panel-note">Generation time, expiry time, countdown, age, stale flag, and transitions</div>
107
+ </div>
108
+ <div id="signalTimeline" class="timeline"></div>
109
+ </section>
110
+
111
+ <section class="panel card health-panel">
112
+ <div class="panel-head">
113
+ <h2>Engine health</h2>
114
+ <div class="panel-note">Error counts and recent errors from engine_health()</div>
115
+ </div>
116
+ <pre class="json-box" id="healthOutput"></pre>
117
+ </section>
118
+
119
+ <section class="panel card log-panel">
120
+ <div class="panel-head">
121
+ <h2>Compact log viewer</h2>
122
+ <div class="panel-note">Recent candle-close ticks</div>
123
+ </div>
124
+ <div id="logList" class="log-list"></div>
125
+ </section>
126
+
127
+ <section class="panel card code-panel">
128
+ <div class="panel-head">
129
+ <h2>Source code viewer</h2>
130
+ <div class="panel-note">Scroll, search, and copy the uploaded maythos_patched.py verbatim</div>
131
+ </div>
132
+ <div class="code-toolbar">
133
+ <input id="codeSearch" placeholder="Search in source..." />
134
+ <button id="codeCopy" class="btn">Copy</button>
135
+ <span class="code-meta" id="codeMeta"></span>
136
+ </div>
137
+ <div id="sourceViewer" class="source-viewer"></div>
138
+ </section>
139
+ </main>
140
+ </div>
141
+
142
+ <script src="/static/app.js"></script>
143
+ </body>
144
+ </html>
frontend/styles.css ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ :root{
3
+ --bg:#0b1020;
4
+ --bg2:#11182c;
5
+ --card:#131b31;
6
+ --card2:#0f1629;
7
+ --text:#e6edf7;
8
+ --muted:#91a0be;
9
+ --line:#27324d;
10
+ --accent:#8dd5ff;
11
+ --accent2:#80ffb2;
12
+ --warn:#ffcc66;
13
+ --bad:#ff7c7c;
14
+ --good:#58d68d;
15
+ --chip:#1c2640;
16
+ --shadow:0 12px 30px rgba(0,0,0,.25);
17
+ --radius:18px;
18
+ --code-bg:#0a0f1d;
19
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
20
+ --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
21
+ }
22
+ *{box-sizing:border-box}
23
+ body{
24
+ margin:0;
25
+ font-family:var(--sans);
26
+ color:var(--text);
27
+ background:
28
+ radial-gradient(circle at top left, rgba(141,213,255,.11), transparent 25%),
29
+ radial-gradient(circle at 80% 0%, rgba(128,255,178,.08), transparent 28%),
30
+ linear-gradient(180deg, var(--bg), #070a13 85%);
31
+ }
32
+ .app-shell{max-width:1600px;margin:0 auto;padding:18px}
33
+ .topbar{
34
+ display:flex;justify-content:space-between;align-items:flex-start;gap:18px;
35
+ margin-bottom:16px;padding:18px 18px 8px 18px
36
+ }
37
+ .eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)}
38
+ h1{margin:.25rem 0 .4rem;font-size:32px}
39
+ .subtitle{margin:0;color:var(--muted);max-width:920px}
40
+ .top-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:flex-end}
41
+ .status-pill{
42
+ padding:8px 12px;border-radius:999px;background:var(--chip);border:1px solid var(--line);
43
+ font-size:13px;text-transform:uppercase;letter-spacing:.06em
44
+ }
45
+ .status-pill.subtle{opacity:.85}
46
+ .card{
47
+ background:linear-gradient(180deg, rgba(255,255,255,.03), transparent 40%), var(--card);
48
+ border:1px solid var(--line);
49
+ border-radius:var(--radius);
50
+ box-shadow:var(--shadow);
51
+ }
52
+ .toolbar{padding:16px;margin-bottom:16px}
53
+ .toolbar-row{display:grid;grid-template-columns:1.1fr 1.1fr 1.6fr;gap:14px;align-items:end}
54
+ .toolbar-row.compact{grid-template-columns:repeat(5,1fr);margin-top:12px}
55
+ .field span{display:block;color:var(--muted);font-size:12px;margin-bottom:6px}
56
+ .inline{display:flex;gap:8px;align-items:center}
57
+ select,input,button{
58
+ font:inherit
59
+ }
60
+ select,input{
61
+ width:100%;padding:11px 12px;border-radius:12px;background:var(--card2);color:var(--text);
62
+ border:1px solid var(--line);outline:none
63
+ }
64
+ .btn{
65
+ padding:11px 14px;border:none;border-radius:12px;background:linear-gradient(180deg, #2d6cdf, #214fd0);
66
+ color:white;cursor:pointer;white-space:nowrap
67
+ }
68
+ .btn.secondary{background:linear-gradient(180deg, #1e2c4c, #13203a)}
69
+ .tf-buttons{flex-wrap:wrap}
70
+ .tf-btn{
71
+ border:1px solid var(--line);background:var(--chip);color:var(--text);border-radius:999px;
72
+ padding:8px 12px;cursor:pointer;font-size:13px
73
+ }
74
+ .tf-btn.active{background:linear-gradient(180deg, #214fd0, #1f8bd1);border-color:transparent}
75
+ .mini-stat{
76
+ background:var(--card2);border:1px solid var(--line);border-radius:14px;padding:10px 12px;
77
+ display:flex;flex-direction:column;gap:4px
78
+ }
79
+ .mini-stat span{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}
80
+ .mini-stat strong{font-size:14px;font-family:var(--mono);font-weight:600;overflow:hidden;text-overflow:ellipsis}
81
+ .grid{
82
+ display:grid;
83
+ grid-template-columns:repeat(2, minmax(0, 1fr));
84
+ gap:16px;
85
+ }
86
+ .panel{padding:16px;min-width:0}
87
+ .chart-panel,.price-panel,.code-panel{grid-column:1 / -1}
88
+ .panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px}
89
+ .panel-head h2{margin:0;font-size:20px}
90
+ .panel-note{color:var(--muted);font-size:12px;max-width:65ch;text-align:right}
91
+ canvas{
92
+ width:100%;
93
+ display:block;
94
+ background:linear-gradient(180deg, rgba(255,255,255,.02), transparent 30%), #09101e;
95
+ border:1px solid var(--line);
96
+ border-radius:16px
97
+ }
98
+ .signal-strip{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
99
+ .signal-chip{
100
+ padding:9px 12px;border:1px solid var(--line);background:var(--chip);border-radius:999px;
101
+ font-size:13px;font-family:var(--mono)
102
+ }
103
+ .status-grid{
104
+ display:grid;
105
+ grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));
106
+ gap:10px;
107
+ }
108
+ .status-card{
109
+ background:var(--card2);border:1px solid var(--line);border-radius:14px;padding:10px 12px;
110
+ display:flex;flex-direction:column;gap:6px;min-height:74px
111
+ }
112
+ .status-card .k{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}
113
+ .status-card .v{font-family:var(--mono);font-size:14px;word-break:break-word}
114
+ .status-card.good .v{color:var(--good)}
115
+ .status-card.warn .v{color:var(--warn)}
116
+ .status-card.bad .v{color:var(--bad)}
117
+ .json-box{
118
+ margin:0;padding:14px;background:var(--code-bg);border-radius:16px;border:1px solid var(--line);
119
+ overflow:auto;max-height:420px;font-family:var(--mono);font-size:12px;line-height:1.5
120
+ }
121
+ .debug-trace details{
122
+ border:1px solid var(--line);border-radius:14px;background:var(--card2);margin-bottom:10px;overflow:hidden
123
+ }
124
+ .debug-trace summary{
125
+ cursor:pointer;padding:12px 14px;font-family:var(--mono);font-size:13px;background:rgba(255,255,255,.02)
126
+ }
127
+ .debug-trace pre{
128
+ margin:0;padding:12px 14px;overflow:auto;max-height:260px;background:var(--code-bg);font-size:12px
129
+ }
130
+ .timeline{
131
+ display:grid;gap:10px
132
+ }
133
+ .signal-row{
134
+ display:grid;grid-template-columns:150px 1fr 1fr 1fr 120px 130px;gap:8px;align-items:center;
135
+ background:var(--card2);border:1px solid var(--line);border-radius:14px;padding:10px 12px;
136
+ font-family:var(--mono);font-size:12px
137
+ }
138
+ .signal-row.head{background:#10182c;font-weight:700;text-transform:uppercase;font-size:11px;color:var(--muted)}
139
+ .signal-row .active{color:var(--good)}
140
+ .signal-row .stale{color:var(--warn)}
141
+ .signal-row .expired{color:var(--bad)}
142
+ .log-list{
143
+ display:grid;gap:8px;max-height:320px;overflow:auto
144
+ }
145
+ .log-item{
146
+ background:var(--card2);border:1px solid var(--line);border-radius:14px;padding:10px 12px;font-family:var(--mono);
147
+ display:grid;grid-template-columns:1fr auto auto;gap:8px;align-items:center
148
+ }
149
+ .log-item .meta{color:var(--muted);font-size:12px}
150
+ .log-item .reason{color:var(--accent);font-size:12px;grid-column:1 / -1}
151
+ .code-toolbar{
152
+ display:flex;gap:8px;align-items:center;margin-bottom:10px;flex-wrap:wrap
153
+ }
154
+ .code-toolbar input{max-width:320px}
155
+ .code-meta{color:var(--muted);font-size:12px}
156
+ .source-viewer{
157
+ max-height:820px;overflow:auto;border:1px solid var(--line);border-radius:16px;background:var(--code-bg);
158
+ font-family:var(--mono);font-size:12px;line-height:1.55
159
+ }
160
+ .code-line{display:grid;grid-template-columns:72px 1fr;gap:10px;padding:0 12px}
161
+ .code-line:nth-child(odd){background:rgba(255,255,255,.015)}
162
+ .code-line .ln{color:#5d6a8f;user-select:none;text-align:right;padding-right:6px;border-right:1px solid rgba(255,255,255,.04)}
163
+ .code-line .code{white-space:pre-wrap;word-break:break-word}
164
+ .code-line.match{background:rgba(141,213,255,.14)}
165
+ .code-line.hidden{display:none}
166
+ .hl{font-family:var(--mono)}
167
+ @media (max-width: 1200px){
168
+ .toolbar-row,.toolbar-row.compact,.grid{grid-template-columns:1fr}
169
+ .panel-head{flex-direction:column}
170
+ .panel-note{text-align:left}
171
+ .signal-row{grid-template-columns:1fr 1fr}
172
+ }