diff --git a/dashboard_publisher.py b/dashboard_publisher.py index f31adec..4f9c239 100644 --- a/dashboard_publisher.py +++ b/dashboard_publisher.py @@ -82,7 +82,8 @@ SUMMARY_PHP_FILE = "news_summary.php" # Repo files copied into the out dir at daemon start (git pull + restart # updates them). The PHP endpoint reads news.json in place, so it lives # next to the page. -STATIC_FILES = [PAGE_FILE, "dashboard.css", "dashboard.js", SUMMARY_PHP_FILE] +STATIC_FILES = [PAGE_FILE, "dashboard.css", "dashboard.js", "gauge.js", + SUMMARY_PHP_FILE] # ══════════════════════════════════════════════════════════════════ diff --git a/static/dashboard.css b/static/dashboard.css index 81995be..d1694c6 100644 --- a/static/dashboard.css +++ b/static/dashboard.css @@ -8,12 +8,43 @@ :root { --xau-font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; --xau-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Consolas, Menlo, monospace; + --xau-gold: #f5b301; +} + +/* Forced dark theme (owner decision 2026-08-16): dark background, pure + * white text (no gray), vivid red/green for data accents. Loaded after + * Bootstrap so these overrides win. */ +[data-bs-theme="dark"] { + --bs-body-bg: #0a0e14; + --bs-secondary-bg: #111823; + --bs-tertiary-bg: #0e141d; + --bs-body-color: #ffffff; + --bs-emphasis-color: #ffffff; + --bs-heading-color: #ffffff; + --bs-secondary-color: rgba(255, 255, 255, 0.92); + --bs-tertiary-color: rgba(255, 255, 255, 0.8); + --bs-border-color: rgba(255, 255, 255, 0.14); + --bs-card-bg: #0e141d; + --bs-card-border-color: rgba(255, 255, 255, 0.12); + --bs-modal-bg: #0e141d; + --bs-success: #22c55e; + --bs-success-rgb: 34, 197, 94; + --bs-success-text-emphasis: #4ade80; + --bs-success-bg-subtle: rgba(34, 197, 94, 0.16); + --bs-success-border-subtle: rgba(34, 197, 94, 0.45); + --bs-danger: #ef4444; + --bs-danger-rgb: 239, 68, 68; + --bs-danger-text-emphasis: #f87171; + --bs-danger-bg-subtle: rgba(239, 68, 68, 0.16); + --bs-danger-border-subtle: rgba(239, 68, 68, 0.45); } html, body { font-family: var(--xau-font-sans); letter-spacing: -0.005em; -webkit-font-smoothing: antialiased; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); } .num, .xau-price, .xau-symbol { @@ -25,12 +56,12 @@ html, body { font-size: 0.95rem; font-weight: 700; letter-spacing: 0.08em; - color: var(--bs-primary); + color: var(--xau-gold); } .xau-price { line-height: 1; - color: var(--bs-primary-text-emphasis); + color: #ffffff; } .live-dot { @@ -100,6 +131,14 @@ html, body { overflow: hidden; } +/* Chart.js gauge canvas host (see gauge.js) — thin bar in the row */ +.scorebar .gauge-container { + position: relative; + flex: 1; + min-width: 60px; + height: 22px; +} + .scorebar .track::after { content: ""; position: absolute; diff --git a/static/dashboard.html b/static/dashboard.html index 5ca29e1..94faeb3 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -4,7 +4,7 @@ Gold Desk — XAU/USD - + @@ -12,13 +12,6 @@ - @@ -27,16 +20,15 @@
-
XAU/USD
+
XAU/USD
—
…
-
M5 close in --:--
+
M5 close in --:--
updated —
-
@@ -53,18 +45,21 @@
+ Timeframes — select one for full detail
-
Fundamentals
+
+ Fundamentals +
-
Economic calendar
+
Economic calendar
times local · USD events
@@ -94,7 +89,7 @@
-
Newswire
+
Newswire
@@ -105,6 +100,7 @@
+ Signals are generated on closed candles only and refresh at candle boundaries (M5/M15/H1/H4). Not financial advice — educational and research use only.
diff --git a/static/dashboard.js b/static/dashboard.js index 3666d24..03f96c5 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -80,32 +80,9 @@ const badgeCls = s => s === "buy" ? "bg-success-subtle text-success-emphasis bor : "bg-secondary-subtle text-secondary-emphasis border-secondary-subtle"; /* ── theme ────────────────────────────────────────────────────── */ -(function () { - const KEY = "xau-theme"; - const sysDark = matchMedia("(prefers-color-scheme: dark)"); - const stored = () => localStorage.getItem(KEY); - const apply = theme => { - document.documentElement.setAttribute("data-bs-theme", theme); - const btn = $("theme-toggle"); - if (!btn) return; - const icon = btn.querySelector("i"); - if (theme === "dark") { - btn.title = "Switch to light mode"; - if (icon) icon.className = "bi bi-sun-fill"; - } else { - btn.title = "Switch to dark mode"; - if (icon) icon.className = "bi bi-moon-fill"; - } - }; - apply(stored() || (sysDark.matches ? "dark" : "light")); - sysDark.addEventListener("change", e => { if (!stored()) apply(e.matches ? "dark" : "light"); }); - const btn = $("theme-toggle"); - if (btn) btn.addEventListener("click", () => { - const next = document.documentElement.getAttribute("data-bs-theme") === "dark" ? "light" : "dark"; - localStorage.setItem(KEY, next); - apply(next); - }); -})(); +// Dark theme is forced site-wide (owner decision 2026-08-16): dark +// background, pure-white text, red/green data accents. +document.documentElement.setAttribute("data-bs-theme", "dark"); /* ── hero ─────────────────────────────────────────────────────── */ function renderHero() { @@ -176,29 +153,30 @@ function scoreBar(label, score) { const v = Math.max(-100, Math.min(100, score ?? 0)); return `
${label} -
- ${v >= 0 ? "+" : ""}${v.toFixed(1)} +
+ ${v >= 0 ? "+" : ""}${v.toFixed(1)}
`; } function setupCard(ts) { if (!ts) return ""; const dir = ts.direction === "BUY" ? "buy" : "sell"; + const dirIcon = ts.direction === "BUY" ? "bi-arrow-up-circle-fill" : "bi-arrow-down-circle-fill"; const trail = (ts.trail_arm_r != null && ts.trail_dist_r != null) ? `arm ${ts.trail_arm_r}R · dist ${ts.trail_dist_r}R` : "platform default"; return `
- ${ts.direction} + ${ts.direction} ${esc(ts.strategy || "")} R:R ${(+ts.risk_reward).toFixed(1)}
- - - - - + + + + +
Entry${fmt$(ts.entry)}TP1${fmt$(ts.take_profit_1)}
Stop${fmt$(ts.stop_loss)}TP2${fmt$(ts.take_profit_2)}
Trail${esc(trail)}
Entry${fmt$(ts.entry)}TP1${fmt$(ts.take_profit_1)}
Stop${fmt$(ts.stop_loss)}TP2${fmt$(ts.take_profit_2)}
Trail${esc(trail)}
`; @@ -252,7 +230,25 @@ const tile = (k, v, cls = "") => ${v}
`; -const sec = (title, html) => html ? `
${esc(title)}
${html}
` : ""; +const SEC_ICON = { + "At a glance": "bi-speedometer2", + "Trade setup": "bi-bullseye", + "Why this signal": "bi-lightbulb", + "Multi-timeframe confirmation": "bi-layers", + "News context": "bi-newspaper", + "AI panel (advisory)": "bi-robot", + "Scan detail": "bi-clock-history", + "Score": "bi-graph-up", + "From the source": "bi-quote", + "AI gold-impact summary": "bi-stars", + "Timing": "bi-alarm", + "Numbers": "bi-calculator" +}; +const sec = (title, html) => { + if (!html) return ""; + const icon = SEC_ICON[title]; + return `
${icon ? `` : ""}${esc(title)}
${html}
`; +}; function indTable(src) { const rows = IND_ORDER.map(name => { @@ -748,6 +744,7 @@ async function poll() { if (snapChanged) { renderHero(); renderCards(); + if (window.renderGauges) window.renderGauges(); renderDetail(); } if (newsChanged) { diff --git a/static/gauge.js b/static/gauge.js index 42488d0..7ee3813 100644 --- a/static/gauge.js +++ b/static/gauge.js @@ -1,44 +1,50 @@ -document.addEventListener('DOMContentLoaded', function() { - // Draw horizontal bar charts for each score using Chart.js - function drawCharts() { - const gaugeContainers = document.querySelectorAll('.gauge-container'); - gaugeContainers.forEach(container => { - const label = container.dataset.label; - const score = parseFloat(container.dataset.score); - // Clear any previous content - container.innerHTML = ''; - // Create canvas element - const canvas = document.createElement('canvas'); - // Set a fixed height for thin bar - canvas.height = 30; - container.appendChild(canvas); - const ctx = canvas.getContext('2d'); - // Determine bar colour based on sign - const bgColor = score >= 0 ? 'rgba(40,167,69,0.8)' : 'rgba(220,53,69,0.8)'; // success/red - new Chart(ctx, { - type: 'bar', +/* Gold Desk — score gauges. + * + * Draws a horizontal diverging Chart.js bar (−100…+100, zero-centered) + * into every `.gauge-container` element. The containers are created + * dynamically by dashboard.js after each snapshot poll, so this module + * exposes window.renderGauges() which dashboard.js calls after every + * card render (and on resize). Re-running it destroys and redraws each + * chart, so it is safe to call repeatedly. + */ +(function () { + "use strict"; + + function renderGauges() { + if (typeof Chart === "undefined") return; + document.querySelectorAll(".gauge-container").forEach(function (container) { + var score = parseFloat(container.dataset.score); + if (!isFinite(score)) return; + + var canvas = container.querySelector("canvas"); + if (!canvas) { + container.innerHTML = ""; + canvas = document.createElement("canvas"); + container.appendChild(canvas); + } else { + var existing = Chart.getChart(canvas); + if (existing) existing.destroy(); + } + + var pos = score >= 0; + new Chart(canvas.getContext("2d"), { + type: "bar", data: { - labels: [label], + labels: [container.dataset.label || ""], datasets: [{ - label: label, data: [score], - backgroundColor: bgColor, - borderColor: 'rgba(0,0,0,0.1)', + backgroundColor: pos ? "rgba(25,135,84,0.85)" : "rgba(220,53,69,0.85)", + borderColor: pos ? "#198754" : "#dc3545", borderWidth: 1, - barThickness: 12, - maxBarThickness: 12, + barThickness: 10, + maxBarThickness: 10, + borderRadius: 3 }] }, options: { - indexAxis: 'y', + indexAxis: "y", scales: { - x: { - min: -100, - max: 100, - display: false, - grid: { display: false }, - ticks: { display: false } - }, + x: { min: -100, max: 100, display: false, grid: { display: false }, ticks: { display: false } }, y: { display: false, grid: { display: false } } }, plugins: { @@ -48,12 +54,17 @@ document.addEventListener('DOMContentLoaded', function() { }, maintainAspectRatio: false, responsive: true, - animation: { duration: 500, easing: 'easeOutQuad' } + animation: { duration: 400, easing: "easeOutQuad" } } }); }); } - drawCharts(); - window.addEventListener('resize', drawCharts); -}); \ No newline at end of file + window.renderGauges = renderGauges; + + var resizeTimer = null; + window.addEventListener("resize", function () { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(renderGauges, 150); + }); +})(); diff --git a/test_all.py b/test_all.py index 93f26ae..d7aad57 100644 --- a/test_all.py +++ b/test_all.py @@ -5670,7 +5670,7 @@ def _(): pub = dpub.DashboardPublisher(out_dir=tmp) pub.install_static() assert sorted(os.listdir(tmp)) == \ - ["dashboard.css", "dashboard.html", "dashboard.js", "news_summary.php"] # all installed, no .tmp + ["dashboard.css", "dashboard.html", "dashboard.js", "gauge.js", "news_summary.php"] # all installed, no .tmp @test("dashboard: publish is atomic and leaves no temp file")