Spaces:
Paused
Paused
Upload 3 files
Browse files- frontend/app.js +548 -0
- frontend/index.html +144 -0
- 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, "&")
|
| 361 |
+
.replace(/</g, "<")
|
| 362 |
+
.replace(/>/g, ">");
|
| 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 |
+
}
|