|
|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
// /home/def/monitor/frontend/app.js
|
|
|
|
|
|
|
|
|
|
function el(tag, cls) { |
|
|
|
|
const n = document.createElement(tag); |
|
|
|
|
const n = document.createElement(tag || "div"); |
|
|
|
|
if (cls) n.className = cls; |
|
|
|
|
return n; |
|
|
|
|
} |
|
|
|
|
@ -376,12 +376,120 @@ function makeOraclePanel(pricesData) {
|
|
|
|
|
`;
|
|
|
|
|
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"); |
|
|
|
|
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>`; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (btn && !btn.dataset.bound) { |
|
|
|
|
btn.dataset.bound = "1"; |
|
|
|
|
btn.addEventListener("click", loadQuote); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (input && !input.dataset.bound) { |
|
|
|
|
input.dataset.bound = "1"; |
|
|
|
|
input.addEventListener("keydown", (ev) => { |
|
|
|
|
if (ev.key === "Enter") loadQuote(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -432,7 +540,7 @@ async function refresh() {
|
|
|
|
|
|
|
|
|
|
linesData = await linesRes.json(); |
|
|
|
|
oraclePricesData = await oracleRes.json(); |
|
|
|
|
} catch (e) { |
|
|
|
|
} catch (_e) { |
|
|
|
|
setStatus(false, false, "Fetch failed"); |
|
|
|
|
root.innerHTML = ""; |
|
|
|
|
const m = el("div", "subline"); |
|
|
|
|
@ -449,7 +557,7 @@ async function refresh() {
|
|
|
|
|
|
|
|
|
|
root.innerHTML = ""; |
|
|
|
|
|
|
|
|
|
if (oraclePricesData && oraclePricesData.assets) { |
|
|
|
|
if (oraclePricesData && oraclePricesData.assets && Object.keys(oraclePricesData.assets).length > 0) { |
|
|
|
|
root.appendChild(makeOraclePanel(oraclePricesData)); |
|
|
|
|
|
|
|
|
|
const sep0 = el("div", "sep"); |
|
|
|
|
|