|
|
|
|
@ -1,7 +1,12 @@
|
|
|
|
|
// /home/def/monitor/frontend/app.js
|
|
|
|
|
|
|
|
|
|
function el(tag, cls) { |
|
|
|
|
const n = document.createElement(tag || "div"); |
|
|
|
|
const n = document.createElement("div"); |
|
|
|
|
if (tag && tag !== "div") { |
|
|
|
|
const real = document.createElement(tag); |
|
|
|
|
if (cls) real.className = cls; |
|
|
|
|
return real; |
|
|
|
|
} |
|
|
|
|
if (cls) n.className = cls; |
|
|
|
|
return n; |
|
|
|
|
} |
|
|
|
|
@ -86,14 +91,28 @@ function ageTextFromIso(iso) {
|
|
|
|
|
return `${Math.floor(s / 86400)}d ago`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function fmtCad(v) { |
|
|
|
|
const n = Number(v); |
|
|
|
|
if (!Number.isFinite(n)) return "—"; |
|
|
|
|
return new Intl.NumberFormat(undefined, { |
|
|
|
|
style: "currency", |
|
|
|
|
currency: "CAD", |
|
|
|
|
minimumFractionDigits: 2, |
|
|
|
|
maximumFractionDigits: 2 |
|
|
|
|
}).format(n); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ---------------- Tooltip (singleton) ---------------- */ |
|
|
|
|
let tipEl = null; |
|
|
|
|
|
|
|
|
|
function ensureTip() { |
|
|
|
|
if (tipEl) return tipEl; |
|
|
|
|
tipEl = el("div", "spark-tip"); |
|
|
|
|
const date = el("div", "spark-tip-date"); |
|
|
|
|
const val = el("div", "spark-tip-val"); |
|
|
|
|
tipEl = document.createElement("div"); |
|
|
|
|
tipEl.className = "spark-tip"; |
|
|
|
|
const date = document.createElement("div"); |
|
|
|
|
date.className = "spark-tip-date"; |
|
|
|
|
const val = document.createElement("div"); |
|
|
|
|
val.className = "spark-tip-val"; |
|
|
|
|
tipEl.appendChild(date); |
|
|
|
|
tipEl.appendChild(val); |
|
|
|
|
document.body.appendChild(tipEl); |
|
|
|
|
@ -255,16 +274,22 @@ function attachSparkHover(canvas, line) {
|
|
|
|
|
|
|
|
|
|
/* ---------------- DOM builders ---------------- */ |
|
|
|
|
function makeRow(line) { |
|
|
|
|
const row = el("div", "row"); |
|
|
|
|
const row = document.createElement("div"); |
|
|
|
|
row.className = "row"; |
|
|
|
|
|
|
|
|
|
const left = el("div", "left"); |
|
|
|
|
const ic = el("div", "ic"); |
|
|
|
|
const left = document.createElement("div"); |
|
|
|
|
left.className = "left"; |
|
|
|
|
const ic = document.createElement("div"); |
|
|
|
|
ic.className = "ic"; |
|
|
|
|
ic.textContent = iconText(line.icon); |
|
|
|
|
|
|
|
|
|
const mid = el("div", "mid"); |
|
|
|
|
const sym = el("div", "val"); |
|
|
|
|
const mid = document.createElement("div"); |
|
|
|
|
mid.className = "mid"; |
|
|
|
|
const sym = document.createElement("div"); |
|
|
|
|
sym.className = "val"; |
|
|
|
|
sym.textContent = line.key || line.symbol || ""; |
|
|
|
|
const name = el("div", "subline"); |
|
|
|
|
const name = document.createElement("div"); |
|
|
|
|
name.className = "subline"; |
|
|
|
|
name.textContent = line.name || ""; |
|
|
|
|
|
|
|
|
|
left.appendChild(ic); |
|
|
|
|
@ -272,17 +297,21 @@ function makeRow(line) {
|
|
|
|
|
mid.appendChild(name); |
|
|
|
|
left.appendChild(mid); |
|
|
|
|
|
|
|
|
|
const center = el("div", "mid"); |
|
|
|
|
const center = document.createElement("div"); |
|
|
|
|
center.className = "mid"; |
|
|
|
|
center.style.textAlign = "right"; |
|
|
|
|
const value = el("div", "val"); |
|
|
|
|
const value = document.createElement("div"); |
|
|
|
|
value.className = "val"; |
|
|
|
|
value.textContent = line.display != null ? String(line.display) : fmtNumber(line.value); |
|
|
|
|
const vs = el("div", "subline"); |
|
|
|
|
const vs = document.createElement("div"); |
|
|
|
|
vs.className = "subline"; |
|
|
|
|
vs.textContent = line.vs ? `in ${line.vs}` : ""; |
|
|
|
|
|
|
|
|
|
center.appendChild(value); |
|
|
|
|
center.appendChild(vs); |
|
|
|
|
|
|
|
|
|
const right = el("div", "right"); |
|
|
|
|
const right = document.createElement("div"); |
|
|
|
|
right.className = "right"; |
|
|
|
|
const spark = document.createElement("canvas"); |
|
|
|
|
spark.className = "spark"; |
|
|
|
|
right.appendChild(spark); |
|
|
|
|
@ -297,35 +326,48 @@ function makeRow(line) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function makeOracleAssetRow(asset) { |
|
|
|
|
const row = el("div", "oracle-asset-row"); |
|
|
|
|
const row = document.createElement("div"); |
|
|
|
|
row.className = "oracle-asset-row"; |
|
|
|
|
|
|
|
|
|
const left = document.createElement("div"); |
|
|
|
|
left.className = "oracle-asset-left"; |
|
|
|
|
|
|
|
|
|
const left = el("div", "oracle-asset-left"); |
|
|
|
|
const title = el("div", "oracle-asset-key"); |
|
|
|
|
const title = document.createElement("div"); |
|
|
|
|
title.className = "oracle-asset-key"; |
|
|
|
|
title.textContent = `${asset.symbol} • ${asset.chain}`; |
|
|
|
|
const sub = el("div", "oracle-asset-sub"); |
|
|
|
|
|
|
|
|
|
const sub = document.createElement("div"); |
|
|
|
|
sub.className = "oracle-asset-sub"; |
|
|
|
|
const source = asset.source || "—"; |
|
|
|
|
const sourceStatus = asset.source_status || "—"; |
|
|
|
|
const freshness = asset.freshness || "unknown"; |
|
|
|
|
sub.textContent = `source: ${source} (${sourceStatus}) • ${freshness}`; |
|
|
|
|
|
|
|
|
|
left.appendChild(title); |
|
|
|
|
left.appendChild(sub); |
|
|
|
|
|
|
|
|
|
const right = el("div", "oracle-asset-right"); |
|
|
|
|
const badges = el("div", "oracle-badges"); |
|
|
|
|
const right = document.createElement("div"); |
|
|
|
|
right.className = "oracle-asset-right"; |
|
|
|
|
|
|
|
|
|
const freshBadge = el("span", `oracle-badge ${asset.stale ? "badge-stale" : "badge-fresh"}`); |
|
|
|
|
const badges = document.createElement("div"); |
|
|
|
|
badges.className = "oracle-badges"; |
|
|
|
|
|
|
|
|
|
const freshBadge = document.createElement("span"); |
|
|
|
|
freshBadge.className = `oracle-badge ${asset.stale ? "badge-stale" : "badge-fresh"}`; |
|
|
|
|
freshBadge.textContent = asset.stale ? "stale" : "fresh"; |
|
|
|
|
badges.appendChild(freshBadge); |
|
|
|
|
|
|
|
|
|
if (asset.billing_enabled) { |
|
|
|
|
const billingBadge = el("span", "oracle-badge badge-billing"); |
|
|
|
|
const billingBadge = document.createElement("span"); |
|
|
|
|
billingBadge.className = "oracle-badge badge-billing"; |
|
|
|
|
billingBadge.textContent = "billing"; |
|
|
|
|
badges.appendChild(billingBadge); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
right.appendChild(badges); |
|
|
|
|
|
|
|
|
|
const price = el("div", "oracle-price"); |
|
|
|
|
const price = document.createElement("div"); |
|
|
|
|
price.className = "oracle-price"; |
|
|
|
|
if (asset.price_cad != null && isFinite(asset.price_cad)) { |
|
|
|
|
price.textContent = `${fmtNumber(asset.price_cad)} CAD`; |
|
|
|
|
} else { |
|
|
|
|
@ -339,23 +381,31 @@ function makeOracleAssetRow(asset) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function makeOraclePanel(pricesData) { |
|
|
|
|
const wrap = el("div", "oracle-panel"); |
|
|
|
|
const wrap = document.createElement("div"); |
|
|
|
|
wrap.className = "oracle-panel"; |
|
|
|
|
|
|
|
|
|
const top = document.createElement("div"); |
|
|
|
|
top.className = "oracle-top"; |
|
|
|
|
|
|
|
|
|
const top = el("div", "oracle-top"); |
|
|
|
|
const left = document.createElement("div"); |
|
|
|
|
|
|
|
|
|
const left = el("div"); |
|
|
|
|
const title = el("div", "section-title"); |
|
|
|
|
const title = document.createElement("div"); |
|
|
|
|
title.className = "section-title"; |
|
|
|
|
title.textContent = "Oracle Status"; |
|
|
|
|
title.style.margin = "0 0 8px 0"; |
|
|
|
|
|
|
|
|
|
const updated = el("div", "oracle-updated"); |
|
|
|
|
const updated = document.createElement("div"); |
|
|
|
|
updated.className = "oracle-updated"; |
|
|
|
|
updated.textContent = `Last update: ${fmtDateTime(pricesData.updated_at)} • ${ageTextFromIso(pricesData.updated_at)}`; |
|
|
|
|
|
|
|
|
|
left.appendChild(title); |
|
|
|
|
left.appendChild(updated); |
|
|
|
|
|
|
|
|
|
const right = el("div", "oracle-summary"); |
|
|
|
|
const overall = el("span", `oracle-badge ${pricesData.status === "fresh" ? "badge-fresh" : "badge-stale"}`); |
|
|
|
|
const right = document.createElement("div"); |
|
|
|
|
right.className = "oracle-summary"; |
|
|
|
|
|
|
|
|
|
const overall = document.createElement("span"); |
|
|
|
|
overall.className = `oracle-badge ${pricesData.status === "fresh" ? "badge-fresh" : "badge-stale"}`; |
|
|
|
|
overall.textContent = pricesData.status || "unknown"; |
|
|
|
|
right.appendChild(overall); |
|
|
|
|
|
|
|
|
|
@ -369,127 +419,139 @@ function makeOraclePanel(pricesData) {
|
|
|
|
|
const billingEnabled = assets.filter(a => a.billing_enabled).length; |
|
|
|
|
const freshCount = assets.filter(a => !a.stale).length; |
|
|
|
|
|
|
|
|
|
const meta = el("div", "oracle-meta"); |
|
|
|
|
const meta = document.createElement("div"); |
|
|
|
|
meta.className = "oracle-meta"; |
|
|
|
|
meta.innerHTML = ` |
|
|
|
|
<div class="oracle-meta-item"><span class="oracle-meta-label">Billing assets</span><span class="oracle-meta-value">${billingEnabled}</span></div> |
|
|
|
|
<div class="oracle-meta-item"><span class="oracle-meta-label">Fresh assets</span><span class="oracle-meta-value">${freshCount}/${assets.length}</span></div> |
|
|
|
|
`;
|
|
|
|
|
wrap.appendChild(meta); |
|
|
|
|
|
|
|
|
|
const quoteSection = el("div", "oracle-quote-wrap"); |
|
|
|
|
const quoteTitle = el("div", "oracle-quote-title"); |
|
|
|
|
quoteTitle.textContent = "Live Quote Calculator"; |
|
|
|
|
|
|
|
|
|
const quoteForm = el("div", "oracle-quote-form"); |
|
|
|
|
|
|
|
|
|
const amountWrap = el("div", "oracle-quote-field"); |
|
|
|
|
const amountLabel = el("label", "oracle-quote-label"); |
|
|
|
|
amountLabel.setAttribute("for", "oracleQuoteAmount"); |
|
|
|
|
amountLabel.textContent = "Amount (CAD)"; |
|
|
|
|
const amountInput = document.createElement("input"); |
|
|
|
|
amountInput.id = "oracleQuoteAmount"; |
|
|
|
|
amountInput.className = "oracle-quote-input"; |
|
|
|
|
amountInput.type = "number"; |
|
|
|
|
amountInput.min = "0.01"; |
|
|
|
|
amountInput.step = "0.01"; |
|
|
|
|
amountInput.value = "100.00"; |
|
|
|
|
amountWrap.appendChild(amountLabel); |
|
|
|
|
amountWrap.appendChild(amountInput); |
|
|
|
|
|
|
|
|
|
const buttonWrap = el("div", "oracle-quote-actions"); |
|
|
|
|
const quoteBtn = document.createElement("button"); |
|
|
|
|
quoteBtn.className = "oracle-quote-button"; |
|
|
|
|
quoteBtn.type = "button"; |
|
|
|
|
quoteBtn.textContent = "Get Quote"; |
|
|
|
|
buttonWrap.appendChild(quoteBtn); |
|
|
|
|
|
|
|
|
|
quoteForm.appendChild(amountWrap); |
|
|
|
|
quoteForm.appendChild(buttonWrap); |
|
|
|
|
|
|
|
|
|
const quoteResults = el("div", "oracle-quote-results"); |
|
|
|
|
quoteResults.id = "oracleQuoteResults"; |
|
|
|
|
quoteResults.innerHTML = `<div class="oracle-quote-empty">Enter an amount and load a live quote.</div>`; |
|
|
|
|
|
|
|
|
|
quoteSection.appendChild(quoteTitle); |
|
|
|
|
quoteSection.appendChild(quoteForm); |
|
|
|
|
quoteSection.appendChild(quoteResults); |
|
|
|
|
wrap.appendChild(quoteSection); |
|
|
|
|
|
|
|
|
|
const list = el("div", "oracle-assets"); |
|
|
|
|
const list = document.createElement("div"); |
|
|
|
|
list.className = "oracle-assets"; |
|
|
|
|
for (const asset of assets) { |
|
|
|
|
list.appendChild(makeOracleAssetRow(asset)); |
|
|
|
|
} |
|
|
|
|
wrap.appendChild(list); |
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => { |
|
|
|
|
const btn = document.querySelector(".oracle-quote-button"); |
|
|
|
|
const input = document.getElementById("oracleQuoteAmount"); |
|
|
|
|
const results = document.getElementById("oracleQuoteResults"); |
|
|
|
|
|
|
|
|
|
async function loadQuote() { |
|
|
|
|
const raw = input?.value; |
|
|
|
|
const amount = Number(raw); |
|
|
|
|
|
|
|
|
|
if (!Number.isFinite(amount) || amount <= 0) { |
|
|
|
|
results.innerHTML = `<div class="oracle-quote-empty">Enter a valid CAD amount above 0.</div>`; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
results.innerHTML = `<div class="oracle-quote-empty">Loading quote…</div>`; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const res = await fetch(`/api/oracle/quote?fiat=CAD&amount=${encodeURIComponent(amount)}`, { cache: "no-store" }); |
|
|
|
|
const data = await res.json(); |
|
|
|
|
|
|
|
|
|
if (!res.ok || !data || !Array.isArray(data.quotes)) { |
|
|
|
|
results.innerHTML = `<div class="oracle-quote-empty">Quote request failed.</div>`; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const rows = data.quotes.map((q) => { |
|
|
|
|
const availability = q.available ? "available" : (q.reason || "unavailable"); |
|
|
|
|
const recommended = q.recommended ? `<span class="oracle-badge badge-billing">recommended</span>` : ""; |
|
|
|
|
const stale = q.available ? `<span class="oracle-badge badge-fresh">live</span>` : `<span class="oracle-badge badge-stale">${availability}</span>`; |
|
|
|
|
|
|
|
|
|
return ` |
|
|
|
|
<div class="oracle-quote-row"> |
|
|
|
|
<div class="oracle-quote-row-left"> |
|
|
|
|
<div class="oracle-quote-asset">${q.symbol} • ${q.chain}</div> |
|
|
|
|
<div class="oracle-quote-sub">1 ${q.symbol} = ${q.price_cad != null ? fmtNumber(q.price_cad) : "—"} CAD</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="oracle-quote-row-right"> |
|
|
|
|
<div class="oracle-quote-badges">${recommended}${stale}</div> |
|
|
|
|
<div class="oracle-quote-amount">${q.display_amount || "—"}</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
`;
|
|
|
|
|
}).join(""); |
|
|
|
|
|
|
|
|
|
results.innerHTML = ` |
|
|
|
|
<div class="oracle-quote-meta"> |
|
|
|
|
<div>Quoted: ${fmtDateTime(data.quoted_at)}</div> |
|
|
|
|
<div>Expires: ${fmtDateTime(data.expires_at)}</div> |
|
|
|
|
</div> |
|
|
|
|
<div class="oracle-quote-list">${rows}</div> |
|
|
|
|
`;
|
|
|
|
|
} catch (_err) { |
|
|
|
|
results.innerHTML = `<div class="oracle-quote-empty">Quote request failed.</div>`; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (btn && !btn.dataset.bound) { |
|
|
|
|
btn.dataset.bound = "1"; |
|
|
|
|
btn.addEventListener("click", loadQuote); |
|
|
|
|
} |
|
|
|
|
function makeQuoteRow(q) { |
|
|
|
|
const row = document.createElement("div"); |
|
|
|
|
row.className = "quote-row"; |
|
|
|
|
|
|
|
|
|
const left = document.createElement("div"); |
|
|
|
|
left.className = "quote-left"; |
|
|
|
|
|
|
|
|
|
const symbol = document.createElement("div"); |
|
|
|
|
symbol.className = "quote-symbol"; |
|
|
|
|
symbol.textContent = `${q.symbol} • ${q.chain}`; |
|
|
|
|
|
|
|
|
|
const detail = document.createElement("div"); |
|
|
|
|
detail.className = "quote-detail"; |
|
|
|
|
detail.textContent = q.price_cad != null && isFinite(q.price_cad) |
|
|
|
|
? `1 ${q.symbol} = ${fmtNumber(q.price_cad)} CAD` |
|
|
|
|
: "price unavailable"; |
|
|
|
|
|
|
|
|
|
left.appendChild(symbol); |
|
|
|
|
left.appendChild(detail); |
|
|
|
|
|
|
|
|
|
const right = document.createElement("div"); |
|
|
|
|
right.className = "quote-right"; |
|
|
|
|
|
|
|
|
|
const amount = document.createElement("div"); |
|
|
|
|
amount.className = "quote-amount"; |
|
|
|
|
amount.textContent = q.display_amount || "—"; |
|
|
|
|
|
|
|
|
|
const badges = document.createElement("div"); |
|
|
|
|
badges.className = "quote-badges"; |
|
|
|
|
|
|
|
|
|
if (q.recommended) { |
|
|
|
|
const rec = document.createElement("span"); |
|
|
|
|
rec.className = "oracle-badge badge-billing"; |
|
|
|
|
rec.textContent = "recommended"; |
|
|
|
|
badges.appendChild(rec); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const avail = document.createElement("span"); |
|
|
|
|
avail.className = `oracle-badge ${q.available ? "badge-fresh" : "badge-stale"}`; |
|
|
|
|
avail.textContent = q.available ? "available" : (q.reason || "unavailable"); |
|
|
|
|
badges.appendChild(avail); |
|
|
|
|
|
|
|
|
|
right.appendChild(amount); |
|
|
|
|
right.appendChild(badges); |
|
|
|
|
|
|
|
|
|
row.appendChild(left); |
|
|
|
|
row.appendChild(right); |
|
|
|
|
|
|
|
|
|
return row; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (input && !input.dataset.bound) { |
|
|
|
|
input.dataset.bound = "1"; |
|
|
|
|
input.addEventListener("keydown", (ev) => { |
|
|
|
|
if (ev.key === "Enter") loadQuote(); |
|
|
|
|
}); |
|
|
|
|
function makeQuotePanel(quoteData, currentAmount) { |
|
|
|
|
const wrap = document.createElement("div"); |
|
|
|
|
wrap.className = "quote-panel"; |
|
|
|
|
|
|
|
|
|
const top = document.createElement("div"); |
|
|
|
|
top.className = "quote-top"; |
|
|
|
|
|
|
|
|
|
const left = document.createElement("div"); |
|
|
|
|
|
|
|
|
|
const title = document.createElement("div"); |
|
|
|
|
title.className = "section-title"; |
|
|
|
|
title.textContent = "Live Quote"; |
|
|
|
|
title.style.margin = "0 0 8px 0"; |
|
|
|
|
|
|
|
|
|
const sub = document.createElement("div"); |
|
|
|
|
sub.className = "oracle-updated"; |
|
|
|
|
sub.textContent = `Quote for ${fmtCad(currentAmount)} • expires ${fmtDateTime(quoteData.expires_at)}`; |
|
|
|
|
|
|
|
|
|
left.appendChild(title); |
|
|
|
|
left.appendChild(sub); |
|
|
|
|
|
|
|
|
|
const controls = document.createElement("div"); |
|
|
|
|
controls.className = "quote-controls"; |
|
|
|
|
|
|
|
|
|
const input = document.createElement("input"); |
|
|
|
|
input.type = "number"; |
|
|
|
|
input.min = "0.01"; |
|
|
|
|
input.step = "0.01"; |
|
|
|
|
input.value = String(currentAmount); |
|
|
|
|
input.id = "quoteAmountInput"; |
|
|
|
|
input.className = "quote-input"; |
|
|
|
|
|
|
|
|
|
const button = document.createElement("button"); |
|
|
|
|
button.type = "button"; |
|
|
|
|
button.className = "quote-button"; |
|
|
|
|
button.textContent = "Update"; |
|
|
|
|
|
|
|
|
|
button.addEventListener("click", () => { |
|
|
|
|
const v = Number(input.value); |
|
|
|
|
if (!Number.isFinite(v) || v <= 0) return; |
|
|
|
|
refresh(v); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
input.addEventListener("keydown", (ev) => { |
|
|
|
|
if (ev.key === "Enter") { |
|
|
|
|
ev.preventDefault(); |
|
|
|
|
button.click(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
controls.appendChild(input); |
|
|
|
|
controls.appendChild(button); |
|
|
|
|
|
|
|
|
|
top.appendChild(left); |
|
|
|
|
top.appendChild(controls); |
|
|
|
|
wrap.appendChild(top); |
|
|
|
|
|
|
|
|
|
const list = document.createElement("div"); |
|
|
|
|
list.className = "quote-list"; |
|
|
|
|
for (const q of (quoteData.quotes || [])) { |
|
|
|
|
list.appendChild(makeQuoteRow(q)); |
|
|
|
|
} |
|
|
|
|
wrap.appendChild(list); |
|
|
|
|
|
|
|
|
|
return wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -525,25 +587,39 @@ function setCycle(progress) {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ---------------- Main render ---------------- */ |
|
|
|
|
async function refresh() { |
|
|
|
|
async function refresh(keepQuote = true) { |
|
|
|
|
const root = document.getElementById("root"); |
|
|
|
|
if (!root) return; |
|
|
|
|
|
|
|
|
|
const currentAmount = Number(localStorage.getItem("quoteAmountCad") || "25"); |
|
|
|
|
|
|
|
|
|
let linesData; |
|
|
|
|
let oraclePricesData; |
|
|
|
|
let quoteData = null; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const [linesRes, oracleRes] = await Promise.all([ |
|
|
|
|
const requests = [ |
|
|
|
|
fetch("/api/lines", { cache: "no-store" }), |
|
|
|
|
fetch("/api/oracle/prices", { cache: "no-store" }) |
|
|
|
|
]); |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
if (!keepQuote) { |
|
|
|
|
requests.push(fetch(`/api/oracle/quote?fiat=CAD&amount=${encodeURIComponent(currentAmount)}`, { cache: "no-store" })); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const responses = await Promise.all(requests); |
|
|
|
|
|
|
|
|
|
linesData = await responses[0].json(); |
|
|
|
|
oraclePricesData = await responses[1].json(); |
|
|
|
|
|
|
|
|
|
linesData = await linesRes.json(); |
|
|
|
|
oraclePricesData = await oracleRes.json(); |
|
|
|
|
} catch (_e) { |
|
|
|
|
if (!keepQuote && responses[2]) { |
|
|
|
|
quoteData = await responses[2].json(); |
|
|
|
|
} |
|
|
|
|
} catch (e) { |
|
|
|
|
setStatus(false, false, "Fetch failed"); |
|
|
|
|
root.innerHTML = ""; |
|
|
|
|
const m = el("div", "subline"); |
|
|
|
|
const m = document.createElement("div"); |
|
|
|
|
m.className = "subline"; |
|
|
|
|
m.textContent = "Failed to load monitor/oracle data"; |
|
|
|
|
root.appendChild(m); |
|
|
|
|
return; |
|
|
|
|
@ -555,32 +631,56 @@ async function refresh() {
|
|
|
|
|
setStatus(ok, warnings); |
|
|
|
|
setCycle(linesData.progress); |
|
|
|
|
|
|
|
|
|
const existingQuotePanel = keepQuote ? root.querySelector(".quote-panel") : null; |
|
|
|
|
const existingQuoteSep = keepQuote ? (existingQuotePanel ? existingQuotePanel.nextElementSibling : null) : null; |
|
|
|
|
|
|
|
|
|
root.innerHTML = ""; |
|
|
|
|
|
|
|
|
|
if (oraclePricesData && oraclePricesData.assets && Object.keys(oraclePricesData.assets).length > 0) { |
|
|
|
|
root.appendChild(makeOraclePanel(oraclePricesData)); |
|
|
|
|
if (keepQuote && existingQuotePanel) { |
|
|
|
|
root.appendChild(existingQuotePanel); |
|
|
|
|
if (existingQuoteSep && existingQuoteSep.classList && existingQuoteSep.classList.contains("sep")) { |
|
|
|
|
root.appendChild(existingQuoteSep); |
|
|
|
|
} else { |
|
|
|
|
const sepQ = document.createElement("div"); |
|
|
|
|
sepQ.className = "sep"; |
|
|
|
|
root.appendChild(sepQ); |
|
|
|
|
} |
|
|
|
|
} else if (quoteData && Array.isArray(quoteData.quotes)) { |
|
|
|
|
root.appendChild(makeQuotePanel(quoteData, currentAmount)); |
|
|
|
|
const sepQ = document.createElement("div"); |
|
|
|
|
sepQ.className = "sep"; |
|
|
|
|
root.appendChild(sepQ); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const sep0 = el("div", "sep"); |
|
|
|
|
if (oraclePricesData && oraclePricesData.assets && Object.keys(oraclePricesData.assets).length) { |
|
|
|
|
root.appendChild(makeOraclePanel(oraclePricesData)); |
|
|
|
|
const sep0 = document.createElement("div"); |
|
|
|
|
sep0.className = "sep"; |
|
|
|
|
root.appendChild(sep0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const fiatTitle = el("div", "section-title"); |
|
|
|
|
const fiatTitle = document.createElement("div"); |
|
|
|
|
fiatTitle.className = "section-title"; |
|
|
|
|
fiatTitle.textContent = "Currency"; |
|
|
|
|
root.appendChild(fiatTitle); |
|
|
|
|
|
|
|
|
|
const fiatWrap = el("div", "rows"); |
|
|
|
|
const fiatWrap = document.createElement("div"); |
|
|
|
|
fiatWrap.className = "rows"; |
|
|
|
|
const fiat = Array.isArray(linesData.fiat) ? linesData.fiat : []; |
|
|
|
|
for (const line of fiat) fiatWrap.appendChild(makeRow(line)); |
|
|
|
|
root.appendChild(fiatWrap); |
|
|
|
|
|
|
|
|
|
const sep = el("div", "sep"); |
|
|
|
|
const sep = document.createElement("div"); |
|
|
|
|
sep.className = "sep"; |
|
|
|
|
root.appendChild(sep); |
|
|
|
|
|
|
|
|
|
const cryptoTitle = el("div", "section-title"); |
|
|
|
|
const cryptoTitle = document.createElement("div"); |
|
|
|
|
cryptoTitle.className = "section-title"; |
|
|
|
|
cryptoTitle.textContent = "Cryptocurrency"; |
|
|
|
|
root.appendChild(cryptoTitle); |
|
|
|
|
|
|
|
|
|
const cryptoWrap = el("div", "rows"); |
|
|
|
|
const cryptoWrap = document.createElement("div"); |
|
|
|
|
cryptoWrap.className = "rows"; |
|
|
|
|
const crypto = Array.isArray(linesData.crypto) ? linesData.crypto : []; |
|
|
|
|
for (const line of crypto) cryptoWrap.appendChild(makeRow(line)); |
|
|
|
|
root.appendChild(cryptoWrap); |
|
|
|
|
@ -588,7 +688,7 @@ async function refresh() {
|
|
|
|
|
|
|
|
|
|
(function boot() { |
|
|
|
|
window.addEventListener("DOMContentLoaded", () => { |
|
|
|
|
refresh(); |
|
|
|
|
setInterval(refresh, 10000); |
|
|
|
|
refresh(false); |
|
|
|
|
setInterval(() => refresh(true), 10000); |
|
|
|
|
}); |
|
|
|
|
})(); |
|
|
|
|
|