/* ForexBot2 dashboard — polls api.php, renders numbers + plain English. No frameworks, no build step. All text lands via textContent. */ "use strict"; const API = "api.php"; const PRICE_POLL_MS = 2000; const REPORT_POLL_MS = 15000; const TF_ORDER = ["5min", "15min", "1h", "4h"]; const TF_LABEL = { "5min": "M5", "15min": "M15", "1h": "H1", "4h": "H4" }; const TF_SECONDS = { "5min": 300, "15min": 900, "1h": 3600, "4h": 14400 }; const SIG_TEXT = { STRONG_BUY: "▲ STRONG BUY", BUY: "▲ BUY", NEUTRAL: "— NEUTRAL", SELL: "▼ SELL", STRONG_SELL: "▼ STRONG SELL", }; const SIG_CLASS = { STRONG_BUY: "sig-buy", BUY: "sig-buy", NEUTRAL: "sig-neutral", SELL: "sig-sell", STRONG_SELL: "sig-sell", }; const IND_LABEL = { ema_trend: "EMA trend", rsi: "RSI", macd: "MACD", bollinger: "Bollinger", adx: "ADX", roc: "Momentum (ROC)", keltner: "Keltner", divergence: "Divergence", candlestick: "Candlestick", atr: "Volatility (ATR)", }; const IND_ORDER = ["ema_trend", "rsi", "macd", "roc", "keltner", "bollinger", "adx", "divergence", "candlestick", "atr"]; const state = { price: null, reports: {}, activeTf: "5min", lastPrice: null }; const $ = (id) => document.getElementById(id); const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; }; // ── polling ────────────────────────────────────────────────────────── async function fetchJson(url) { const r = await fetch(url, { cache: "no-store" }); if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); } async function pollPrice() { try { const d = await fetchJson(API + "?what=price"); if (d.ok) { state.price = d.price; hideError(); renderPrice(); } } catch (e) { showError("Price feed unreachable — retrying…"); } } async function pollReports() { try { const d = await fetchJson(API); if (d.ok) { state.price = d.price; state.reports = d.reports || {}; if (!state.reports[state.activeTf]) { const first = TF_ORDER.find(tf => state.reports[tf]); if (first) state.activeTf = first; } hideError(); renderPrice(); renderStrip(); renderDetail(); } } catch (e) { showError("Server unreachable — retrying…"); } } function showError(msg) { const b = $("error-banner"); b.textContent = msg; b.classList.remove("hidden"); } function hideError() { $("error-banner").classList.add("hidden"); } // ── rendering ──────────────────────────────────────────────────────── function renderPrice() { const p = state.price; const hero = $("hero-price"), dot = $("feed-dot"), status = $("feed-status"); if (!p) { hero.textContent = "—"; status.textContent = "no price data yet"; dot.className = "dot"; return; } const v = Number(p.price); hero.textContent = v.toFixed(2); hero.classList.remove("up", "down"); if (state.lastPrice !== null && v !== state.lastPrice) { hero.classList.add(v > state.lastPrice ? "up" : "down"); } state.lastPrice = v; const age = Number(p.age_sec); if (age <= 60) { dot.className = "dot live"; status.textContent = "live · tick " + age + "s ago"; } else { dot.className = "dot stale"; status.textContent = "feed stale — last tick " + fmtAge(age) + " ago (market closed or bot offline)"; } } function renderStrip() { const strip = $("tf-strip"); strip.textContent = ""; for (const tf of TF_ORDER) { const rep = state.reports[tf]; const tile = el("button", "tf-tile"); tile.setAttribute("aria-selected", String(tf === state.activeTf)); tile.addEventListener("click", () => { state.activeTf = tf; renderStrip(); renderDetail(); }); tile.appendChild(el("div", "tf-name", TF_LABEL[tf] || tf)); if (rep) { const sig = el("div", "tf-sig", SIG_TEXT[rep.signal] || rep.signal); const cls = SIG_CLASS[rep.signal]; if (cls === "sig-buy") sig.style.color = "var(--good)"; if (cls === "sig-sell") sig.style.color = "var(--critical)"; tile.appendChild(sig); tile.appendChild(el("div", "tf-sub", "grade " + (rep.quality || "F") + " · score " + fmtNum(rep.score, 0))); } else { tile.appendChild(el("div", "tf-sig", "…")); tile.appendChild(el("div", "tf-sub", "no data yet")); } strip.appendChild(tile); } } function renderDetail() { const root = $("detail"); root.textContent = ""; const tf = state.activeTf; const rep = state.reports[tf]; if (!rep) { root.appendChild(el("div", "empty", "No " + (TF_LABEL[tf] || tf) + " report yet — it appears after the next candle close.")); return; } // verdict card const verdict = el("section", "card"); const row = el("div", "verdict-row"); const sigChip = el("span", "chip " + (SIG_CLASS[rep.signal] || ""), SIG_TEXT[rep.signal] || rep.signal); row.appendChild(sigChip); row.appendChild(el("span", "chip grade", "grade " + (rep.quality || "F"))); row.appendChild(el("span", "chip", "score " + fmtNum(rep.score, 0) + " · conf " + fmtNum(rep.confidence, 0))); verdict.appendChild(row); verdict.appendChild(el("div", "summary", rep.summary || "")); if (rep.regime_plain) verdict.appendChild(el("div", "meta-line", rep.regime_plain)); if (rep.mtf_plain) verdict.appendChild(el("div", "meta-line", rep.mtf_plain)); const age = Number(rep.age_sec || 0); const staleTxt = age > 2 * (TF_SECONDS[tf] || 3600) ? " · ⚠ this report is stale" : ""; verdict.appendChild(el("div", "bar-time", "Signal bar: " + (rep.bar_ts || "?") + " UTC · updated " + fmtAge(age) + " ago" + staleTxt)); root.appendChild(verdict); // trade setup card if (rep.trade_setup) { const ts = rep.trade_setup; const card = el("section", "card"); card.appendChild(el("h2", null, "Trade setup (" + ts.direction + " · " + (ts.strategy || "") + ")")); const grid = el("div", "setup-grid"); const stats = [ ["Entry", fmtNum(ts.entry, 2)], ["Stop loss", fmtNum(ts.stop_loss, 2)], ["Target 1", fmtNum(ts.take_profit_1, 2)], ["Target 2", fmtNum(ts.take_profit_2, 2)], ["Risk (per oz)", "$" + fmtNum(ts.risk_usd, 2)], ["Reward : risk", fmtNum(ts.risk_reward, 2)], ]; for (const [label, value] of stats) { const s = el("div", "stat"); s.appendChild(el("div", "label", label)); s.appendChild(el("div", "value", value)); grid.appendChild(s); } card.appendChild(grid); root.appendChild(card); } // warnings card const warns = rep.warnings_plain || []; if (warns.length) { const card = el("section", "card"); card.appendChild(el("h2", null, "Things to know")); const ul = el("ul", "warn-list"); warns.forEach((w, i) => { const li = el("li"); li.appendChild(el("span", "w-ico", "⚠")); li.appendChild(document.createTextNode(w)); if (rep.warnings && rep.warnings[i]) li.title = rep.warnings[i]; ul.appendChild(li); }); card.appendChild(ul); root.appendChild(card); } // indicators table const inds = rep.indicators || {}; const keys = IND_ORDER.filter(k => inds[k]).concat( Object.keys(inds).filter(k => !IND_ORDER.includes(k))); if (keys.length) { const card = el("section", "card"); card.appendChild(el("h2", null, "What each indicator says")); const tbl = el("table", "ind"); const thead = el("thead"); const hr = el("tr"); for (const h of ["Indicator", "Value", "Reading", "What it means"]) { hr.appendChild(el("th", null, h)); } thead.appendChild(hr); tbl.appendChild(thead); const tbody = el("tbody"); for (const k of keys) { const ind = inds[k]; const tr = el("tr"); tr.appendChild(el("td", "ind-name", IND_LABEL[k] || k)); tr.appendChild(el("td", "num", fmtIndValue(k, ind.value))); const rd = el("td", "num"); const chip = el("span", "chip " + (SIG_CLASS[ind.signal] || ""), SIG_TEXT[ind.signal] || ind.signal); chip.style.fontSize = "12px"; rd.appendChild(chip); tr.appendChild(rd); const what = el("td", "ind-plain"); what.appendChild(el("div", null, ind.plain || "")); if (ind.description) what.appendChild(el("div", "ind-desc", ind.description)); tr.appendChild(what); tbody.appendChild(tr); } tbl.appendChild(tbody); card.appendChild(tbl); root.appendChild(card); } } // ── formatting ─────────────────────────────────────────────────────── function fmtNum(v, dp) { const n = Number(v); return Number.isFinite(n) ? n.toFixed(dp) : "—"; } function fmtIndValue(name, v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; switch (name) { case "rsi": case "adx": return n.toFixed(1); case "roc": return n.toFixed(2) + "%"; case "bollinger": return (n * 100).toFixed(0) + "%"; case "atr": return "$" + n.toFixed(2); case "divergence": return String(Math.round(n)); case "ema_trend": return n.toFixed(2); default: return n.toFixed(2); } } function fmtAge(sec) { if (sec < 90) return sec + "s"; if (sec < 5400) return Math.round(sec / 60) + "m"; return (sec / 3600).toFixed(1) + "h"; } // ── boot ───────────────────────────────────────────────────────────── let priceTimer = null, reportTimer = null; function startPolling() { stopPolling(); pollReports(); priceTimer = setInterval(pollPrice, PRICE_POLL_MS); reportTimer = setInterval(pollReports, REPORT_POLL_MS); } function stopPolling() { if (priceTimer) clearInterval(priceTimer); if (reportTimer) clearInterval(reportTimer); priceTimer = reportTimer = null; } document.addEventListener("visibilitychange", () => { if (document.hidden) stopPolling(); else startPolling(); }); renderStrip(); startPolling();