Spaces:
Paused
Paused
| 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, "&") | |
| .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 = `<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); | |