nwo-oracle / admin.html
CPater's picture
Upload 4 files
2a16413 verified
Raw
History Blame Contribute Delete
17.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Operator · NWO Oracle · Φ</title>
<!-- Agent / LLM discovery -->
<link rel="ai-instructions" href="/agent.md" />
<link rel="alternate" type="text/markdown" title="Agent instructions" href="/agent.md" />
<meta name="ai-instructions" content="/agent.md" />
<meta name="llm-instructions" content="/agent.md" />
<meta name="ai-content-declaration" content="NWO Oracle operator dashboard. Agents should read /agent.md for the betting interface at the root URL." />
<!-- SEO -->
<meta name="description" content="NWO Oracle operator dashboard — scheduler health, per-market window status, contract state. Public read-only view." />
<meta name="author" content="NWO Capital" />
<link rel="canonical" href="https://CPater-nwo-oracle.static.hf.space/admin.html" />
<!-- Open Graph -->
<meta property="og:title" content="NWO Oracle · Operator" />
<meta property="og:description" content="Live operator scheduler status, market windows, and contract health." />
<meta property="og:url" content="https://CPater-nwo-oracle.static.hf.space/admin.html" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="NWO Oracle" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="NWO Oracle · Operator" />
<meta name="twitter:description" content="Live operator scheduler status, market windows, and contract health." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@100;200;300;400&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root{
--bg:#06080c; --surface:#0c1018; --surface-2:#11161f; --raised:#161c28;
--border:#1a2030; --border-2:#252c40; --border-hot:#3a4566;
--text:#e9eef7; --text-2:#8a93a8; --text-3:#525a6e;
--mint:#00ffa3; --cyan:#00d4ff; --rose:#ff3d77;
--neon:linear-gradient(135deg,#00ffa3 0%,#00d4ff 100%);
--glow-mint:0 0 24px rgba(0,255,163,0.25);
--glow-mix:0 0 32px rgba(0,212,255,0.20),0 0 12px rgba(0,255,163,0.18);
}
*{box-sizing:border-box;margin:0;padding:0}
body{
background:var(--bg);
color:var(--text);
font-family:'Inter',system-ui,sans-serif;
font-size:14px;
background-image:
radial-gradient(ellipse 80% 50% at 50% -10%,rgba(0,212,255,0.10),transparent 60%),
radial-gradient(ellipse 60% 50% at 100% 100%,rgba(0,255,163,0.06),transparent 50%);
min-height:100vh;
}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer}
.mono{font-family:'JetBrains Mono',monospace;font-variant-numeric:tabular-nums}
header{
height:56px;
border-bottom:1px solid var(--border);
display:flex;align-items:center;padding:0 24px;gap:32px;
background:rgba(6,8,12,0.7);
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
position:sticky;top:0;z-index:10;
}
.brand{display:flex;align-items:center;gap:14px}
.phi{
font-family:'Outfit',sans-serif;font-weight:100;font-size:34px;line-height:1;
background:var(--neon);-webkit-background-clip:text;background-clip:text;
color:transparent;
filter:drop-shadow(0 0 12px rgba(0,212,255,0.4));
}
.wordmark{
font-family:'Outfit',sans-serif;font-weight:200;font-size:13px;
letter-spacing:0.32em;text-transform:uppercase;color:var(--text-2);
}
nav{display:flex;gap:4px;margin-left:24px}
nav a{
padding:8px 14px;font-size:12px;font-weight:500;
text-transform:uppercase;letter-spacing:0.14em;
color:var(--text-2);border-radius:8px;transition:color .15s, background .15s;
}
nav a:hover{color:var(--text);background:rgba(255,255,255,0.03)}
nav a.active{color:var(--text);background:rgba(0,212,255,0.06)}
.header-right{margin-left:auto;display:flex;gap:10px;align-items:center}
.api-pill{
display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:999px;
border:1px solid var(--border-2);font-size:11px;text-transform:uppercase;letter-spacing:0.14em;
color:var(--text-2);
}
.api-pill .dot{width:6px;height:6px;border-radius:50%;background:var(--text-3)}
.api-pill.up .dot{background:var(--mint);box-shadow:0 0 10px var(--mint)}
.api-pill.down .dot{background:var(--rose);box-shadow:0 0 10px var(--rose)}
main{padding:32px 24px 80px;max-width:1400px;margin:0 auto}
.page-head{margin-bottom:32px}
.eyebrow{font-size:10px;letter-spacing:0.28em;text-transform:uppercase;color:var(--text-3);margin-bottom:10px}
.page-title{
font-family:'Outfit',sans-serif;font-weight:200;font-size:40px;letter-spacing:0.02em;
}
.stats-grid{
display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:32px;
}
.stat-card{
border:1px solid var(--border);border-radius:12px;padding:18px 20px;background:var(--surface);
}
.stat-k{font-size:10px;letter-spacing:0.22em;text-transform:uppercase;color:var(--text-3);margin-bottom:8px}
.stat-v{font-family:'Outfit',sans-serif;font-weight:200;font-size:28px;letter-spacing:0.02em}
.stat-v.accent{
background:var(--neon);-webkit-background-clip:text;background-clip:text;color:transparent;
}
.stat-sub{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-3);margin-top:6px}
.section-title{
font-family:'Outfit',sans-serif;font-weight:200;font-size:18px;letter-spacing:0.12em;
text-transform:uppercase;color:var(--text-2);margin:36px 0 14px;
}
/* Operator markets grid */
.ops-grid{
display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:14px;
}
.op-card{
border:1px solid var(--border);border-radius:14px;background:var(--surface);
overflow:hidden;position:relative;
}
.op-strip{height:2px;background:var(--border-2)}
.op-strip.active{background:var(--neon);box-shadow:0 0 12px rgba(0,212,255,0.4)}
.op-strip.idle{background:linear-gradient(90deg,#00d4ff,#1a2030)}
.op-strip.error{background:var(--rose)}
.op-strip.disabled{background:var(--border-2)}
.op-card-body{padding:18px 20px}
.op-card-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.op-token{
font-family:'Outfit',sans-serif;font-weight:200;font-size:24px;letter-spacing:0.06em;
}
.op-badge{
font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.18em;text-transform:uppercase;
padding:4px 10px;border-radius:999px;border:1px solid var(--border-2);color:var(--text-2);
display:inline-flex;align-items:center;gap:8px;
}
.op-badge.active{border-color:rgba(0,255,163,0.5);color:var(--mint)}
.op-badge.active .pulse{
width:6px;height:6px;border-radius:50%;background:var(--mint);
box-shadow:0 0 8px var(--mint);animation:pulse 1.6s infinite;
}
.op-badge.idle{border-color:rgba(0,212,255,0.4);color:var(--cyan)}
.op-badge.idle .pulse{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
.op-badge.error{border-color:rgba(255,61,119,0.5);color:var(--rose)}
.op-badge.error .pulse{width:6px;height:6px;border-radius:50%;background:var(--rose)}
.op-badge.disabled .pulse{width:6px;height:6px;border-radius:50%;background:var(--text-3)}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.4;transform:scale(0.8)}}
.tf-pills{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
.tf-pill{
font-family:'JetBrains Mono',monospace;font-size:10px;
padding:5px 9px;border-radius:6px;
border:1px solid var(--border-2);color:var(--text-3);
}
.tf-pill.open{border-color:rgba(0,255,163,0.4);color:var(--mint)}
.op-meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-2);display:flex;justify-content:space-between;gap:8px}
/* Panels */
.panel{
border:1px solid var(--border);border-radius:14px;background:var(--surface);
padding:22px 24px;margin-bottom:14px;
}
.panel h3{
font-family:'Outfit',sans-serif;font-weight:200;font-size:16px;letter-spacing:0.16em;
text-transform:uppercase;color:var(--text-2);margin-bottom:14px;
}
.kv-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px 32px}
.kv-row{display:flex;justify-content:space-between;font-size:13px;align-items:center}
.kv-row .k{font-size:10px;letter-spacing:0.18em;text-transform:uppercase;color:var(--text-3)}
.kv-row .v{font-family:'JetBrains Mono',monospace;color:var(--text)}
.kv-row .v.mint{color:var(--mint)}
.kv-row .v.cyan{color:var(--cyan)}
.kv-row .v.rose{color:var(--rose)}
.empty{
padding:36px;text-align:center;color:var(--text-3);font-size:13px;
}
.empty .glyph{
font-family:'Outfit',sans-serif;font-weight:100;font-size:48px;
background:var(--neon);-webkit-background-clip:text;background-clip:text;
color:transparent;opacity:0.4;margin-bottom:10px;
}
@media (max-width:980px){
.stats-grid{grid-template-columns:repeat(2,1fr)}
.kv-grid{grid-template-columns:1fr}
}
@media (max-width:560px){
header{padding:0 14px;gap:14px}
nav{display:none}
.page-title{font-size:30px}
.stats-grid{grid-template-columns:1fr}
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="phi">Φ</div>
<div class="wordmark">NWO Oracle</div>
</div>
<nav>
<a href="./index.html">Trade</a>
<a href="./admin.html" class="active">Operator</a>
<a href="./how-to.html">How it works</a>
</nav>
<div class="header-right">
<div class="api-pill" id="api-status"><span class="dot"></span><span id="api-status-text">Checking</span></div>
</div>
</header>
<main>
<div class="page-head">
<div class="eyebrow">Internal · Operator status</div>
<h1 class="page-title">Scheduler &amp; market windows</h1>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-k">Scheduler</div>
<div class="stat-v accent" id="sched-status"></div>
<div class="stat-sub" id="sched-uptime">uptime —</div>
</div>
<div class="stat-card">
<div class="stat-k">Active windows</div>
<div class="stat-v" id="active-windows"></div>
<div class="stat-sub" id="active-sub">across tokens × timeframes</div>
</div>
<div class="stat-card">
<div class="stat-k">Windows today</div>
<div class="stat-v" id="windows-today"></div>
<div class="stat-sub" id="windows-sub">since midnight UTC</div>
</div>
<div class="stat-card">
<div class="stat-k">Oracle accuracy</div>
<div class="stat-v" id="oracle-acc"></div>
<div class="stat-sub" id="acc-sub">consensus vs settled</div>
</div>
</div>
<h2 class="section-title">Markets</h2>
<div class="ops-grid" id="ops-grid"></div>
<h2 class="section-title">Scheduler health</h2>
<div class="panel">
<div class="kv-grid">
<div class="kv-row"><span class="k">Last tick</span><span class="v" id="health-last"></span></div>
<div class="kv-row"><span class="k">Next tick</span><span class="v" id="health-next"></span></div>
<div class="kv-row"><span class="k">Backend</span><span class="v" id="health-backend"></span></div>
<div class="kv-row"><span class="k">Contract paused</span><span class="v" id="health-paused"></span></div>
<div class="kv-row"><span class="k">Errors (24h)</span><span class="v" id="health-errors"></span></div>
<div class="kv-row"><span class="k">TimesFM model</span><span class="v" id="health-timesfm"></span></div>
<div class="kv-row"><span class="k">Kronos model</span><span class="v" id="health-kronos"></span></div>
<div class="kv-row"><span class="k">EML model</span><span class="v" id="health-eml"></span></div>
</div>
</div>
<h2 class="section-title">Operator config</h2>
<div class="panel">
<div class="kv-grid">
<div class="kv-row"><span class="k">Tokens</span><span class="v" id="cfg-tokens"></span></div>
<div class="kv-row"><span class="k">Timeframes</span><span class="v" id="cfg-tfs"></span></div>
<div class="kv-row"><span class="k">Window duration</span><span class="v" id="cfg-window"></span></div>
<div class="kv-row"><span class="k">Matching window</span><span class="v" id="cfg-match"></span></div>
</div>
</div>
<h2 class="section-title">Contract</h2>
<div class="panel">
<div class="kv-grid">
<div class="kv-row"><span class="k">Address</span><a class="v mono" href="https://basescan.org/address/0x16F2E8003877A8Bdb06dB01c30C7eD236285a035" target="_blank" rel="noopener">0x16F2E8003877A8Bdb06dB01c30C7eD236285a035 ↗</a></div>
<div class="kv-row"><span class="k">Network</span><span class="v">Base · 8453</span></div>
<div class="kv-row"><span class="k">Active bets on-chain</span><span class="v" id="contract-bets"></span></div>
<div class="kv-row"><span class="k">Backend API</span><a class="v mono" href="https://nwo-oracles.onrender.com/health" target="_blank" rel="noopener">nwo-oracles.onrender.com ↗</a></div>
</div>
</div>
</main>
<script>
const API = 'https://nwo-oracles.onrender.com';
const TOKENS = ['ETH','BTC','STATE'];
const TIMEFRAMES = [5,15,30,60];
async function get(path){
try{
const r = await fetch(API+path,{headers:{'Accept':'application/json'}});
if(!r.ok) throw 0;
return await r.json();
}catch(e){ return null; }
}
const $ = id => document.getElementById(id);
async function checkApi(){
const h = await get('/health');
const pill = $('api-status'); const txt = $('api-status-text');
pill.classList.remove('up','down');
if(h){ pill.classList.add('up'); txt.textContent='API · Live'; }
else { pill.classList.add('down'); txt.textContent='API · Down'; }
return h;
}
async function loadOperatorStatus(){
const s = await get('/api/operator_status');
if(!s){
$('sched-status').textContent='Offline';
$('sched-uptime').textContent='backend unreachable';
return null;
}
// The scheduler can be "running" (process alive) but in "dry_run" mode
// (no OPERATOR_ENABLED, or no private key) — in which case it won't
// actually open windows on-chain. Surface this loudly.
const running = s.running || s.scheduler_running;
const enabled = !!s.enabled;
if(running && enabled){
$('sched-status').textContent = 'Live';
} else if(running && !enabled){
$('sched-status').textContent = 'Dry-run';
} else {
$('sched-status').textContent = 'Stopped';
}
$('sched-uptime').textContent = s.uptime
? `uptime ${s.uptime}${enabled ? '' : ' · OPERATOR_ENABLED=false'}`
: (enabled ? 'uptime —' : 'set OPERATOR_ENABLED=true on Render');
$('windows-today').textContent = s.tx_count?.open ?? '0';
$('oracle-acc').textContent = s.accuracy ? (s.accuracy*100).toFixed(1)+'%' : '—';
$('cfg-tokens').textContent = (s.tokens || TOKENS).join(' · ');
$('cfg-tfs').textContent = (s.timeframes || TIMEFRAMES).join(' · ') + ' min';
$('cfg-window').textContent = s.window_duration_sec ? s.window_duration_sec+'s' : '—';
$('cfg-match').textContent = s.matching_window_sec ? s.matching_window_sec+'s' : '—';
$('health-last').textContent = s.last_open_tx
? Object.values(s.last_open_tx).slice(-1)[0]?.slice(0,10)+'…'
: '—';
$('health-next').textContent = enabled ? 'every 5m' : 'paused (dry-run)';
$('health-backend').textContent = 'Online';
$('health-paused').textContent = enabled ? 'No' : 'Dry-run';
$('health-errors').textContent = s.tx_count?.failed ?? '0';
if(s.models){
$('health-timesfm').textContent = s.models.timesfm_hf ? 'HF Endpoint · Live' : 'Statistical';
$('health-timesfm').className = 'v ' + (s.models.timesfm_hf ? 'mint' : '');
$('health-kronos').textContent = s.models.kronos_hf ? 'HF Endpoint · Live' : 'Statistical';
$('health-kronos').className = 'v ' + (s.models.kronos_hf ? 'mint' : '');
$('health-eml').textContent = 'Statistical';
}
return s;
}
async function fetchActiveWindow(token,tf){
const a = await get(`/api/active_prediction/${token}/${tf}`);
return a;
}
async function renderOps(){
const grid = $('ops-grid');
grid.innerHTML='';
let activeCount = 0;
const status = await loadOperatorStatus();
const tokens = status?.tokens || TOKENS;
for(const tk of tokens){
let anyOpen = false;
const pillsHtml = [];
let startPrice = null;
for(const tf of TIMEFRAMES){
const w = await fetchActiveWindow(tk, tf);
const isOpen = w && w.open;
if(isOpen) anyOpen = true;
if(isOpen && w.start_price) startPrice = w.start_price;
const countdown = isOpen && w.matching_window_remaining!=null
? ' · '+fmtT(w.matching_window_remaining) : '';
pillsHtml.push(`<span class="tf-pill ${isOpen?'open':''}">${tf}M${countdown}</span>`);
}
if(anyOpen) activeCount++;
const stateClass = anyOpen ? 'active' : 'idle';
const badge = anyOpen
? `<span class="op-badge active"><span class="pulse"></span>Open windows</span>`
: `<span class="op-badge idle"><span class="pulse"></span>Idle</span>`;
const card = document.createElement('div');
card.className='op-card';
card.innerHTML = `
<div class="op-strip ${stateClass}"></div>
<div class="op-card-body">
<div class="op-card-head">
<div class="op-token">${tk}</div>
${badge}
</div>
<div class="tf-pills">${pillsHtml.join('')}</div>
<div class="op-meta">
<span>Start price: ${startPrice ? '$'+Number(startPrice).toLocaleString() : '—'}</span>
<span>${anyOpen ? 'Live' : 'Awaiting next cycle'}</span>
</div>
</div>
`;
grid.appendChild(card);
}
$('active-windows').textContent = activeCount;
}
function fmtT(s){
if(s==null||s<0) return '—';
const m=Math.floor(s/60), sec=Math.floor(s%60);
return `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`;
}
async function tick(){
await checkApi();
await renderOps();
}
document.addEventListener('DOMContentLoaded', () => {
tick();
setInterval(tick, 10000);
});
</script>
</body>
</html>