Browse Source

Bump Monitor to v1.1.0 with dynamic version badge

main v1.1.0
def 1 month ago
parent
commit
6976c8b3e0
  1. 12
      PROJECT_STATE.md
  2. 11
      README.md
  3. 2
      VERSION
  4. 23
      frontend/app.js
  5. 44
      frontend/brand.js
  6. BIN
      frontend/favicon.png
  7. 66
      frontend/index.html
  8. 471
      frontend/styles.css
  9. 17
      oracle/assets.json
  10. 151
      oracle/fetch_prices.js
  11. 2
      package.json

12
PROJECT_STATE.md

@ -1,3 +1,15 @@
# PROJECT_STATE - Monitor
## v1.1.0 - 2026-05-17
Current state:
- Monitor web UI is active for monitor.outsidethebox.top.
- Version badge is shown beside the main Monitor page title.
- OTB Oracle live quote panel is active.
- Billing-facing assets currently include USDC, ETH, ETHO, EGAZ, and ETI.
- Project version bumped to v1.1.0.
- Previous git version noted by user: v1.0.1.
## Update - 2026-03-22 16:00 ## Update - 2026-03-22 16:00
- Live frontend source of truth identified as /var/www/monitor. - Live frontend source of truth identified as /var/www/monitor.

11
README.md

@ -1,3 +1,14 @@
# Monitor v1.1.0
Build date: 2026-05-17
## v1.1.0 changes
- Bumped monitor project from v1.0.1 to v1.1.0.
- Added visible version badge beside the Monitor page heading.
- Updated docs for the current OTB Oracle / monitor state.
- Current monitor page includes live OTB Oracle quote display and payment asset pricing context.
## v1.0.1 - 2026-03-22 ## 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. - Fixed live quote calculator so the entered CAD value no longer resets to the default during timed page refreshes.

2
VERSION

@ -1 +1 @@
v1.0.1 v1.1.0

23
frontend/app.js

@ -16,12 +16,12 @@ function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme); localStorage.setItem("theme", theme);
const toggle = document.getElementById("themeToggle"); const toggle = document.getElementById("otbThemeToggle");
if (toggle) toggle.checked = (theme === "light"); if (toggle) toggle.checked = (theme === "light");
} }
function toggleThemeFromCheckbox() { function toggleThemeFromCheckbox() {
const toggle = document.getElementById("themeToggle"); const toggle = document.getElementById("otbThemeToggle");
const wantsLight = !!toggle?.checked; const wantsLight = !!toggle?.checked;
applyTheme(wantsLight ? "light" : "dark"); applyTheme(wantsLight ? "light" : "dark");
} }
@ -31,7 +31,7 @@ function toggleThemeFromCheckbox() {
applyTheme(saved); applyTheme(saved);
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
const toggle = document.getElementById("themeToggle"); const toggle = document.getElementById("otbThemeToggle");
if (toggle) { if (toggle) {
toggle.addEventListener("change", toggleThemeFromCheckbox); toggle.addEventListener("change", toggleThemeFromCheckbox);
toggle.checked = (document.documentElement.getAttribute("data-theme") === "light"); toggle.checked = (document.documentElement.getAttribute("data-theme") === "light");
@ -692,3 +692,20 @@ async function refresh(keepQuote = true) {
setInterval(() => refresh(true), 10000); setInterval(() => refresh(true), 10000);
}); });
})(); })();
async function loadMonitorVersionBadge() {
const badge = document.getElementById("monitor-version-badge");
if (!badge) return;
try {
const res = await fetch(`/VERSION?ts=${Date.now()}`, { cache: "no-store" });
if (!res.ok) throw new Error(`VERSION fetch failed: ${res.status}`);
const version = (await res.text()).trim();
badge.textContent = version || "";
} catch (err) {
console.warn("Monitor version badge unavailable:", err);
badge.textContent = "";
}
}
document.addEventListener("DOMContentLoaded", loadMonitorVersionBadge);

44
frontend/brand.js

