diff --git a/frontend/app.js b/frontend/app.js
index c6fcaa9..851b263 100644
--- a/frontend/app.js
+++ b/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 = `
Enter an amount and load a live quote.
`;
+
+ 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 = `Enter a valid CAD amount above 0.
`;
+ return;
+ }
+
+ results.innerHTML = `Loading quote…
`;
+
+ 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 = `Quote request failed.
`;
+ return;
+ }
+
+ const rows = data.quotes.map((q) => {
+ const availability = q.available ? "available" : (q.reason || "unavailable");
+ const recommended = q.recommended ? `recommended` : "";
+ const stale = q.available ? `live` : `${availability}`;
+
+ return `
+
+
+
${q.symbol} • ${q.chain}
+
1 ${q.symbol} = ${q.price_cad != null ? fmtNumber(q.price_cad) : "—"} CAD
+
+
+
${recommended}${stale}
+
${q.display_amount || "—"}
+
+
+ `;
+ }).join("");
+
+ results.innerHTML = `
+
+ ${rows}
+ `;
+ } catch (_err) {
+ results.innerHTML = `Quote request failed.
`;
+ }
+ }
+
+ 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");
diff --git a/frontend/styles.css b/frontend/styles.css
index 68ad9f6..524dec8 100644
--- a/frontend/styles.css
+++ b/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;
+ }
+}