Browse Source

monitor v1.0.1: preserve live quote value across timed refresh

main
def 6 hours ago
parent
commit
a0b055e6d7
  1. 7
      PROJECT_STATE.md
  2. 6
      README.md
  3. 2
      VERSION
  4. 12
      deploy-monitor.sh
  5. 404
      frontend/app.js
  6. 3
      frontend/index.html

7
PROJECT_STATE.md

@ -1,3 +1,10 @@
## Update - 2026-03-22 16:00
- Live frontend source of truth identified as /var/www/monitor.
- Working deployment workflow established: edit repo copy in /home/def/monitor/frontend and deploy to /var/www/monitor via rsync script.
- Quote calculator behavior corrected so the amount field no longer snaps back to the default during timed refresh cycles.
- Monitor page now refreshes market/oracle data without forcibly rebuilding the active quote panel state.
# PROJECT_STATE.md
Project: monitor

6
README.md

@ -1,3 +1,9 @@
## v1.0.1 - 2026-03-22
- Fixed live quote calculator so the entered CAD value no longer resets to the default during timed page refreshes.
- Changed monitor refresh behavior so the quote panel remains stable until the user explicitly updates it.
- Confirmed live frontend is served from /var/www/monitor and synced working files back into repo frontend for future deployments.
# Monitor
Monitor is the pricing and oracle dashboard for `monitor.outsidethebox.top`.

2
VERSION

@ -1 +1 @@
v0.2.0
v1.0.1

12
deploy-monitor.sh

@ -0,0 +1,12 @@
#!/bin/bash
set -e
SRC="/home/def/monitor/frontend/"
DST="/var/www/monitor/"
echo "Deploying monitor frontend..."
rsync -rlptDv --delete --no-owner --no-group "$SRC" "$DST"
echo
echo "Deploy complete:"
ls -lah "$DST"

404
frontend/app.js

@ -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);
});
})();

3
frontend/index.html

@ -28,7 +28,8 @@
<div class="card">
<!-- app.js renders everything inside here -->
<div id="root"></div>
<div id="oracleQuoteStandalone"></div>
<div id="root"></div>
</div>
</div>

Loading…
Cancel
Save