Spaces:
Running
Running
| <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 & 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> | |