Spaces:
Runtime error
Runtime error
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Crypto Data Authority Pack – Demo UI</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <!-- Vazirmatn --> | |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#ffffff; | |
| --fg:#0b1220; | |
| --muted:#6b7280; | |
| --primary:#4f46e5; | |
| --primary-weak:#eef2ff; | |
| --success:#10b981; | |
| --warn:#f59e0b; | |
| --danger:#ef4444; | |
| --glass: rgba(255,255,255,0.65); | |
| --border: rgba(15,23,42,0.08); | |
| --shadow: 0 12px 30px rgba(2,6,23,0.08); | |
| --radius:14px; | |
| --radius-sm:10px; | |
| --card-blur: 10px; | |
| --kpi-bg:#f8fafc; | |
| --chip:#0ea5e9; | |
| --table-stripe:#f8fafc; | |
| --code-bg:#0b1220; | |
| --code-fg:#e5e7eb; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%} | |
| body{ | |
| margin:0; background:var(--bg); color:var(--fg); | |
| font-family:"Vazirmatn",system-ui,Segoe UI,Roboto,Arial,sans-serif; | |
| } | |
| .page{ | |
| display:grid; grid-template-rows:auto auto 1fr; gap:18px; min-height:100vh; | |
| padding:24px clamp(16px,3vw,32px) 32px; | |
| } | |
| /* Header */ | |
| .topbar{ | |
| display:flex; align-items:center; gap:16px; flex-wrap:wrap; | |
| } | |
| .brand{ | |
| display:flex; align-items:center; gap:10px; padding:10px 14px; | |
| border:1px solid var(--border); border-radius:var(--radius); | |
| background:var(--glass); backdrop-filter: blur(var(--card-blur)); box-shadow:var(--shadow); | |
| } | |
| .brand svg{width:24px;height:24px} | |
| .brand h1{font-size:16px; margin:0} | |
| .ribbon{ | |
| margin-inline-start:auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; | |
| } | |
| .chip{ | |
| display:inline-flex; align-items:center; gap:8px; padding:8px 12px; border-radius:999px; | |
| background:var(--primary-weak); color:var(--primary); border:1px solid var(--border); | |
| font-size:12px; font-weight:600; | |
| } | |
| .chip .dot{width:8px;height:8px;border-radius:50%;} | |
| .dot.green{background:var(--success)} .dot.gray{background:#94a3b8} .dot.red{background:var(--danger)} | |
| /* Toolbar */ | |
| .toolbar{ | |
| display:flex; gap:12px; flex-wrap:wrap; align-items:center; | |
| background:var(--glass); border:1px solid var(--border); | |
| border-radius:var(--radius); padding:12px; backdrop-filter: blur(var(--card-blur)); box-shadow:var(--shadow); | |
| } | |
| .toolbar .group{display:flex; gap:8px; align-items:center; flex-wrap:wrap} | |
| .input{ | |
| display:flex; align-items:center; gap:8px; padding:10px 12px; border:1px solid var(--border); | |
| background:#ffffff; border-radius:12px; min-width:260px; | |
| } | |
| .input input{ | |
| border:none; outline:none; background:transparent; width:180px; font-family:inherit; font-size:14px; | |
| } | |
| .btn{ | |
| appearance:none; border:none; outline:none; cursor:pointer; font-family:inherit; | |
| padding:10px 14px; border-radius:12px; font-weight:700; transition: .2s ease; | |
| background:var(--primary); color:white; box-shadow:0 6px 16px rgba(79,70,229,.25); | |
| } | |
| .btn.ghost{background:transparent; color:var(--primary); border:1px solid var(--border)} | |
| .btn:active{transform:translateY(1px)} | |
| .switch{ | |
| display:inline-flex; gap:6px; border:1px solid var(--border); border-radius:999px; padding:6px; | |
| background:#fff; | |
| } | |
| .switch button{padding:8px 12px; border-radius:999px; border:none; background:transparent; cursor:pointer; font-weight:700} | |
| .switch button.active{background:var(--primary-weak); color:var(--primary)} | |
| /* Tabs */ | |
| .tabs{ | |
| display:flex; gap:8px; flex-wrap:wrap; position:sticky; top:12px; z-index:3; | |
| } | |
| .tab{ | |
| border:1px solid var(--border); background:#fff; border-radius:12px; padding:10px 12px; cursor:pointer; font-weight:700; | |
| } | |
| .tab.active{background:var(--primary); color:#fff; box-shadow:0 6px 16px rgba(79,70,229,.25)} | |
| .content{ | |
| display:grid; gap:18px; | |
| } | |
| /* Cards */ | |
| .grid{ | |
| display:grid; gap:16px; | |
| grid-template-columns: repeat(12, minmax(0,1fr)); | |
| } | |
| .col-12{grid-column: span 12} | |
| .col-6{grid-column: span 6} | |
| .col-4{grid-column: span 4} | |
| .col-3{grid-column: span 3} | |
| @media (max-width:1100px){ .col-6,.col-4{grid-column: span 12} .col-3{grid-column: span 6} } | |
| .card{ | |
| background:var(--glass); border:1px solid var(--border); | |
| border-radius:var(--radius); box-shadow:var(--shadow); backdrop-filter: blur(var(--card-blur)); | |
| padding:16px; | |
| } | |
| .card h3{margin:0 0 6px 0; font-size:15px} | |
| .muted{color:var(--muted); font-size:13px} | |
| .kpi{ | |
| display:flex; align-items:end; justify-content:space-between; background:var(--kpi-bg); | |
| border:1px solid var(--border); border-radius:var(--radius-sm); padding:14px; | |
| } | |
| .kpi .big{font-size:26px; font-weight:800} | |
| .kpi .trend{display:flex; align-items:center; gap:6px; font-weight:700} | |
| .trend.up{color:var(--success)} .trend.down{color:var(--danger)} | |
| /* Table */ | |
| .table{ | |
| width:100%; border-collapse:separate; border-spacing:0; overflow:auto; border:1px solid var(--border); border-radius:12px; | |
| } | |
| .table th, .table td{ | |
| text-align:start; padding:10px 12px; border-bottom:1px solid var(--border); font-size:13px; | |
| vertical-align:middle; | |
| } | |
| .table tr:nth-child(odd) td{background:var(--table-stripe)} | |
| .badge{display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; font-weight:700; font-size:12px;} | |
| .badge.ok{background:#ecfdf5; color:var(--success); border:1px solid #d1fae5} | |
| .badge.warn{background:#fff7ed; color:var(--warn); border:1px solid #ffedd5} | |
| .badge.err{background:#fef2f2; color:var(--danger); border:1px solid #fee2e2} | |
| /* Code */ | |
| pre{ | |
| margin:0; background:var(--code-bg); color:var(--code-fg); | |
| border-radius:12px; padding:12px; direction:ltr; overflow:auto; font-family:ui-monospace,Menlo,Consolas,monospace; font-size:12px; | |
| } | |
| /* Toast */ | |
| .toast{ | |
| position:fixed; bottom:24px; inset-inline:24px auto; display:none; z-index:10; | |
| padding:12px 16px; border-radius:12px; background:#0b1220; color:#e5e7eb; box-shadow:var(--shadow); | |
| } | |
| .toast.show{display:block; animation:fade .25s ease} | |
| @keyframes fade{from{opacity:0; transform:translateY(8px)} to{opacity:1; transform:translateY(0)}} | |
| /* Icon button */ | |
| .icon-btn{display:inline-flex; align-items:center; gap:8px; border:1px solid var(--border); padding:10px 12px; border-radius:12px; background:#fff; cursor:pointer} | |
| .icon-btn svg{width:18px;height:18px} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page" id="app"> | |
| <!-- Header --> | |
| <header class="topbar" aria-label="Header"> | |
| <div class="brand" aria-label="Brand"> | |
| <!-- Logo SVG --> | |
| <svg viewBox="0 0 24 24" fill="none" aria-hidden="true"> | |
| <defs> | |
| <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1"> | |
| <stop offset="0" stop-color="#6366f1"/><stop offset="1" stop-color="#22d3ee"/> | |
| </linearGradient> | |
| </defs> | |
| <circle cx="12" cy="12" r="10" stroke="url(#g1)" stroke-width="2"></circle> | |
| <path d="M8 12h8M12 8v8" stroke="url(#g1)" stroke-width="2" stroke-linecap="round"/> | |
| </svg> | |
| <div> | |
| <h1>Crypto Data Authority Pack</h1> | |
| <div class="muted" id="subtitle">مرجع یکپارچه منابع بازار، خبر، سنتیمنت، آنچین</div> | |
| </div> | |
| </div> | |
| <div class="ribbon"> | |
| <span class="chip" title="Backend status"> | |
| <span class="dot green"></span> Backend: Healthy | |
| </span> | |
| <span class="chip" id="ws-status" title="WebSocket status"> | |
| <span class="dot gray"></span> WS: Disconnected | |
| </span> | |
| <span class="chip" title="Updated"> | |
| ⏱️ Updated: <span id="updatedAt">—</span> | |
| </span> | |
| </div> | |
| </header> | |
| <!-- Toolbar --> | |
| <section class="toolbar" role="region" aria-label="Toolbar"> | |
| <div class="group" aria-label="Auth"> | |
| <div class="input" title="Service Token (Api-Key)"> | |
| <!-- key icon --> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none"> | |
| <path d="M15 7a4 4 0 1 0-6 3.465V14h3v3h3l2-2v-2h2l1-1" stroke="#64748b" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| <input id="token" type="password" placeholder="توکن سرویس (Api-Key)..." aria-label="Service token"> | |
| </div> | |
| <button class="btn" id="btnApply">اعمال توکن</button> | |
| <button class="btn ghost" id="btnTest">تست اتصال</button> | |
| </div> | |
| <div class="group" aria-label="Toggles"> | |
| <div class="switch" role="tablist" aria-label="Language"> | |
| <button id="fa" class="active" aria-selected="true">FA</button> | |
| <button id="en">EN</button> | |
| </div> | |
| <div class="switch" aria-label="Direction"> | |
| <button id="rtl" class="active">RTL</button> | |
| <button id="ltr">LTR</button> | |
| </div> | |
| </div> | |
| <div class="group"> | |
| <button class="icon-btn" id="btnExport" title="Export current JSON"> | |
| <!-- download icon --> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0l-4-4m4 4l4-4M5 21h14" stroke="#0ea5e9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| خروجی JSON | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Tabs --> | |
| <nav class="tabs" aria-label="Sections"> | |
| <button class="tab active" data-tab="overview">Overview</button> | |
| <button class="tab" data-tab="registry">Registry</button> | |
| <button class="tab" data-tab="failover">Failover</button> | |
| <button class="tab" data-tab="realtime">Realtime</button> | |
| <button class="tab" data-tab="collection">Collection Plan</button> | |
| <button class="tab" data-tab="templates">Query Templates</button> | |
| <button class="tab" data-tab="observability">Observability</button> | |
| <button class="tab" data-tab="docs">Docs</button> | |
| </nav> | |
| <!-- Content --> | |
| <main class="content"> | |
| <!-- OVERVIEW --> | |
| <section class="grid" id="tab-overview" role="tabpanel" aria-labelledby="Overview"> | |
| <div class="card col-12"> | |
| <h3>خلاصه / Summary</h3> | |
| <div class="muted">این دموی UI نمای کلی «پک مرجع دادههای رمز ارز» را با کارتهای KPI، تبهای پیمایش و جدولهای فشرده نمایش میدهد.</div> | |
| </div> | |
| <div class="col-3 card"> | |
| <div class="kpi"> | |
| <div> | |
| <div class="muted">Total Providers</div> | |
| <div class="big" id="kpiTotal">—</div> | |
| </div> | |
| <div class="trend up">▲ +5</div> | |
| </div> | |
| </div> | |
| <div class="col-3 card"> | |
| <div class="kpi"> | |
| <div> | |
| <div class="muted">Free Endpoints</div> | |
| <div class="big" id="kpiFree">—</div> | |
| </div> | |
| <div class="trend up">▲ 2</div> | |
| </div> | |
| </div> | |
| <div class="col-3 card"> | |
| <div class="kpi"> | |
| <div> | |
| <div class="muted">Failover Chains</div> | |
| <div class="big" id="kpiChains">—</div> | |
| </div> | |
| <div class="trend up">▲ 1</div> | |
| </div> | |
| </div> | |
| <div class="col-3 card"> | |
| <div class="kpi"> | |
| <div> | |
| <div class="muted">WS Topics</div> | |
| <div class="big" id="kpiWs">—</div> | |
| </div> | |
| <div class="trend up">▲ 3</div> | |
| </div> | |
| </div> | |
| <div class="col-12 card"> | |
| <h3>نمونه درخواستها (Examples)</h3> | |
| <div class="grid"> | |
| <div class="col-6"> | |
| <div class="muted">CoinGecko – Simple Price</div> | |
| <pre>curl -s 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd'</pre> | |
| </div> | |
| <div class="col-6"> | |
| <div class="muted">Binance – Klines</div> | |
| <pre>curl -s 'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=100'</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- REGISTRY --> | |
| <section class="grid" id="tab-registry" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Registry Snapshot</h3> | |
| <div class="muted">نمای خلاصهی ردهها و سرویسها (نمونهداده داخلی)</div> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Categories</h3> | |
| <table class="table" id="tblCategories" aria-label="Categories table"> | |
| <thead><tr><th>Category</th><th>Count</th><th>Notes</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Highlighted Providers</h3> | |
| <table class="table" id="tblProviders" aria-label="Providers table"> | |
| <thead><tr><th>Name</th><th>Role</th><th>Status</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- FAILOVER --> | |
| <section class="grid" id="tab-failover" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Failover Chains</h3> | |
| <div class="muted">زنجیرههای جایگزینی آزاد-محور (Free-first)</div> | |
| </div> | |
| <div class="card col-12" id="failoverList"></div> | |
| </section> | |
| <!-- REALTIME --> | |
| <section class="grid" id="tab-realtime" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Realtime (WebSocket)</h3> | |
| <div class="muted">قرارداد موضوعها، پیامها، heartbeat و استراتژی reconnect</div> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Topics</h3> | |
| <table class="table" id="tblWs" aria-label="WS topics"> | |
| <thead><tr><th>Topic</th><th>Example</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Sample Message</h3> | |
| <pre id="wsMessage"></pre> | |
| <div style="margin-top:10px; display:flex; gap:8px"> | |
| <button class="btn" id="btnWsConnect">Connect (Mock)</button> | |
| <button class="btn ghost" id="btnWsDisconnect">Disconnect</button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- COLLECTION PLAN --> | |
| <section class="grid" id="tab-collection" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Collection Plan (ETL/ELT)</h3> | |
| <div class="muted">زمانبندی دریافت داده و TTL</div> | |
| </div> | |
| <div class="card col-12"> | |
| <table class="table" id="tblCollection"> | |
| <thead><tr><th>Bucket</th><th>Endpoints</th><th>Schedule</th><th>TTL</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- TEMPLATES --> | |
| <section class="grid" id="tab-templates" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Query Templates</h3> | |
| <div class="muted">قرارداد endpointها + نمونه cURL</div> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>coingecko.simple_price</h3> | |
| <pre>GET /simple/price?ids={ids}&vs_currencies={fiats}</pre> | |
| <pre>curl -s 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd'</pre> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>binance_public.klines</h3> | |
| <pre>GET /api/v3/klines?symbol={symbol}&interval={interval}&limit={n}</pre> | |
| <pre>curl -s 'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=100'</pre> | |
| </div> | |
| </section> | |
| <!-- OBSERVABILITY --> | |
| <section class="grid" id="tab-observability" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Observability</h3> | |
| <div class="muted">متریکها، بررسی کیفیت داده، هشدارها</div> | |
| </div> | |
| <div class="card col-4"> | |
| <div class="kpi"> | |
| <div><div class="muted">Success Rate</div><div class="big" id="succRate">—</div></div> | |
| <div class="trend up">▲</div> | |
| </div> | |
| </div> | |
| <div class="card col-4"> | |
| <div class="kpi"> | |
| <div><div class="muted">p95 Latency</div><div class="big" id="p95">—</div></div> | |
| <div class="trend down">▼</div> | |
| </div> | |
| </div> | |
| <div class="card col-4"> | |
| <div class="kpi"> | |
| <div><div class="muted">Failover Activations</div><div class="big" id="fo">—</div></div> | |
| <div class="trend up">▲</div> | |
| </div> | |
| </div> | |
| <div class="card col-12"> | |
| <h3>Data Quality Checklist</h3> | |
| <table class="table" id="tblDQ"> | |
| <thead><tr><th>Rule</th><th>Status</th><th>Note</th></tr></thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- DOCS --> | |
| <section class="grid" id="tab-docs" role="tabpanel" hidden> | |
| <div class="card col-12"> | |
| <h3>Docs (Compact)</h3> | |
| <div class="muted">راهنمای استفاده، امنیت و نسخهبندی بهصورت خلاصه</div> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Quick Start</h3> | |
| <ol style="margin:0; padding-inline-start:20px"> | |
| <li>JSON اصلی را لود کنید.</li> | |
| <li>از discovery برای یافتن id استفاده کنید.</li> | |
| <li>query_templates را بخوانید.</li> | |
| <li>Auth را اعمال کنید (توکن سرویس + کلید آزاد).</li> | |
| <li>درخواست بزنید یا به WS مشترک شوید.</li> | |
| </ol> | |
| </div> | |
| <div class="card col-6"> | |
| <h3>Security Notes</h3> | |
| <ul style="margin:0; padding-inline-start:20px"> | |
| <li>کلیدهای رایگان عمومیاند؛ برای سقف بیشتر کلید خودتان را وارد کنید.</li> | |
| <li>توکن سرویس، سهمیه و دسترسی را کنترل میکند.</li> | |
| <li>کلیدها در لاگ ماسک میشوند.</li> | |
| </ul> | |
| </div> | |
| <div class="card col-12"> | |
| <h3>Change Log</h3> | |
| <pre>{ | |
| "version": "3.0.0", | |
| "changes": ["Added WS spec","Expanded failover","Token-based access & quotas","Observability & DQ"] | |
| }</pre> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <!-- Toast --> | |
| <div class="toast" id="toast" role="status" aria-live="polite">پیام نمونه...</div> | |
| <script> | |
| // -------- Sample Data (compact mirror of your spec) -------- | |
| const sample = { | |
| metadata:{updated:new Date().toISOString()}, | |
| registry:{ | |
| rpc_nodes: [{id:"publicnode_eth_mainnet",name:"PublicNode Ethereum",role:"rpc",base_url:"https://ethereum.publicnode.com"}], | |
| block_explorers:[{id:"etherscan_primary",name:"Etherscan",role:"primary",base_url:"https://api.etherscan.io/api"}], | |
| market_data_apis:[ | |
| {id:"coingecko",name:"CoinGecko",free:true,base_url:"https://api.coingecko.com/api/v3"}, | |
| {id:"binance_public",name:"Binance Public",free:true,base_url:"https://api.binance.com"} | |
| ], | |
| news_apis:[ | |
| {id:"rss_coindesk",name:"CoinDesk RSS",free:true}, | |
| {id:"cointelegraph_rss",name:"CoinTelegraph RSS",free:true} | |
| ], | |
| sentiment_apis:[{id:"alternative_me_fng",name:"Alternative.me FNG",free:true}], | |
| onchain_analytics_apis:[{id:"glassnode_general",name:"Glassnode",free:false}], | |
| whale_tracking_apis:[{id:"whale_alert",name:"Whale Alert",free:false}], | |
| community_sentiment_apis:[{id:"reddit_cryptocurrency_new",name:"Reddit r/CryptoCurrency",free:true}], | |
| hf_resources:[{id:"hf_model_elkulako_cryptobert",name:"CryptoBERT",type:"model"}], | |
| free_http_endpoints:[ | |
| {id:"cg_simple_price",name:"CG Simple Price"}, | |
| {id:"binance_klines",name:"Binance Klines"} | |
| ], | |
| local_backend_routes:[{id:"local_market_quotes",name:"Local Quotes"}], | |
| cors_proxies:[{id:"allorigins",name:"AllOrigins"}] | |
| }, | |
| failover:{ | |
| market:{chain:["coingecko","coinpaprika","coincap"],ttlSec:120}, | |
| news:{chain:["rss_coindesk","cointelegraph_rss","decrypt_rss"],ttlSec:600}, | |
| sentiment:{chain:["alternative_me_fng","cfgi_v1","cfgi_legacy"],ttlSec:300}, | |
| onchain:{chain:["etherscan_primary","blockscout_ethereum","blockchair_ethereum"],ttlSec:180} | |
| }, | |
| realtime_spec:{ | |
| topics:["market.ticker","market.klines","indices.fng","news.headlines","social.aggregate"], | |
| example:{topic:"market.ticker",ts:0,payload:{symbol:"BTCUSDT",price:67890.12}} | |
| }, | |
| collection_plan:[ | |
| {bucket:"market", endpoints:["coingecko.simple_price"], schedule:"every 1 min", ttlSec:120}, | |
| {bucket:"indices", endpoints:["alternative_me_fng.fng"], schedule:"every 5 min", ttlSec:300}, | |
| {bucket:"news", endpoints:["rss_coindesk.feed","cointelegraph_rss.feed"], schedule:"every 10 min", ttlSec:600} | |
| ], | |
| observability:{ | |
| successRate:"98.2%", p95:"420 ms", failovers:3, | |
| dq:[{rule:"non_empty_payload",ok:true},{rule:"freshness_within_ttl",ok:true},{rule:"price_nonnegative",ok:true}] | |
| } | |
| }; | |
| // -------- Helpers -------- | |
| const $ = (sel, root=document)=>root.querySelector(sel); | |
| const $$ = (sel, root=document)=>Array.from(root.querySelectorAll(sel)); | |
| const toast = (msg,ms=2400)=>{ | |
| const t = $('#toast'); t.textContent = msg; t.classList.add('show'); | |
| setTimeout(()=>t.classList.remove('show'), ms); | |
| }; | |
| // -------- Init KPIs -------- | |
| function initKPIs(){ | |
| const r = sample.registry; | |
| const total = Object.values(r).reduce((s,arr)=> s + (Array.isArray(arr)?arr.length:0), 0); | |
| const free = (r.market_data_apis?.filter(x=>x.free).length||0) + | |
| (r.news_apis?.filter(x=>x.free).length||0) + | |
| (r.community_sentiment_apis?.filter(x=>x.free).length||0) + | |
| (r.free_http_endpoints?.length||0); | |
| $('#kpiTotal').textContent = total; | |
| $('#kpiFree').textContent = free; | |
| $('#kpiChains').textContent = Object.keys(sample.failover||{}).length; | |
| $('#kpiWs').textContent = (sample.realtime_spec?.topics||[]).length; | |
| $('#updatedAt').textContent = new Date(sample.metadata.updated).toLocaleString('fa-IR'); | |
| } | |
| // -------- Registry Tables -------- | |
| function renderRegistry(){ | |
| const tbody = $('#tblCategories tbody'); | |
| tbody.innerHTML = ''; | |
| const reg = sample.registry; | |
| for(const k of Object.keys(reg)){ | |
| const count = (reg[k]||[]).length; | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${k}</td><td>${count}</td><td class="muted">—</td>`; | |
| tbody.appendChild(tr); | |
| } | |
| const pBody = $('#tblProviders tbody'); | |
| pBody.innerHTML = ''; | |
| const highlights = [ | |
| {name:"CoinGecko", role:"Market", ok:true}, | |
| {name:"Binance Public", role:"Market/Klines", ok:true}, | |
| {name:"Etherscan", role:"Explorer", ok:true}, | |
| {name:"Glassnode", role:"On-chain", ok:false}, | |
| ]; | |
| highlights.forEach(h=>{ | |
| const badge = h.ok ? '<span class="badge ok">Online</span>' : '<span class="badge warn">Limited</span>'; | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${h.name}</td><td>${h.role}</td><td>${badge}</td>`; | |
| pBody.appendChild(tr); | |
| }); | |
| } | |
| // -------- Failover -------- | |
| function renderFailover(){ | |
| const wrap = $('#failoverList'); wrap.innerHTML = ''; | |
| const fo = sample.failover; | |
| for(const bucket in fo){ | |
| const row = document.createElement('div'); | |
| row.className = 'card'; | |
| const chips = fo[bucket].chain.map((id,i)=>`<span class="chip" style="margin:4px">${i+1}. ${id}</span>`).join(' '); | |
| row.innerHTML = `<div class="muted">Bucket</div><h3 style="margin:4px 0 10px">${bucket}</h3> | |
| <div>${chips}</div> | |
| <div class="muted" style="margin-top:8px">TTL: ${fo[bucket].ttlSec}s</div>`; | |
| wrap.appendChild(row); | |
| } | |
| } | |
| // -------- Realtime -------- | |
| function renderRealtime(){ | |
| const tb = $('#tblWs tbody'); tb.innerHTML=''; | |
| (sample.realtime_spec.topics||[]).forEach(t=>{ | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${t}</td><td class="muted">SUBSCRIBE → "${t}"</td>`; | |
| tb.appendChild(tr); | |
| }); | |
| $('#wsMessage').textContent = JSON.stringify(sample.realtime_spec.example,null,2); | |
| } | |
| // -------- Collection Plan -------- | |
| function renderCollection(){ | |
| const tb = $('#tblCollection tbody'); tb.innerHTML=''; | |
| (sample.collection_plan||[]).forEach(x=>{ | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${x.bucket}</td><td>${x.endpoints.join(', ')}</td><td>${x.schedule}</td><td>${x.ttlSec}s</td>`; | |
| tb.appendChild(tr); | |
| }); | |
| } | |
| // -------- Observability -------- | |
| function renderObs(){ | |
| $('#succRate').textContent = sample.observability.successRate; | |
| $('#p95').textContent = sample.observability.p95; | |
| $('#fo').textContent = sample.observability.failovers; | |
| const tb = $('#tblDQ tbody'); tb.innerHTML=''; | |
| sample.observability.dq.forEach(r=>{ | |
| const st = r.ok ? '<span class="badge ok">OK</span>' : '<span class="badge err">Fail</span>'; | |
| const tr = document.createElement('tr'); | |
| tr.innerHTML = `<td>${r.rule}</td><td>${st}</td><td class="muted">—</td>`; | |
| tb.appendChild(tr); | |
| }); | |
| } | |
| // -------- Tabs -------- | |
| $$('.tab').forEach(btn=>{ | |
| btn.addEventListener('click', ()=>{ | |
| $$('.tab').forEach(b=>b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| const key = btn.dataset.tab; | |
| $$('[role="tabpanel"]').forEach(p=>p.hidden = true); | |
| $('#tab-'+key).hidden = false; | |
| window.scrollTo({top:0,behavior:'smooth'}); | |
| }); | |
| }); | |
| // -------- Toggles -------- | |
| $('#fa').onclick = ()=>{ document.documentElement.lang='fa'; $('#fa').classList.add('active'); $('#en').classList.remove('active'); $('#subtitle').textContent='مرجع یکپارچه منابع بازار، خبر، سنتیمنت، آنچین'; toast('زبان: فارسی'); }; | |
| $('#en').onclick = ()=>{ document.documentElement.lang='en'; $('#en').classList.add('active'); $('#fa').classList.remove('active'); $('#subtitle').textContent='Unified registry for market, news, sentiment & on-chain'; toast('Language: English'); }; | |
| $('#rtl').onclick = ()=>{ document.documentElement.dir='rtl'; $('#rtl').classList.add('active'); $('#ltr').classList.remove('active'); toast('جهت: RTL'); }; | |
| $('#ltr').onclick = ()=>{ document.documentElement.dir='ltr'; $('#ltr').classList.add('active'); $('#rtl').classList.remove('active'); toast('Direction: LTR'); }; | |
| // -------- Token + WS Mock -------- | |
| $('#btnApply').onclick = ()=>{ | |
| const tok = $('#token').value.trim(); | |
| if(!tok){ toast('توکن خالی است'); return;} | |
| toast('توکن اعمال شد'); | |
| }; | |
| $('#btnTest').onclick = ()=> toast('اتصال HTTP (نمونه) موفق ✔'); | |
| let wsMock = false; | |
| function setWsStatus(on){ | |
| const chip = $('#ws-status'); const dot = chip.querySelector('.dot'); | |
| if(on){ dot.className='dot green'; chip.lastChild.textContent=' WS: Connected'; } | |
| else{ dot.className='dot gray'; chip.lastChild.textContent=' WS: Disconnected'; } | |
| } | |
| $('#btnWsConnect').onclick = ()=>{ wsMock=true; setWsStatus(true); toast('WS connected (mock)'); }; | |
| $('#btnWsDisconnect').onclick = ()=>{ wsMock=false; setWsStatus(false); toast('WS disconnected'); }; | |
| // -------- Export -------- | |
| $('#btnExport').onclick = ()=>{ | |
| const blob = new Blob([JSON.stringify(sample,null,2)], {type:'application/json'}); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'crypto_resources_authoritative.sample.json'; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| }; | |
| // -------- Mount -------- | |
| function mount(){ | |
| initKPIs(); renderRegistry(); renderFailover(); renderRealtime(); renderCollection(); renderObs(); | |
| } | |
| mount(); | |
| </script> | |
| </body> | |
| </html> | |