xauusd / static /index.html
callernode's picture
Create static/index.html
01022b0 verified
Raw History Blame Contribute Delete
7.35 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>XAUUSD · Live Gold Spot</title>
<style>
:root { --bg:#0b1120; --card:#131c31; --line:#1f2b45; --txt:#e2e8f0; --dim:#8b9ab5; --up:#22c55e; --dn:#ef4444; --gold:#f5c451; }
* { box-sizing:border-box; }
body { margin:0; min-height:100vh; background:var(--bg); color:var(--txt);
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
padding:28px 20px 40px; }
.wrap { max-width:980px; margin:0 auto; }
header { display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin-bottom:22px; }
h1 { font-size:20px; margin:0; letter-spacing:.3px; }
h1 span { color:var(--gold); }
.badge { font-size:11px; padding:4px 10px; border-radius:999px; border:1px solid var(--line);
color:var(--dim); text-transform:uppercase; letter-spacing:.08em; }
.badge.live { color:var(--up); border-color:#14532d; }
.badge.err { color:var(--dn); border-color:#7f1d1d; }
button { margin-left:auto; background:#1c2740; color:var(--txt); border:1px solid var(--line);
border-radius:8px; padding:8px 14px; font-size:13px; cursor:pointer; }
button:hover { background:#243050; }
.price-card { background:linear-gradient(160deg,#16203a,#0f1728); border:1px solid var(--line);
border-radius:16px; padding:22px 24px; }
.label { font-size:11px; text-transform:uppercase; letter-spacing:.1em; color:var(--dim); }
.price { font-size:52px; font-weight:650; line-height:1.1; margin:6px 0 2px; font-variant-numeric:tabular-nums; }
.price small { font-size:20px; color:var(--dim); font-weight:400; margin-left:6px; }
.delta { font-size:14px; font-variant-numeric:tabular-nums; }
.delta.up { color:var(--up); } .delta.dn { color:var(--dn); } .delta.flat { color:var(--dim); }
.spark { width:100%; height:170px; margin-top:14px; display:block; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:12px; margin-top:16px; }
.stat { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
.stat .v { font-size:19px; margin-top:5px; font-variant-numeric:tabular-nums; }
footer { margin-top:20px; font-size:12px; color:var(--dim); line-height:1.7; }
a { color:#7dd3fc; text-decoration:none; }
.warn { color:var(--gold); font-size:12px; margin-top:8px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>XAU<span>/USD</span> · Live Gold Spot</h1>
<span class="badge live" id="badge">live</span>
<button id="refresh">Refresh</button>
</header>
<section class="price-card">
<div class="label">Spot price · USD per troy ounce</div>
<div class="price" id="price">—<small id="unit"></small></div>
<div class="delta flat" id="delta">waiting for data…</div>
<svg class="spark" id="spark" viewBox="0 0 600 150" preserveAspectRatio="none"></svg>
<div class="warn" id="warn" hidden></div>
</section>
<section class="grid">
<div class="stat"><div class="label">Per gram</div><div class="v" id="gram">—</div></div>
<div class="stat"><div class="label">Per kg</div><div class="v" id="kg">—</div></div>
<div class="stat"><div class="label">Silver XAG/oz</div><div class="v" id="silver">—</div></div>
<div class="stat"><div class="label">Gold / Silver ratio</div><div class="v" id="ratio">—</div></div>
<div class="stat"><div class="label">XAUT premium</div><div class="v" id="xaut">—</div></div>
<div class="stat"><div class="label">PAXG premium</div><div class="v" id="paxg">—</div></div>
<div class="stat"><div class="label">BTC in gold (oz)</div><div class="v" id="btc">—</div></div>
<div class="stat"><div class="label">Samples this session</div><div class="v" id="n">0</div></div>
</section>
<footer>
<div>Source: <span id="src">—</span> · upstream updated <span id="upd">—</span> · fetched <span id="fet">—</span></div>
<div>Auto-refresh every 30s (server caches upstream for 30s). Prices are indicative spot references, delayed, and for informational purposes only — not investment advice.</div>
</footer>
</div>
<script>
const $ = id => document.getElementById(id);
const fmt = (n, d = 2) => (n === null || n === undefined || isNaN(n))
? '—' : Number(n).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
function drawSpark(values) {
const svg = $('spark'), W = 600, H = 150, pad = 10;
if (!values || values.length < 2) {
svg.innerHTML = '<text x="300" y="78" text-anchor="middle" fill="#8b9ab5" font-size="13">collecting data…</text>';
return;
}
const min = Math.min(...values), max = Math.max(...values), span = (max - min) || 1;
const X = i => pad + (W - 2 * pad) * i / (values.length - 1);
const Y = v => H - pad - (H - 2 * pad) * (v - min) / span;
const pts = values.map((v, i) => `${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(' ');
const up = values[values.length - 1] >= values[0];
const stroke = up ? '#22c55e' : '#ef4444';
svg.innerHTML =
`<polygon points="${pad},${H - pad} ${pts} ${W - pad},${H - pad}" fill="${stroke}22" />` +
`<polyline points="${pts}" fill="none" stroke="${stroke}" stroke-width="2.5" ` +
`stroke-linejoin="round" stroke-linecap="round" />`;
}
function setBadge(cls, text) { const b = $('badge'); b.className = 'badge ' + cls; b.textContent = text; }
async function load() {
try {
const res = await fetch('/api/price', { cache: 'no-store' });
const d = await res.json();
if (!res.ok || !d.ok) throw new Error(d.error || ('HTTP ' + res.status));
$('price').innerHTML = fmt(d.price) + '<small>USD</small>';
const history = d.history || [];
if (history.length > 1) {
const diff = d.price - history[0];
const pct = (diff / history[0]) * 100;
const cls = diff > 0 ? 'up' : diff < 0 ? 'dn' : 'flat';
const sign = diff > 0 ? '+' : '';
$('delta').className = 'delta ' + cls;
$('delta').textContent = `${sign}${fmt(diff)} (${sign}${fmt(pct)}%) this session`;
} else {
$('delta').className = 'delta flat';
$('delta').textContent = 'session baseline set';
}
drawSpark(history);
$('gram').textContent = fmt(d.per_gram) + ' $';
$('kg').textContent = fmt(d.per_kg, 0) + ' $';
$('silver').textContent = fmt(d.silver);
$('ratio').textContent = fmt(d.gold_silver_ratio);
$('xaut').textContent = d.xaut_ratio === null ? '—' : fmt(d.xaut_ratio, 4) + '×';
$('paxg').textContent = d.paxg_ratio === null ? '—' : fmt(d.paxg_ratio, 4) + '×';
$('btc').textContent = fmt(d.btc_gold_oz);
$('n').textContent = history.length;
$('src').textContent = d.source || '—';
$('upd').textContent = d.upstream_updated_at || '—';
$('fet').textContent = d.fetched_at || '—';
const w = $('warn');
w.hidden = !d.warning; w.textContent = d.warning || '';
setBadge(d.warning ? 'err' : 'live', d.warning ? 'stale' : 'live');
} catch (e) {
setBadge('err', 'error');
$('delta').className = 'delta dn';
$('delta').textContent = String(e.message || e);
}
}
$('refresh').addEventListener('click', load);
load();
setInterval(load, 30000);
</script>
</body>
</html>