dashboard: fix gauge charts (install gauge.js, render after polls), force dark theme with white text and red/green accents, add icons
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
+41
-2
@@ -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;
|
||||
|
||||
+10
-14
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Gold Desk — XAU/USD</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%230d6efd'/%3E%3C/svg%3E">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%23f5b301'/%3E%3C/svg%3E">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
|
||||
@@ -12,13 +12,6 @@
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap">
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var t = localStorage.getItem("xau-theme");
|
||||
if (!t && matchMedia("(prefers-color-scheme: dark)").matches) t = "dark";
|
||||
if (t) document.documentElement.setAttribute("data-bs-theme", t);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -27,16 +20,15 @@
|
||||
<header class="card mb-3" id="hero">
|
||||
<div class="card-body d-flex align-items-center flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="xau-symbol">XAU/USD</div>
|
||||
<div class="xau-symbol"><i class="bi bi-coin me-1"></i>XAU/USD</div>
|
||||
<div class="display-5 fw-semibold xau-price" id="hero-price">—</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2" id="hero-rail" aria-label="Signal by timeframe"></div>
|
||||
<div class="ms-auto text-end">
|
||||
<div id="hero-status"><span class="live-dot"></span>…</div>
|
||||
<div class="small text-secondary">M5 close in <span class="num" id="hero-cd">--:--</span></div>
|
||||
<div class="small text-secondary"><i class="bi bi-hourglass-split me-1"></i>M5 close in <span class="num" id="hero-cd">--:--</span></div>
|
||||
<div class="small text-secondary" id="hero-updated">updated —</div>
|
||||
</div>
|
||||
<button id="theme-toggle" class="btn btn-outline-secondary" type="button" aria-label="Toggle dark mode"><i class="bi bi-moon-fill"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -53,18 +45,21 @@
|
||||
</section>
|
||||
|
||||
<h6 class="text-uppercase text-secondary small fw-bold mb-3 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-clock-history text-primary"></i>
|
||||
Timeframes
|
||||
<span class="fw-normal text-body-secondary" style="text-transform:none">— select one for full detail</span>
|
||||
</h6>
|
||||
<div class="row g-3" id="tf-grid"></div>
|
||||
|
||||
<h6 class="text-uppercase text-secondary small fw-bold mb-3 mt-4">Fundamentals</h6>
|
||||
<h6 class="text-uppercase text-secondary small fw-bold mb-3 mt-4 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-globe2 text-primary"></i>Fundamentals
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap">
|
||||
<h5 class="card-title mb-0">Economic calendar</h5>
|
||||
<h5 class="card-title mb-0"><i class="bi bi-calendar-event me-2 text-primary"></i>Economic calendar</h5>
|
||||
<span class="text-secondary small">times local · USD events</span>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox" id="hi-only">
|
||||
@@ -94,7 +89,7 @@
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap">
|
||||
<h5 class="card-title mb-0">Newswire</h5>
|
||||
<h5 class="card-title mb-0"><i class="bi bi-newspaper me-2 text-primary"></i>Newswire</h5>
|
||||
<span class="text-secondary small" id="wire-note"></span>
|
||||
</div>
|
||||
<div id="wire-total" class="d-none"></div>
|
||||
@@ -105,6 +100,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="mt-4 pt-3 border-top text-secondary small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Signals are generated on closed candles only and refresh at candle boundaries
|
||||
(M5/M15/H1/H4). Not financial advice — educational and research use only.
|
||||
</footer>
|
||||
|
||||
+32
-35
@@ -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 `<div class="scorebar">
|
||||
<span class="lbl">${label}</span>
|
||||
<div class="gauge-container" style="height: 160px;" data-label="${label}" data-score="${v}"></div>
|
||||
<span class="val num">${v >= 0 ? "+" : ""}${v.toFixed(1)}</span>
|
||||
<div class="gauge-container" data-label="${label}" data-score="${v}"></div>
|
||||
<span class="val num ${v > 0 ? "text-success" : v < 0 ? "text-danger" : ""}">${v >= 0 ? "+" : ""}${v.toFixed(1)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 `<div class="card border-signal sig-${dir} bg-body-tertiary">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex align-items-baseline gap-2 mb-2">
|
||||
<span class="fw-bold text-signal sig-${dir}">${ts.direction}</span>
|
||||
<span class="fw-bold text-signal sig-${dir}"><i class="bi ${dirIcon} me-1"></i>${ts.direction}</span>
|
||||
<span class="small text-secondary font-monospace">${esc(ts.strategy || "")}</span>
|
||||
<span class="small text-secondary ms-auto font-monospace">R:R ${(+ts.risk_reward).toFixed(1)}</span>
|
||||
</div>
|
||||
<table class="table table-sm table-borderless mb-0">
|
||||
<tr><td class="text-secondary">Entry</td><td class="num">${fmt$(ts.entry)}</td>
|
||||
<td class="text-secondary ps-4">TP1</td><td class="num">${fmt$(ts.take_profit_1)}</td></tr>
|
||||
<tr><td class="text-secondary">Stop</td><td class="num">${fmt$(ts.stop_loss)}</td>
|
||||
<td class="text-secondary ps-4">TP2</td><td class="num">${fmt$(ts.take_profit_2)}</td></tr>
|
||||
<tr><td class="text-secondary">Trail</td><td class="num" colspan="3">${esc(trail)}</td></tr>
|
||||
<tr><td class="text-secondary"><i class="bi bi-box-arrow-in-right me-1"></i>Entry</td><td class="num">${fmt$(ts.entry)}</td>
|
||||
<td class="text-secondary ps-4"><i class="bi bi-flag-fill me-1 text-success"></i>TP1</td><td class="num text-success">${fmt$(ts.take_profit_1)}</td></tr>
|
||||
<tr><td class="text-secondary"><i class="bi bi-shield-x me-1 text-danger"></i>Stop</td><td class="num text-danger">${fmt$(ts.stop_loss)}</td>
|
||||
<td class="text-secondary ps-4"><i class="bi bi-flag-fill me-1 text-success"></i>TP2</td><td class="num text-success">${fmt$(ts.take_profit_2)}</td></tr>
|
||||
<tr><td class="text-secondary"><i class="bi bi-signpost-split me-1"></i>Trail</td><td class="num" colspan="3">${esc(trail)}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -252,7 +230,25 @@ const tile = (k, v, cls = "") =>
|
||||
<span class="v ${cls}">${v}</span>
|
||||
</div></div></div>`;
|
||||
|
||||
const sec = (title, html) => html ? `<section class="mb-4"><h6 class="text-uppercase text-secondary small fw-bold mb-2 d-flex align-items-center gap-2"><span>${esc(title)}</span><span class="flex-fill border-bottom"></span></h6>${html}</section>` : "";
|
||||
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 `<section class="mb-4"><h6 class="text-uppercase text-secondary small fw-bold mb-2 d-flex align-items-center gap-2">${icon ? `<i class="bi ${icon} text-primary"></i>` : ""}<span>${esc(title)}</span><span class="flex-fill border-bottom"></span></h6>${html}</section>`;
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
+47
-36
@@ -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;
|
||||
/* 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);
|
||||
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',
|
||||
} 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);
|
||||
});
|
||||
window.renderGauges = renderGauges;
|
||||
|
||||
var resizeTimer = null;
|
||||
window.addEventListener("resize", function () {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(renderGauges, 150);
|
||||
});
|
||||
})();
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user