Browse Source

Add live quote calculator to oracle panel

main
def 7 days ago
parent
commit
7a4a9f1989
  1. 114
      frontend/app.js
  2. 164
      frontend/styles.css

114
frontend/app.js

@ -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");

164
frontend/styles.css

@ -380,3 +380,167 @@ canvas.spark{
grid-template-columns:1fr;
}
}
/* Quote calculator */
.oracle-quote-wrap{
display:flex;
flex-direction:column;
gap:10px;
border:1px solid rgba(255,255,255,.06);
border-radius:14px;
padding:12px;
background: rgba(0,0,0,.12);
}
[data-theme="light"] .oracle-quote-wrap{
background: rgba(255,255,255,.55);
border:1px solid rgba(0,0,0,.06);
}
.oracle-quote-title{
font-size:13px;
letter-spacing:.08em;
text-transform:uppercase;
color:var(--muted);
font-weight:800;
}
.oracle-quote-form{
display:grid;
grid-template-columns:minmax(0,1fr) auto;
gap:10px;
align-items:end;
}
.oracle-quote-field{
display:flex;
flex-direction:column;
gap:6px;
}
.oracle-quote-label{
font-size:12px;
color:var(--muted);
}
.oracle-quote-input{
width:100%;
background: rgba(255,255,255,.04);
color: var(--text);
border:1px solid rgba(255,255,255,.10);
border-radius:10px;
padding:10px 12px;
outline:none;
}
[data-theme="light"] .oracle-quote-input{
background: rgba(0,0,0,.03);
border:1px solid rgba(0,0,0,.10);
}
.oracle-quote-actions{
display:flex;
align-items:end;
}
.oracle-quote-button{
border:1px solid rgba(255,255,255,.10);
background: var(--toggle-on);
color:#fff;
border-radius:10px;
padding:10px 14px;
font-weight:800;
cursor:pointer;
}
.oracle-quote-button:hover{
filter:brightness(1.06);
}
.oracle-quote-results{
display:flex;
flex-direction:column;
gap:8px;
}
.oracle-quote-empty{
font-size:12px;
color:var(--muted);
}
.oracle-quote-meta{
display:flex;
justify-content:space-between;
gap:10px;
font-size:12px;
color:var(--muted);
}
.oracle-quote-list{
display:flex;
flex-direction:column;
gap:8px;
}
.oracle-quote-row{
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
border:1px solid rgba(255,255,255,.06);
border-radius:12px;
padding:10px 12px;
background: rgba(255,255,255,.02);
}
[data-theme="light"] .oracle-quote-row{
background: rgba(0,0,0,.02);
border:1px solid rgba(0,0,0,.06);
}
.oracle-quote-row-left{
min-width:0;
}
.oracle-quote-asset{
font-size:14px;
font-weight:800;
}
.oracle-quote-sub{
font-size:12px;
color:var(--muted);
margin-top:3px;
}
.oracle-quote-row-right{
display:flex;
flex-direction:column;
align-items:flex-end;
gap:6px;
}
.oracle-quote-badges{
display:flex;
gap:6px;
flex-wrap:wrap;
justify-content:flex-end;
}
.oracle-quote-amount{
font-size:15px;
font-weight:900;
}
@media (max-width: 720px){
.oracle-quote-form{
grid-template-columns:1fr;
}
.oracle-quote-row,
.oracle-quote-meta{
flex-direction:column;
align-items:flex-start;
}
.oracle-quote-row-right{
align-items:flex-start;
}
}

Loading…
Cancel
Save