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 = `
${label}
${value}
`; 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 = `${name}
${escapeHTML(JSON.stringify(value, null, 2))}
`; wrap.appendChild(d); }); } function escapeHTML(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">"); } 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 = `
ID / Dir
Generated
Expires
Countdown / Age
State
Status
`; 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 `
#${s.signal_id} ${s.direction}
${formatUTC(s.generated_at)}
${formatUTC(s.expires_at)}
${countdown}
${age}
${s.lifecycle_state}
${status}
`; }).join(""); wrap.innerHTML = header + (body || `
idle
inactive
`); $("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) => `
${formatUTC(row.ts)}
${formatNumber(row.price)}
${row.market_state || ""}
dir ${row.direction || "—"} | conf ${row.confidence !== undefined ? Number(row.confidence).toFixed(4) : "—"} | ${row.execution_suitability || "—"} | ${row.lifecycle_state || "—"}
${escapeHTML(row.reason_summary || "")}
`).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 => `
${formatUTC(t.ts)}
${formatNumber(t.price)}
${t.source || "tick"}
bid ${formatNumber(t.bid)} | ask ${formatNumber(t.ask)} | spread ${formatNumber(t.spread, 8)} | ${escapeHTML(t.symbol || "")}
`).join(""); list.innerHTML = rows || `
Waiting for ticks…
`; } 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);