@ -0,0 +1,44 @@
(function () {
const STORAGE_KEY = "otb_theme";
const root = document.documentElement;
function getPreferredTheme() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === "light" || saved === "dark") return saved;
return "dark";
}
function applyTheme(theme) {
root.setAttribute("data-theme", theme);
const toggle = document.getElementById("otbThemeToggle");
if (toggle) {
toggle.checked = theme === "dark";
}
}
function saveTheme(theme) {
localStorage.setItem(STORAGE_KEY, theme);
}
function initThemeToggle() {
const toggle = document.getElementById("otbThemeToggle");
if (!toggle) return;
toggle.addEventListener("change", function () {
const theme = toggle.checked ? "dark" : "light";
applyTheme(theme);
saveTheme(theme);
});
}
function init() {
applyTheme(getPreferredTheme());
initThemeToggle();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

BIN
frontend/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

66
frontend/index.html

@ -7,32 +7,76 @@
<link rel="stylesheet" href="/styles.css" /> <link rel="stylesheet" href="/styles.css" />
</head> </head>
<body> <body>
<header class="site-header">
<div class="site-container">
<div class="site-nav">
<a class="site-brand" href="https://outsidethebox.top">
<img src="https://outsidethebox.top/assets/favicon.png" alt="outsidethebox.top logo" />
<div class="site-title">
<strong>outsidethebox.top</strong>
<span>Managed hosting • no client server logins</span>
</div>
</a>
<div class="site-nav-right">
<nav class="site-navlinks">
<a href="https://outsidethebox.top">Home</a>
<a href="https://outsidethebox.top/pricing.html">Pricing</a>
<a href="https://outsidethebox.top/terms.html">ToS</a>
<a href="https://outsidethebox.top/contact.html">Contact</a>
<div class="dropdown">
<a href="#" class="dropdown-toggle">Services</a>
<div class="dropdown-menu">
<a href="https://follow-me.outsidethebox.top">Follow-me Tracker</a>
<a href="https://monitor.outsidethebox.top">Oracle</a>
</div>
</div>
<a href="https://otb-billing.outsidethebox.top/portal">Portal</a>
</nav>
<label class="otb-theme-switch" title="Toggle light / dark mode">
<input type="checkbox" id="otbThemeToggle" aria-label="Toggle theme" />
<span class="otb-theme-slider"></span>
</label>
</div>
</div>
</div>
</header>
<div class="wrap"> <div class="wrap">
<header class="top"> <header class="top">
<div> <div>
<div class="title">Monitor</div> <div class="title">Monitor <span id="monitor-version-badge" class="monitor-version-badge"></span></div>
<div class="sub">7-day snapshot • rotating refresh</div> <div class="sub">7-day snapshot • rotating refresh</div>
</div> </div>
<div class="top-right"> <div class="top-right">
<div class="status-pill" id="status">Loading…</div> <div class="status-pill" id="status">Loading…</div>
<div class="cycle" id="cycle"></div> <div class="cycle" id="cycle"></div>
<!-- Theme toggle (no text) -->
<label class="switch" title="Toggle theme">
<input type="checkbox" id="themeToggle" aria-label="Toggle theme" />
<span class="slider"></span>
</label>
</div> </div>
</header> </header>
<div class="card"> <div class="card">
<!-- app.js renders everything inside here --> <div id="root"></div>
<div id="oracleQuoteStandalone"></div>
<div id="root"></div>
</div> </div>
</div> </div>
<script src="/app.js" defer></script> <div class="otb-statusbar">
<div class="otb-statusbar-inner">
<strong>All billing is calculated in 🇨🇦 CAD</strong>
<span class="otb-dot"></span>
<span>Crypto conversions use the <a href="https://monitor.outsidethebox.top" target="_blank" rel="noopener noreferrer">OTB Oracle</a></span>
<span class="otb-dot"></span>
<span>
Methods:
<strong>Credit Card</strong> <span style="opacity:0.7;">(via Square)</span>,
<strong>e-Transfer</strong>,
and <strong>enabled crypto assets</strong>
</span>
</div>
</div>
<script src="/app.js" defer></script>
<script src="/brand.js" defer></script>
</body> </body>
</html> </html>

471
frontend/styles.css

@ -1,3 +1,305 @@
/* ===== OTB shared branding ===== */
html[data-theme="dark"]{
--otb-bg:#081225;
--otb-bg2:#09172d;
--otb-text:#e8eefc;
--otb-muted:#aab6d6;
--otb-panel:rgba(18,24,37,.98);
--otb-panel-soft:rgba(18,24,37,.78);
--otb-line:rgba(255,255,255,.08);
}
html[data-theme="light"]{
--otb-bg:#f4f7fb;
--otb-bg2:#eef3f9;
--otb-text:#0f172a;
--otb-muted:#475569;
--otb-panel:rgba(255,255,255,.98);
--otb-panel-soft:rgba(255,255,255,.88);
--otb-line:rgba(15,23,42,.10);
}
body{
padding-bottom:56px;
}
.site-container{
max-width:1100px;
margin:0 auto;
padding:20px 18px 0 18px;
}
.site-header{
width:100%;
}
.site-nav{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
padding:12px 0 22px 0;
}
.site-brand{
display:flex;
align-items:center;
gap:14px;
text-decoration:none;
color:inherit;
}
.site-brand img{
height:60px;
width:auto;
display:block;
object-fit:contain;
background:rgba(255,255,255,0.92);
padding:6px 12px;
border-radius:999px;
box-shadow:0 8px 24px rgba(0,0,0,0.35);
}
.site-title{
display:flex;
flex-direction:column;
line-height:1.1;
}
.site-title strong{
letter-spacing:.2px;
color:var(--otb-text);
}
.site-title span{
color:var(--otb-muted);
font-size:13px;
margin-top:2px;
}
.site-nav-right{
display:flex;
align-items:center;
gap:14px;
flex-wrap:wrap;
justify-content:flex-end;
}
.site-navlinks{
display:flex;
gap:12px;
flex-wrap:wrap;
justify-content:flex-end;
align-items:center;
}
.site-navlinks > a,
.dropdown-toggle{
text-decoration:none;
padding:8px 10px;
border-radius:12px;
color:var(--otb-muted);
border:1px solid transparent;
}
.site-navlinks > a:hover,
.dropdown-toggle:hover{
color:var(--otb-text);
border-color:var(--otb-line);
background:rgba(255,255,255,.03);
}
.dropdown{
position:relative;
display:inline-block;
}
.dropdown-toggle{
display:inline-block;
cursor:pointer;
}
.dropdown-menu{
position:absolute;
top:calc(100% + 8px);
right:0;
min-width:220px;
display:none;
padding:10px;
border-radius:14px;
background:var(--otb-panel);
border:1px solid var(--otb-line);
box-shadow:0 16px 40px rgba(0,0,0,.35);
z-index:9999;
}
.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu{
display:block;
}
.dropdown-menu a{
display:block;
padding:9px 10px;
border-radius:10px;
color:var(--otb-muted);
text-decoration:none;
white-space:nowrap;
margin:0;
}
.dropdown-menu a + a{
margin-top:4px;
}
.dropdown-menu a:hover{
color:var(--otb-text);
background:rgba(255,255,255,.04);
}
.otb-theme-switch{
position:relative;
display:inline-block;
width:54px;
height:30px;
flex:0 0 auto;
}
.otb-theme-switch input{
opacity:0;
width:0;
height:0;
}
.otb-theme-slider{
position:absolute;
inset:0;
cursor:pointer;
background:rgba(255,255,255,.10);
border:1px solid var(--otb-line);
transition:.2s;
border-radius:999px;
}
.otb-theme-slider:before{
content:"";
position:absolute;
height:22px;
width:22px;
left:3px;
top:3px;
background:var(--otb-text);
transition:.2s;
border-radius:50%;
}
.otb-theme-switch input:checked + .otb-theme-slider:before{
transform:translateX(24px);
}
.otb-statusbar{
position:fixed;
left:0;
right:0;
bottom:0;
z-index:9999;
display:flex;
align-items:center;
justify-content:center;
min-height:42px;
padding:8px 14px;
background:var(--otb-panel);
border-top:1px solid var(--otb-line);
backdrop-filter:blur(8px);
box-shadow:0 -8px 24px rgba(0,0,0,.28);
}
.otb-statusbar-inner{
width:100%;
max-width:1100px;
display:flex;
gap:10px;
align-items:center;
justify-content:center;
flex-wrap:wrap;
text-align:center;
color:var(--otb-muted);
font-size:12px;
line-height:1.35;
}
.otb-statusbar strong{
color:var(--otb-text);
font-weight:700;
}
.otb-statusbar a{
color:#62e6b7;
text-decoration:none;
font-weight:600;
}
.otb-statusbar a:hover{
text-decoration:underline;
}
.otb-dot{
width:6px;
height:6px;
border-radius:999px;
display:inline-block;
background:rgba(255,255,255,.25);
flex:0 0 auto;
}
@media (max-width: 900px){
.site-nav{
align-items:flex-start;
flex-direction:column;
}
.site-nav-right{
width:100%;
justify-content:space-between;
}
.site-navlinks{
justify-content:flex-start;
}
.dropdown{
width:100%;
}
.dropdown-toggle{
width:100%;
}
.dropdown-menu{
position:static;
right:auto;
top:auto;
min-width:100%;
margin-top:6px;
}
.site-brand img{
height:54px;
}
}
@media (max-width: 700px){
body{
padding-bottom:72px;
}
.otb-statusbar-inner{
font-size:11px;
line-height:1.25;
}
}
:root{ :root{
--bg:#0b0f19; --bg:#0b0f19;
--card:#101826; --card:#101826;
@ -381,166 +683,11 @@ canvas.spark{
} }
} }
.monitor-version-badge {
/* Quote calculator */ display: inline-block;
.oracle-quote-wrap{ margin-left: 0.55rem;
display:flex; font-size: 0.85rem;
flex-direction:column; font-weight: 600;
gap:10px; opacity: 0.72;
border:1px solid rgba(255,255,255,.06); vertical-align: middle;
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;
}
} }

