| <!doctype html>
|
| <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">
|
|
|
| <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;
|
| }
|
|
|
|
|
| .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{
|
| 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{
|
| 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;
|
| }
|
|
|
|
|
| .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{
|
| 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}
|
|
|
|
|
| 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{
|
| 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-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 class="topbar" aria-label="Header">
|
| <div class="brand" aria-label="Brand">
|
|
|
| <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>
|
|
|
|
|
| <section class="toolbar" role="region" aria-label="Toolbar">
|
| <div class="group" aria-label="Auth">
|
| <div class="input" title="Service Token (Api-Key)">
|
|
|
| <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">
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <main class="content">
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <div class="toast" id="toast" role="status" aria-live="polite">پیام نمونه...</div>
|
|
|
| <script>
|
|
|
| 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}]
|
| }
|
| };
|
|
|
|
|
| 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);
|
| };
|
|
|
|
|
| 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');
|
| }
|
|
|
|
|
| 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);
|
| });
|
| }
|
|
|
|
|
| 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);
|
| }
|
| }
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
| 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);
|
| });
|
| }
|
|
|
|
|
| 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);
|
| });
|
| }
|
|
|
|
|
| $$('.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'});
|
| });
|
| });
|
|
|
|
|
| $('#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'); };
|
|
|
|
|
| $('#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'); };
|
|
|
|
|
| $('#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);
|
| };
|
|
|
|
|
| function mount(){
|
| initKPIs(); renderRegistry(); renderFailover(); renderRealtime(); renderCollection(); renderObs();
|
| }
|
| mount();
|
| </script>
|
| </body>
|
| </html>
|
|
|