|
|
<!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>
|
|
|
|