17
oracle/assets.json

@ -11,8 +11,9 @@
"decimals": 6, "decimals": 6,
"billing_enabled": true, "billing_enabled": true,
"quote_priority": 1, "quote_priority": 1,
"primary_source": "coingecko", "primary_source": "coinpaprika",
"fallback_source": "static_usd" "fallback_source": "coingecko",
"coinpaprika_id": "usdc-usdc"
}, },
"ETH_ETH": { "ETH_ETH": {
"symbol": "ETH", "symbol": "ETH",
@ -22,8 +23,9 @@
"decimals": 18, "decimals": 18,
"billing_enabled": true, "billing_enabled": true,
"quote_priority": 2, "quote_priority": 2,
"primary_source": "coingecko", "primary_source": "coinpaprika",
"fallback_source": "dexscreener" "fallback_source": "coingecko",
"coinpaprika_id": "eth-ethereum"
}, },
"ETHO_ETHO": { "ETHO_ETHO": {
"symbol": "ETHO", "symbol": "ETHO",
@ -33,8 +35,9 @@
"decimals": 18, "decimals": 18,
"billing_enabled": true, "billing_enabled": true,
"quote_priority": 3, "quote_priority": 3,
"primary_source": "coingecko", "primary_source": "coinpaprika",
"fallback_source": "local" "fallback_source": "coingecko",
"coinpaprika_id": "etho-ethoprotocol"
}, },
"EGAZ_ETICA": { "EGAZ_ETICA": {
"symbol": "EGAZ", "symbol": "EGAZ",
@ -42,7 +45,7 @@
"chain": "etica", "chain": "etica",
"type": "native", "type": "native",
"decimals": 18, "decimals": 18,
"billing_enabled": false, "billing_enabled": true,
"quote_priority": 4, "quote_priority": 4,
"primary_source": "nonkyc", "primary_source": "nonkyc",
"fallback_source": "local" "fallback_source": "local"

151
oracle/fetch_prices.js

@ -2,12 +2,13 @@ const {
updateCachedPrice, updateCachedPrice,
loadCache loadCache
} = require('./price_engine'); } = require('./price_engine');
const { fetchPair } = require('../backend/providers/klingex');
async function fetchJson(url) { async function fetchJson(url) {
const res = await fetch(url, { const res = await fetch(url, {
headers: { headers: {
'accept': 'application/json', 'accept': 'application/json',
'user-agent': 'otb-oracle/0.2' 'user-agent': 'otb-oracle/0.3'
} }
}); });
@ -18,16 +19,10 @@ async function fetchJson(url) {
return res.json(); return res.json();
} }
async function fetchCoinGeckoSimplePrice() { async function fetchCoinGeckoSimplePrice(ids) {
const ids = [ const joined = Array.isArray(ids) ? ids.join(',') : String(ids || '');
'usd-coin',
'ethereum',
'ether-1',
'etica'
].join(',');
const url = const url =
`https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,cad`; `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(joined)}&vs_currencies=usd,cad`;
return fetchJson(url); return fetchJson(url);
} }
@ -37,16 +32,23 @@ async function fetchCoinPaprikaTicker(coinId) {
return fetchJson(url); return fetchJson(url);
} }
function getCadPerUsd(coingeckoData) { function hasFinitePrice(value) {
if (coingeckoData?.['usd-coin']?.cad && Number.isFinite(Number(coingeckoData['usd-coin'].cad))) { return value !== null && value !== undefined && Number.isFinite(Number(value)) && Number(value) > 0;
return Number(coingeckoData['usd-coin'].cad); }
}
function getCachedCadPerUsd() {
try { try {
const cache = loadCache(); const cache = loadCache();
const cachedCad = cache?.assets?.USDC_ARB?.price_cad; const usdc = cache?.assets?.USDC_ARB || {};
if (cachedCad && Number.isFinite(Number(cachedCad))) { const usd = Number(usdc.price_usd);
return Number(cachedCad); const cad = Number(usdc.price_cad);
if (hasFinitePrice(usd) && hasFinitePrice(cad)) {
return cad / usd;
}
if (hasFinitePrice(cad)) {
return cad;
} }
} catch (_) { } catch (_) {
} }
@ -54,11 +56,7 @@ function getCadPerUsd(coingeckoData) {
return 1.38; return 1.38;
} }
function hasFinitePrice(value) { function maybeUpdateFromCoinGecko(pairKey, sourceData, now, sourceStatus = 'primary') {
return value !== null && value !== undefined && Number.isFinite(Number(value)) && Number(value) > 0;
}
function maybeUpdateFromCoinGecko(pairKey, cgKey, sourceData, now) {
if (!sourceData || !hasFinitePrice(sourceData.usd) || !hasFinitePrice(sourceData.cad)) { if (!sourceData || !hasFinitePrice(sourceData.usd) || !hasFinitePrice(sourceData.cad)) {
return false; return false;
} }
@ -67,14 +65,14 @@ function maybeUpdateFromCoinGecko(pairKey, cgKey, sourceData, now) {
price_usd: Number(sourceData.usd), price_usd: Number(sourceData.usd),
price_cad: Number(sourceData.cad), price_cad: Number(sourceData.cad),
source: 'coingecko', source: 'coingecko',
source_status: 'primary', source_status: sourceStatus,
updated_at: now updated_at: now
}); });
return true; return true;
} }
function maybeUpdateFromCoinPaprika(pairKey, tickerData, cadPerUsd, now) { function maybeUpdateFromCoinPaprika(pairKey, tickerData, cadPerUsd, now, sourceStatus = 'primary') {
const usd = Number(tickerData?.quotes?.USD?.price); const usd = Number(tickerData?.quotes?.USD?.price);
if (!hasFinitePrice(usd)) { if (!hasFinitePrice(usd)) {
@ -87,74 +85,85 @@ function maybeUpdateFromCoinPaprika(pairKey, tickerData, cadPerUsd, now) {
price_usd: usd, price_usd: usd,
price_cad: cad, price_cad: cad,
source: 'coinpaprika', source: 'coinpaprika',
source_status: 'fallback', source_status: sourceStatus,
updated_at: now updated_at: now
}); });
return true; return true;
} }
async function main() { async function updateFromPaprika(pairKey, coinId, cadPerUsd, now, updatedPairs, sourceStatus = 'primary') {
const now = new Date().toISOString();
const updatedPairs = [];
let cgData = {};
try { try {
cgData = await fetchCoinGeckoSimplePrice(); const ticker = await fetchCoinPaprikaTicker(coinId);
if (maybeUpdateFromCoinPaprika(pairKey, ticker, cadPerUsd, now, sourceStatus)) {
updatedPairs.push(pairKey);
return true;
}
} catch (err) { } catch (err) {
console.error(`CoinGecko fetch failed: ${err.message}`); console.error(`CoinPaprika ${pairKey} fetch failed: ${err.message}`);
cgData = {};
} }
const cadPerUsd = getCadPerUsd(cgData); return false;
}
if (maybeUpdateFromCoinGecko('USDC_ARB', 'usd-coin', cgData['usd-coin'], now)) {
updatedPairs.push('USDC_ARB');
}
if (maybeUpdateFromCoinGecko('ETH_ETH', 'ethereum', cgData['ethereum'], now)) { async function updateETHOFromKlingEx(now, updatedPairs, cadPerUsd) {
updatedPairs.push('ETH_ETH'); try {
} const r = await fetchPair('ETHO-USDT');
const usd = Number(r.price);
let ethoUpdated = maybeUpdateFromCoinGecko('ETHO_ETHO', 'ether-1', cgData['ether-1'], now); if (!hasFinitePrice(usd)) {
if (!ethoUpdated) { throw new Error(`KlingEx returned non-usable ETHO price: ${r.price}`);
try {
const pap = await fetchCoinPaprikaTicker('etho-ethoprotocol');
if (maybeUpdateFromCoinPaprika('ETHO_ETHO', pap, cadPerUsd, now)) {
updatedPairs.push('ETHO_ETHO');
ethoUpdated = true;
}
} catch (err) {
console.error(`Coinpaprika ETHO fallback failed: ${err.message}`);
} }
} else {
updatedPairs.push('ETHO_ETHO');
}
let etiUpdated = maybeUpdateFromCoinGecko('ETI_ETICA', 'etica', cgData['etica'], now); const cad = Number((usd * cadPerUsd).toFixed(8));
if (!etiUpdated) {
try { updateCachedPrice('ETHO_ETHO', {
const pap = await fetchCoinPaprikaTicker('eti-etica'); price_usd: usd,
if (maybeUpdateFromCoinPaprika('ETI_ETICA', pap, cadPerUsd, now)) { price_cad: cad,
updatedPairs.push('ETI_ETICA'); source: r.source || 'klingex',
etiUpdated = true; source_status: 'primary',
} updated_at: now
} catch (err) { });
console.error(`Coinpaprika ETI fallback failed: ${err.message}`);
} updatedPairs.push('ETHO_ETHO');
} else { return true;
updatedPairs.push('ETI_ETICA'); } catch (err) {
console.error(`KlingEx ETHO fetch failed: ${err.message}`);
} }
// Last-resort fallback only.
try { try {
const papEgaz = await fetchCoinPaprikaTicker('egaz-egaz'); const cgData = await fetchCoinGeckoSimplePrice(['ether-1']);
if (maybeUpdateFromCoinPaprika('EGAZ_ETICA', papEgaz, cadPerUsd, now)) { if (maybeUpdateFromCoinGecko('ETHO_ETHO', cgData['ether-1'], now, 'fallback')) {
updatedPairs.push('EGAZ_ETICA'); updatedPairs.push('ETHO_ETHO');
return true;
} }
console.error('CoinGecko ETHO fallback returned no usable ether-1 price');
} catch (err) { } catch (err) {
console.error(`Coinpaprika EGAZ fetch failed: ${err.message}`); console.error(`CoinGecko ETHO fallback failed: ${err.message}`);
} }
return false;
}
async function main() {
const now = new Date().toISOString();
const updatedPairs = [];
let cadPerUsd = getCachedCadPerUsd();
await updateFromPaprika('USDC_ARB', 'usdc-usd-coin', cadPerUsd, now, updatedPairs, 'primary');
cadPerUsd = getCachedCadPerUsd();
await updateFromPaprika('ETH_ETH', 'eth-ethereum', cadPerUsd, now, updatedPairs, 'primary');
// ETHO uses KlingEx primary; CoinGecko only as last fallback.
await updateETHOFromKlingEx(now, updatedPairs, cadPerUsd);
await updateFromPaprika('ETI_ETICA', 'eti-etica', cadPerUsd, now, updatedPairs, 'fallback');
await updateFromPaprika('EGAZ_ETICA', 'egaz-egaz', cadPerUsd, now, updatedPairs, 'fallback');
console.log(JSON.stringify({ console.log(JSON.stringify({
ok: true, ok: true,
fetched_at: now, fetched_at: now,

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "monitor", "name": "monitor",
"version": "1.0.0", "version": "1.1.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"

Loading…
Cancel
Save