MYTHOSLIVE / frontend /app.js
3VVM's picture
Upload 3 files
d21ee4b verified
Raw
History Blame Contribute Delete
21.3 kB
let appState = null;
let sourceRaw = "";
let currentTf = 30;
let selectedSymbol = "frxEURUSD";
const tfList = [30, 60, 120, 180, 300];
const tfLabels = {30: "30s", 60: "1m", 120: "2m", 180: "3m", 300: "5m"};
const $ = (id) => document.getElementById(id);
function formatUTC(ts) {
if (ts === null || ts === undefined || Number.isNaN(Number(ts))) return "—";
const d = new Date(Number(ts) * 1000);
return d.toISOString().replace("T", " ").replace("Z", " UTC");
}
function formatNumber(v, digits = 6) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
if (!Number.isFinite(n)) return "—";
if (Math.abs(n) >= 1000) return n.toFixed(2);
if (Math.abs(n) >= 1) return n.toFixed(4);
return n.toFixed(digits);
}
function setPill(id, text, cls = "") {
const el = $(id);
if (!el) return;
el.textContent = text;
el.className = "status-pill " + (cls || "subtle");
}
function prettyJSON(obj) {
return JSON.stringify(obj, null, 2);
}
function safeObj(v, fallback = {}) {
return (v && typeof v === "object") ? v : fallback;
}
function renderCodeViewer(htmlText) {
const viewer = $("sourceViewer");
if (!viewer) return;
viewer.innerHTML = htmlText;
applyCodeSearch();
}
function applyCodeSearch() {
const query = ($("codeSearch")?.value || "").trim().toLowerCase();
const lines = document.querySelectorAll(".code-line");
let matches = 0;
let first = null;
lines.forEach((line) => {
const text = (line.dataset.text || "").toLowerCase();
const hit = !query || text.includes(query);
line.classList.toggle("hidden", !hit);
line.classList.toggle("match", !!query && hit);
if (hit && query) {
matches += 1;
if (!first) first = line;
}
});
const meta = $("codeMeta");
if (meta) {
meta.textContent = query ? `${matches} matching lines` : `${lines.length} lines loaded`;
}
if (query && first) {
first.scrollIntoView({behavior: "smooth", block: "center"});
}
}
function buildTfButtons() {
const wrap = $("tfButtons");
if (!wrap) return;
wrap.innerHTML = "";
tfList.forEach((tf) => {
const b = document.createElement("button");
b.className = "tf-btn" + (tf === currentTf ? " active" : "");
b.textContent = tfLabels[tf];
b.onclick = async () => {
currentTf = tf;
await fetch(`/api/config/timeframe/${tf}`, {method:"POST"});
buildTfButtons();
refreshState();
};
wrap.appendChild(b);
});
}
async function setSymbol(symbol) {
if (!symbol) return;
await fetch(`/api/config/symbol/${encodeURIComponent(symbol)}`, {method:"POST"});
selectedSymbol = symbol;
refreshState();
}
function populateSymbols(symbols) {
const sel = $("symbolSelect");
if (!sel) return;
sel.innerHTML = "";
(symbols || []).forEach((sym) => {
const opt = document.createElement("option");
opt.value = sym;
opt.textContent = sym;
sel.appendChild(opt);
});
if (symbols && symbols.length) {
sel.value = selectedSymbol;
if (!selectedSymbol || !symbols.includes(selectedSymbol)) {
selectedSymbol = symbols[0];
sel.value = selectedSymbol;
}
$("symbolInput").value = selectedSymbol;
}
}
function drawCandles(canvas, candles) {
if (!canvas) return;
const ctx = canvas.getContext("2d");
const w = canvas.width, h = canvas.height;
ctx.clearRect(0,0,w,h);
// background grid
ctx.strokeStyle = "rgba(120,140,180,0.16)";
ctx.lineWidth = 1;
for (let i = 0; i <= 8; i++) {
const y = Math.round((h - 24) * i / 8) + 12;
ctx.beginPath();
ctx.moveTo(8, y);
ctx.lineTo(w - 8, y);
ctx.stroke();
}
if (!candles || candles.length === 0) {
ctx.fillStyle = "#91a0be";
ctx.font = "18px monospace";
ctx.fillText("Waiting for candles…", 24, 38);
return;
}
const visible = candles.slice(-80);
const highs = visible.map(c => Number(c.high));
const lows = visible.map(c => Number(c.low));
const max = Math.max(...highs);
const min = Math.min(...lows);
const pad = (max - min) * 0.12 || (Math.abs(max) * 0.01) || 1;
const hi = max + pad;
const lo = min - pad;
const plotW = w - 36;
const plotH = h - 36;
const x0 = 18, y0 = 14;
const candleWidth = Math.max(4, Math.floor(plotW / visible.length * 0.68));
const gap = Math.max(2, Math.floor(plotW / visible.length * 0.28));
function yFor(price) {
return y0 + (hi - price) / (hi - lo) * plotH;
}
// price scale text
ctx.fillStyle = "#91a0be";
ctx.font = "12px monospace";
ctx.fillText(hi.toFixed(6), 18, 12);
ctx.fillText(lo.toFixed(6), 18, h - 8);
visible.forEach((c, i) => {
const o = Number(c.open), hgh = Number(c.high), l = Number(c.low), cl = Number(c.close);
const x = x0 + i * (candleWidth + gap);
const up = cl >= o;
const bodyTop = yFor(Math.max(o, cl));
const bodyBottom = yFor(Math.min(o, cl));
const wickTop = yFor(hgh);
const wickBottom = yFor(l);
ctx.strokeStyle = up ? "#58d68d" : "#ff7c7c";
ctx.beginPath();
ctx.moveTo(x + candleWidth/2, wickTop);
ctx.lineTo(x + candleWidth/2, wickBottom);
ctx.stroke();
ctx.fillStyle = up ? "rgba(88,214,141,0.85)" : "rgba(255,124,124,0.85)";
const bodyH = Math.max(1, bodyBottom - bodyTop);
ctx.fillRect(x, bodyTop, candleWidth, bodyH);
});
// chart label
const last = visible[visible.length - 1];
ctx.fillStyle = "#e6edf7";
ctx.font = "14px monospace";
ctx.fillText(`TF ${tfLabels[currentTf]} | O ${formatNumber(last.open)} H ${formatNumber(last.high)} L ${formatNumber(last.low)} C ${formatNumber(last.close)}`, 24, 24);
}
function drawSpark(canvas, ticks) {
if (!canvas) return;
const ctx = canvas.getContext("2d");
const w = canvas.width, h = canvas.height;
ctx.clearRect(0,0,w,h);
ctx.strokeStyle = "rgba(120,140,180,0.16)";
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = Math.round((h - 24) * i / 4) + 12;
ctx.beginPath();
ctx.moveTo(8, y);
ctx.lineTo(w - 8, y);
ctx.stroke();
}
if (!ticks || ticks.length < 2) {
ctx.fillStyle = "#91a0be";
ctx.font = "16px monospace";
ctx.fillText("Waiting for ticks…", 24, 32);
return;
}
const visible = ticks.slice(-120);
const prices = visible.map(t => Number(t.price));
const max = Math.max(...prices);
const min = Math.min(...prices);
const pad = (max - min) * 0.15 || (Math.abs(max) * 0.01) || 1;
const hi = max + pad;
const lo = min - pad;
const x0 = 18, y0 = 14;
const plotW = w - 36, plotH = h - 36;
function yFor(price) {
return y0 + (hi - price) / (hi - lo) * plotH;
}
ctx.strokeStyle = "#8dd5ff";
ctx.beginPath();
visible.forEach((t, i) => {
const x = x0 + (i / Math.max(1, visible.length - 1)) * plotW;
const y = yFor(Number(t.price));
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
const last = visible[visible.length - 1];
ctx.fillStyle = "#e6edf7";
ctx.font = "14px monospace";
ctx.fillText(`Last ${formatNumber(last.price)} | Bid ${formatNumber(last.bid)} | Ask ${formatNumber(last.ask)} | Spread ${formatNumber(last.spread, 8)}`, 24, 24);
}
function cardClass(value, goodWhen = null, badWhen = null) {
const s = String(value ?? "").toLowerCase();
if (badWhen && badWhen.includes(s)) return "bad";
if (goodWhen && goodWhen.includes(s)) return "good";
if (["blocked", "invalidated", "expired", "error", "cold_start"].includes(s)) return "bad";
if (["confirmed", "normal", "strong", "warm"].includes(s)) return "good";
if (["degraded", "cautious", "candidate", "forming", "cooling", "demo"].includes(s)) return "warn";
return "";
}
function makeCard(label, value, cls = "") {
const div = document.createElement("div");
div.className = `status-card ${cls}`.trim();
div.innerHTML = `<div class="k">${label}</div><div class="v">${value}</div>`;
return div;
}
function renderStatusGrid(snapshot) {
const grid = $("statusGrid");
if (!grid) return;
const engine = safeObj(snapshot.engine);
const out = safeObj(engine.raw_output);
const conn = safeObj(snapshot.connection);
const tl = safeObj(snapshot.timeline);
const current = safeObj(tl.current);
const clk = safeObj(snapshot.clock);
const items = [
["direction", out.direction ?? "—", cardClass(out.direction, ["buy","sell"])],
["confidence", out.confidence !== undefined ? Number(out.confidence).toFixed(4) : "—", ""],
["execution_suitability", out.execution_suitability ?? "—", cardClass(out.execution_suitability)],
["market_state", out.market_state ?? "—", cardClass(out.market_state)],
["regime_label", out.regime_label ?? "—", cardClass(out.regime_label)],
["asset_mode", out.asset_mode ?? "—", cardClass(out.asset_mode)],
["timeframe_alignment", out.timeframe_alignment !== undefined ? Number(out.timeframe_alignment).toFixed(4) : "—", ""],
["manipulation_probability", out.manipulation_probability !== undefined ? Number(out.manipulation_probability).toFixed(4) : "—", ""],
["liquidity_score", out.liquidity_score !== undefined ? Number(out.liquidity_score).toFixed(4) : "—", ""],
["volatility_score", out.volatility_score !== undefined ? Number(out.volatility_score).toFixed(4) : "—", ""],
["pressure_score", out.pressure_score !== undefined ? Number(out.pressure_score).toFixed(4) : "—", ""],
["momentum_score", out.momentum_score !== undefined ? Number(out.momentum_score).toFixed(4) : "—", ""],
["timing_score", out.timing_score !== undefined ? Number(out.timing_score).toFixed(4) : "—", ""],
["signal_freshness", out.signal_freshness !== undefined ? Number(out.signal_freshness).toFixed(4) : "—", ""],
["spread_health", out.spread_health !== undefined ? Number(out.spread_health).toFixed(4) : "—", ""],
["data_quality", out.data_quality !== undefined ? Number(out.data_quality).toFixed(4) : "—", ""],
["readability_score", out.readability_score !== undefined ? Number(out.readability_score).toFixed(4) : "—", ""],
["blocked_flag", String(out.blocked_flag ?? false), cardClass(out.blocked_flag ? "blocked" : "ok")],
["degraded_mode_flag", String(out.degraded_mode_flag ?? false), cardClass(out.degraded_mode_flag ? "degraded" : "ok")],
["stale_signal_flag", String(out.stale_signal_flag ?? false), cardClass(out.stale_signal_flag ? "expired" : "ok")],
["reason_summary", out.reason_summary ?? "—", ""],
["signal_generated_at", current.generated_at ? formatUTC(current.generated_at) : "—", ""],
["signal_expires_at", current.expires_at ? formatUTC(current.expires_at) : "—", ""],
["signal_countdown", current.countdown !== undefined && current.countdown !== null ? `${Number(current.countdown).toFixed(1)}s` : "—", ""],
["signal_age", current.age !== undefined ? `${Number(current.age).toFixed(1)}s` : "—", ""],
["lifecycle_state", current.lifecycle_state ?? out.lifecycle_state ?? "—", cardClass(current.lifecycle_state ?? out.lifecycle_state ?? "")],
["warm_up_fraction", out.warm_up_fraction !== undefined ? Number(out.warm_up_fraction).toFixed(4) : "—", ""],
["operational_mode", out.operational_mode ?? conn.status ?? "—", cardClass(out.operational_mode ?? conn.status ?? "")],
["base_timeframe", snapshot.base_ref?.timeframe_label ?? "—", ""],
["selected_display_tf", conn.selected_display_tf_label ?? "—", ""],
["tick_count", engine.tick_count ?? 0, ""],
["is_warm", String(engine.is_warm ?? false), cardClass(engine.is_warm ? "warm" : "cold_start")],
["live_mode", String(conn.live_mode ?? false), cardClass(conn.live_mode ? "confirmed" : "demo")],
["demo_mode", String(conn.demo_mode ?? false), cardClass(conn.demo_mode ? "demo" : "ok")],
["ws_status", conn.status ?? "—", cardClass(conn.status)],
["last_error", conn.last_error ?? "—", ""],
["server_time", clk.server_time ? formatUTC(clk.server_time) : "—", ""],
];
grid.innerHTML = "";
items.forEach(([label, value, cls]) => grid.appendChild(makeCard(label, value, cls)));
}
function renderRawOutput(snapshot) {
const pre = $("rawOutput");
if (!pre) return;
const out = safeObj(snapshot.engine).raw_output || {};
pre.textContent = prettyJSON(out);
}
function renderHealth(snapshot) {
const pre = $("healthOutput");
if (!pre) return;
const health = safeObj(snapshot.engine).health || {};
pre.textContent = prettyJSON(health);
}
function renderDebug(snapshot) {
const wrap = $("debugTrace");
if (!wrap) return;
const dbg = safeObj(snapshot.engine).debug_trace || {};
const sections = [
["candle", dbg.candle],
["data_integrity", dbg.data_integrity],
["market_state", dbg.market_state],
["asset_profile", dbg.asset_profile],
["session", dbg.session],
["timeframe_fusion", dbg.timeframe_fusion],
["events", dbg.events],
["liquidity_pressure", dbg.liquidity_pressure],
["technical_stack", dbg.technical_stack],
["score", dbg.score],
["lifecycle", dbg.lifecycle],
["warmup", dbg.warmup],
];
wrap.innerHTML = "";
sections.forEach(([name, value]) => {
const d = document.createElement("details");
d.open = false;
const summaryValue = value && typeof value === "object" ? JSON.stringify(Object.keys(value).slice(0, 5)) : String(value);
d.innerHTML = `<summary>${name}</summary><pre>${escapeHTML(JSON.stringify(value, null, 2))}</pre>`;
wrap.appendChild(d);
});
}
function escapeHTML(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function renderTimeline(snapshot) {
const wrap = $("signalTimeline");
if (!wrap) return;
const tl = safeObj(snapshot.timeline);
const rows = Array.isArray(tl.active_signals) ? tl.active_signals : [];
const current = tl.current;
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>`;
const body = rows.map((s) => {
const countdown = (s.countdown === null || s.countdown === undefined) ? "—" : `${Number(s.countdown).toFixed(1)}s`;
const age = (s.age === null || s.age === undefined) ? "—" : `${Number(s.age).toFixed(1)}s`;
const status = s.invalidated ? "invalidated" : (s.stale ? "stale" : (s.active ? "active" : "closed"));
const statusClass = s.invalidated ? "expired" : (s.stale ? "stale" : (s.active ? "active" : ""));
return `
<div class="signal-row">
<div>#${s.signal_id} ${s.direction}</div>
<div>${formatUTC(s.generated_at)}</div>
<div>${formatUTC(s.expires_at)}</div>
<div>${countdown}<br><span class="meta">${age}</span></div>
<div>${s.lifecycle_state}</div>
<div class="${statusClass}">${status}</div>
</div>
`;
}).join("");
wrap.innerHTML = header + (body || `<div class="signal-row"><div>—</div><div>—</div><div>—</div><div>—</div><div>idle</div><div>inactive</div></div>`);
$("signalDirection").textContent = current?.direction ? `Direction: ${current.direction}` : "Direction: —";
$("signalLifecycle").textContent = current?.lifecycle_state ? `Lifecycle: ${current.lifecycle_state}` : "Lifecycle: —";
$("signalExpiryBucket").textContent = current?.expiry_bucket ? `Expiry bucket: ${current.expiry_bucket}` : "Expiry bucket: —";
$("signalFreshness").textContent = current?.confidence !== undefined ? `Confidence: ${Number(current.confidence).toFixed(4)}` : "Confidence: —";
$("signalState").textContent = current?.stale ? "Stale signal" : (current?.active ? "Active signal" : "Inactive");
}
function renderLogs(snapshot) {
const wrap = $("logList");
if (!wrap) return;
const logs = Array.isArray(snapshot.logs) ? snapshot.logs.slice(-24).reverse() : [];
wrap.innerHTML = logs.map((row) => `
<div class="log-item">
<div>${formatUTC(row.ts)}</div>
<div>${formatNumber(row.price)}</div>
<div>${row.market_state || ""}</div>
<div class="meta">dir ${row.direction || "—"} | conf ${row.confidence !== undefined ? Number(row.confidence).toFixed(4) : "—"} | ${row.execution_suitability || "—"} | ${row.lifecycle_state || "—"}</div>
<div class="reason">${escapeHTML(row.reason_summary || "")}</div>
</div>
`).join("");
}
function renderPrices(snapshot) {
const ticks = safeObj(snapshot.charts).price_stream || [];
const canvas = $("priceSpark");
drawSpark(canvas, ticks);
const list = $("tickList");
if (!list) return;
const rows = ticks.slice(-18).reverse().map(t => `
<div class="log-item">
<div>${formatUTC(t.ts)}</div>
<div>${formatNumber(t.price)}</div>
<div>${t.source || "tick"}</div>
<div class="meta">bid ${formatNumber(t.bid)} | ask ${formatNumber(t.ask)} | spread ${formatNumber(t.spread, 8)} | ${escapeHTML(t.symbol || "")}</div>
</div>
`).join("");
list.innerHTML = rows || `<div class="log-item"><div>Waiting for ticks…</div><div>—</div><div>—</div></div>`;
}
function renderChart(snapshot) {
const charts = safeObj(snapshot.charts);
const tf = String(charts.selected_timeframe || currentTf);
currentTf = Number(tf);
buildTfButtons();
const candles = safeObj(charts.timeframes)[String(currentTf)] || [];
drawCandles($("candleChart"), candles);
$("chartNote").textContent = `Selected: ${tfLabels[currentTf] || currentTf + "s"} | ${candles.length} candles | same live tick stream`;
}
function renderClock(snapshot) {
const clk = safeObj(snapshot.clock);
const conn = safeObj(snapshot.connection);
$("utcClock").textContent = formatUTC(clk.utc_now);
$("latestTick").textContent = clk.latest_tick_ts ? `${formatUTC(clk.latest_tick_ts)} (${Number(clk.latest_tick_age_sec || 0).toFixed(1)}s ago)` : "—";
$("baseRef").textContent = `${snapshot.base_ref?.timeframe_label || "30s"} | ${conn.selected_symbol || "—"} | ${conn.market_type || "unknown"}`;
const current = safeObj(snapshot.timeline).current;
$("signalAge").textContent = current?.age !== undefined && current?.age !== null ? `${Number(current.age).toFixed(1)}s` : "—";
$("signalCountdown").textContent = current?.countdown !== undefined && current?.countdown !== null ? `${Number(current.countdown).toFixed(1)}s` : "—";
}
function renderPills(snapshot) {
const conn = safeObj(snapshot.connection);
const engine = safeObj(snapshot.engine);
const out = safeObj(engine.raw_output);
setPill("connStatus", conn.status || "idle", cardClass(conn.status));
setPill("modeStatus", conn.demo_mode ? "demo mode" : (conn.live_mode ? "live mode" : (conn.status || "idle")), cardClass(conn.demo_mode ? "demo" : conn.status));
setPill("warmStatus", engine.is_warm ? "warm" : "cold start", engine.is_warm ? "good" : "warn");
}
function updateAll(snapshot) {
appState = snapshot;
selectedSymbol = safeObj(snapshot.connection).selected_symbol || selectedSymbol;
renderPills(snapshot);
renderClock(snapshot);
renderChart(snapshot);
renderPrices(snapshot);
renderStatusGrid(snapshot);
renderRawOutput(snapshot);
renderDebug(snapshot);
renderTimeline(snapshot);
renderLogs(snapshot);
renderHealth(snapshot);
}
async function refreshState() {
try {
const res = await fetch("/api/state", {cache: "no-store"});
const data = await res.json();
updateAll(data);
} catch (err) {
setPill("connStatus", "frontend offline", "bad");
}
}
async function loadSource() {
try {
const [rawRes, htmlRes] = await Promise.all([
fetch("/api/source", {cache: "no-store"}),
fetch("/api/source-html", {cache: "no-store"}),
]);
const raw = await rawRes.json();
sourceRaw = raw.source || "";
$("codeMeta").textContent = `${raw.line_count || 0} lines loaded`;
const htmlText = await htmlRes.text();
renderCodeViewer(htmlText);
} catch (err) {
$("sourceViewer").textContent = "Source unavailable.";
}
}
async function loadSymbols() {
try {
const res = await fetch("/api/symbols", {cache: "no-store"});
const data = await res.json();
populateSymbols(data.symbols || []);
selectedSymbol = data.selected || selectedSymbol;
$("symbolInput").value = selectedSymbol;
if (data.selected) {
$("symbolSelect").value = data.selected;
}
} catch (err) {
populateSymbols(["frxEURUSD","frxGBPUSD","frxUSDJPY","cryBTCUSD","cryETHUSD"]);
}
}
function attachHandlers() {
$("symbolApply").onclick = async () => {
const sel = $("symbolSelect").value;
await setSymbol(sel);
};
$("symbolSet").onclick = async () => {
const sym = $("symbolInput").value.trim();
if (sym) await setSymbol(sym);
};
$("codeSearch").addEventListener("input", applyCodeSearch);
$("codeCopy").onclick = async () => {
try {
await navigator.clipboard.writeText(sourceRaw || "");
$("codeCopy").textContent = "Copied";
setTimeout(() => $("codeCopy").textContent = "Copy", 1200);
} catch (err) {
$("codeCopy").textContent = "Copy failed";
setTimeout(() => $("codeCopy").textContent = "Copy", 1200);
}
};
}
async function boot() {
buildTfButtons();
attachHandlers();
await loadSymbols();
await loadSource();
await refreshState();
setInterval(refreshState, 1000);
}
window.addEventListener("DOMContentLoaded", boot);