Really-amin's picture
Upload 136 files
48ae4e0 verified
<!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">
<!-- 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&amp;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&amp;interval=1h&amp;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}&amp;vs_currencies={fiats}</pre>
<pre>curl -s 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&amp;vs_currencies=usd'</pre>
</div>
<div class="card col-6">
<h3>binance_public.klines</h3>
<pre>GET /api/v3/klines?symbol={symbol}&amp;interval={interval}&amp;limit={n}</pre>
<pre>curl -s 'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&amp;interval=1h&amp;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>