Really-amin commited on
Commit
d09c139
·
verified ·
1 Parent(s): 5788794

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +12 -0
  2. dashboard.html +72 -21
  3. index.html +1229 -126
app.py CHANGED
@@ -1112,11 +1112,23 @@ async def hf_run_sentiment(request: SentimentRequest):
1112
  "timestamp": datetime.now().isoformat()
1113
  }
1114
 
 
 
 
 
 
1115
  @app.websocket("/ws/live")
1116
  async def websocket_endpoint(websocket: WebSocket):
1117
  """Real-time WebSocket updates"""
1118
  await manager.connect(websocket)
1119
  try:
 
 
 
 
 
 
 
1120
  while True:
1121
  await asyncio.sleep(5)
1122
 
 
1112
  "timestamp": datetime.now().isoformat()
1113
  }
1114
 
1115
+ @app.websocket("/ws")
1116
+ async def websocket_root(websocket: WebSocket):
1117
+ """WebSocket endpoint for compatibility with websocket-client.js"""
1118
+ await websocket_endpoint(websocket)
1119
+
1120
  @app.websocket("/ws/live")
1121
  async def websocket_endpoint(websocket: WebSocket):
1122
  """Real-time WebSocket updates"""
1123
  await manager.connect(websocket)
1124
  try:
1125
+ # Send welcome message
1126
+ await websocket.send_json({
1127
+ "type": "welcome",
1128
+ "session_id": str(id(websocket)),
1129
+ "message": "Connected to Crypto Monitor WebSocket"
1130
+ })
1131
+
1132
  while True:
1133
  await asyncio.sleep(5)
1134
 
dashboard.html CHANGED
@@ -492,39 +492,90 @@ Market is bullish today</textarea>
492
  <script>
493
  async function loadData() {
494
  try {
 
 
 
 
 
 
 
 
 
495
  // Load status
496
  const statusRes = await fetch('/api/status');
 
 
 
497
  const status = await statusRes.json();
498
 
499
- document.getElementById('totalAPIs').textContent = status.total_providers;
500
- document.getElementById('onlineAPIs').textContent = status.online;
501
- document.getElementById('offlineAPIs').textContent = status.offline;
502
- document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms';
503
- document.getElementById('lastUpdate').textContent = new Date(status.timestamp).toLocaleString();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
  // Load providers
506
  const providersRes = await fetch('/api/providers');
 
 
 
507
  const providers = await providersRes.json();
508
 
509
- const tbody = document.getElementById('providersTable');
510
- tbody.innerHTML = providers.map(p => {
511
- let responseClass = 'response-fast';
512
- if (p.response_time_ms > 3000) responseClass = 'response-slow';
513
- else if (p.response_time_ms > 1000) responseClass = 'response-medium';
514
-
515
- return `
516
- <tr>
517
- <td><strong style="font-size: 15px;">${p.name}</strong></td>
518
- <td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category}</span></td>
519
- <td><span class="status-badge status-${p.status}">${p.status.toUpperCase()}</span></td>
520
- <td><span class="response-time ${responseClass}">${p.response_time_ms}ms</span></td>
521
- <td style="color: #6c757d; font-size: 13px;">${new Date(p.last_fetch).toLocaleTimeString()}</td>
522
- </tr>
523
- `}).join('');
 
 
 
 
 
 
 
 
 
 
524
 
525
  } catch (error) {
526
  console.error('Error loading data:', error);
527
- alert('Error loading data: ' + error.message);
 
 
 
 
 
 
 
 
 
 
 
 
528
  }
529
  }
530
 
 
492
  <script>
493
  async function loadData() {
494
  try {
495
+ // Show loading state
496
+ const tbody = document.getElementById('providersTable');
497
+ if (tbody) {
498
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;"><div class="loading"></div><div style="margin-top: 10px; color: #6c757d;">در حال بارگذاری...</div></td></tr>';
499
+ }
500
+ if (document.getElementById('lastUpdate')) {
501
+ document.getElementById('lastUpdate').textContent = 'در حال بارگذاری...';
502
+ }
503
+
504
  // Load status
505
  const statusRes = await fetch('/api/status');
506
+ if (!statusRes.ok) {
507
+ throw new Error(`خطا در دریافت وضعیت: ${statusRes.status} ${statusRes.statusText}`);
508
+ }
509
  const status = await statusRes.json();
510
 
511
+ if (!status || typeof status.total_providers === 'undefined') {
512
+ throw new Error('داده‌های وضعیت نامعتبر است');
513
+ }
514
+
515
+ if (document.getElementById('totalAPIs')) {
516
+ document.getElementById('totalAPIs').textContent = status.total_providers || 0;
517
+ }
518
+ if (document.getElementById('onlineAPIs')) {
519
+ document.getElementById('onlineAPIs').textContent = status.online || 0;
520
+ }
521
+ if (document.getElementById('offlineAPIs')) {
522
+ document.getElementById('offlineAPIs').textContent = status.offline || 0;
523
+ }
524
+ if (document.getElementById('avgResponse')) {
525
+ document.getElementById('avgResponse').textContent = (status.avg_response_time_ms || 0) + 'ms';
526
+ }
527
+ if (document.getElementById('lastUpdate')) {
528
+ document.getElementById('lastUpdate').textContent = status.timestamp ? new Date(status.timestamp).toLocaleString('fa-IR') : 'نامشخص';
529
+ }
530
 
531
  // Load providers
532
  const providersRes = await fetch('/api/providers');
533
+ if (!providersRes.ok) {
534
+ throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status} ${providersRes.statusText}`);
535
+ }
536
  const providers = await providersRes.json();
537
 
538
+ if (!providers || !Array.isArray(providers)) {
539
+ throw new Error('لیست APIها نامعتبر است');
540
+ }
541
+
542
+ if (tbody) {
543
+ if (providers.length === 0) {
544
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px; color: #6c757d;">هیچ APIای یافت نشد</td></tr>';
545
+ } else {
546
+ tbody.innerHTML = providers.map(p => {
547
+ let responseClass = 'response-fast';
548
+ const responseTime = p.response_time_ms || p.avg_response_time_ms || 0;
549
+ if (responseTime > 3000) responseClass = 'response-slow';
550
+ else if (responseTime > 1000) responseClass = 'response-medium';
551
+
552
+ return `
553
+ <tr>
554
+ <td><strong style="font-size: 15px;">${p.name || 'نامشخص'}</strong></td>
555
+ <td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category || 'نامشخص'}</span></td>
556
+ <td><span class="status-badge status-${p.status || 'unknown'}">${(p.status || 'unknown').toUpperCase()}</span></td>
557
+ <td><span class="response-time ${responseClass}">${responseTime}ms</span></td>
558
+ <td style="color: #6c757d; font-size: 13px;">${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString('fa-IR') : 'نامشخص'}</td>
559
+ </tr>
560
+ `}).join('');
561
+ }
562
+ }
563
 
564
  } catch (error) {
565
  console.error('Error loading data:', error);
566
+ const tbody = document.getElementById('providersTable');
567
+ if (tbody) {
568
+ tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 40px; color: #ef4444;">
569
+ <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
570
+ <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
571
+ <div style="font-size: 14px; color: #6c757d; margin-bottom: 15px;">${error.message || 'خطای نامشخص'}</div>
572
+ <button onclick="loadData()" style="padding: 10px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 12px; color: white; cursor: pointer; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">تلاش مجدد</button>
573
+ </td></tr>`;
574
+ }
575
+ if (document.getElementById('lastUpdate')) {
576
+ document.getElementById('lastUpdate').textContent = 'خطا در بارگذاری';
577
+ }
578
+ alert('❌ خطا در بارگذاری داده‌ها:\n' + (error.message || 'خطای نامشخص'));
579
  }
580
  }
581
 
index.html CHANGED
@@ -1070,15 +1070,589 @@
1070
  transform: translateX(400%);
1071
  }
1072
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  </style>
1074
  </head>
1075
 
1076
  <body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  <!-- WebSocket Status Indicator -->
1078
  <div id="ws-connection-status" class="ws-status-indicator disconnected">
1079
  <div id="ws-status-dot" class="status-dot status-dot-offline"></div>
1080
  <span id="ws-status-text" class="ws-status-text">در حال اتصال...</span>
1081
- <div id="online-users-badge" class="badge badge-info" style="margin-left: 10px;">0</div>
1082
  </div>
1083
 
1084
  <div class="container">
@@ -1215,8 +1789,23 @@
1215
  <!-- Market Table -->
1216
  <div class="market-section">
1217
  <div class="section-header">
1218
- <div class="section-title">💎 Live Market Data</div>
1219
- <button class="refresh-btn" onclick="loadMarketData()">↻ Refresh</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1220
  </div>
1221
  <div style="overflow-x: auto;">
1222
  <table id="marketTable">
@@ -1345,7 +1934,7 @@
1345
  <div class="market-section">
1346
  <div class="section-header">
1347
  <div class="section-title">📊 API Providers Status</div>
1348
- <button class="refresh-btn" onclick="loadMonitorData()">↻ Refresh</button>
1349
  </div>
1350
  <div style="overflow-x: auto;">
1351
  <table>
@@ -1376,7 +1965,7 @@
1376
  ETH looks weak
1377
  Market is bullish today</textarea>
1378
  </div>
1379
- <button class="refresh-btn" onclick="runSentiment()">🧠 Analyze Sentiment</button>
1380
  <div id="sentimentResult"
1381
  style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;">
1382
  —</div>
@@ -2016,116 +2605,317 @@ Crypto market is bullish today</textarea>
2016
  // Market Data Functions
2017
  async function loadMarketData() {
2018
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2019
  const [market, stats, sentiment, trending, defi] = await Promise.all([
2020
- fetch('/api/market').then(r => r.json()),
2021
- fetch('/api/stats').then(r => r.json()),
2022
- fetch('/api/sentiment').then(r => r.json()),
2023
- fetch('/api/trending').then(r => r.json()),
2024
- fetch('/api/defi').then(r => r.json())
2025
  ]);
2026
 
2027
- updateStats(stats, sentiment);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2028
  updateMarketTable(market.cryptocurrencies);
2029
  updateTrending(trending.trending);
2030
  updateDeFi(defi);
2031
  updateCharts(market, sentiment);
2032
  } catch (error) {
2033
  console.error('Error loading market data:', error);
 
 
 
 
 
 
 
 
 
 
2034
  }
2035
  }
2036
 
2037
  function updateStats(stats, sentiment) {
2038
- const mcap = stats.market.total_market_cap;
2039
- document.getElementById('totalMarketCap').textContent = '$' + (mcap / 1e12).toFixed(2) + 'T';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2040
 
2041
- const volume = stats.market.total_volume;
2042
- document.getElementById('totalVolume').textContent = '$' + (volume / 1e9).toFixed(2) + 'B';
 
 
 
2043
 
2044
- const btcDom = stats.market.btc_dominance;
2045
- document.getElementById('btcDominance').textContent = btcDom.toFixed(1) + '%';
 
 
 
2046
 
2047
- const fg = sentiment.fear_greed_index.value;
2048
- const classification = sentiment.fear_greed_index.classification;
2049
- document.getElementById('fearGreed').textContent = fg;
2050
- document.getElementById('sentimentLabel').innerHTML = `<span>${classification}</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2051
 
2052
- const sentEl = document.getElementById('sentimentLabel');
2053
- if (fg < 25) {
2054
- sentEl.style.color = 'var(--accent-red)';
2055
- } else if (fg < 45) {
2056
- sentEl.style.color = 'var(--accent-yellow)';
2057
- } else if (fg < 55) {
2058
- sentEl.style.color = 'var(--text-secondary)';
2059
- } else if (fg < 75) {
2060
- sentEl.style.color = 'var(--accent-blue)';
2061
- } else {
2062
- sentEl.style.color = 'var(--accent-green)';
 
 
 
 
 
 
 
 
 
 
 
 
 
2063
  }
2064
  }
2065
 
2066
  function updateMarketTable(cryptos) {
2067
- const tbody = document.getElementById('marketTableBody');
2068
- tbody.innerHTML = cryptos.map((crypto, index) => `
2069
- <tr>
2070
- <td style="font-weight: 700; color: var(--text-secondary);">${crypto.rank || index + 1}</td>
2071
- <td>
2072
- <div class="crypto-name">
2073
- ${crypto.image ? `<img src="${crypto.image}" class="crypto-img" alt="${crypto.symbol}">` :
2074
- `<div class="crypto-img" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; font-weight: 700;">${crypto.symbol[0]}</div>`}
2075
- <div>
2076
- <div style="font-weight: 600;">${crypto.name}</div>
2077
- <div class="crypto-symbol">${crypto.symbol}</div>
2078
- </div>
2079
- </div>
2080
- </td>
2081
- <td class="price">$${crypto.price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}</td>
2082
- <td><span class="change ${crypto.change_24h >= 0 ? 'positive' : 'negative'}">${crypto.change_24h >= 0 ? '+' : ''}${crypto.change_24h.toFixed(2)}%</span></td>
2083
- <td style="font-weight: 600;">$${(crypto.market_cap / 1e9).toFixed(2)}B</td>
2084
- <td style="color: var(--text-secondary);">$${(crypto.volume_24h / 1e9).toFixed(2)}B</td>
2085
- </tr>
2086
- `).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2087
  }
2088
 
2089
  function updateTrending(trending) {
2090
- const grid = document.getElementById('trendingGrid');
2091
- grid.innerHTML = trending.map((coin, index) => `
2092
- <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; padding: 15px; display: flex; align-items: center; gap: 12px;">
2093
- <div style="font-size: 20px; font-weight: 900; color: var(--accent-yellow);">#${index + 1}</div>
2094
- ${coin.thumb ? `<img src="${coin.thumb}" style="width: 32px; height: 32px; border-radius: 8px;">` : ''}
2095
- <div>
2096
- <div style="font-weight: 600;">${coin.name}</div>
2097
- <div style="font-size: 12px; color: var(--text-secondary);">${coin.symbol}</div>
2098
- </div>
2099
- </div>
2100
- `).join('');
2101
- }
2102
 
2103
- function updateDeFi(defi) {
2104
- const list = document.getElementById('defiList');
2105
- const protocols = defi.protocols || [];
 
2106
 
2107
- list.innerHTML = `
2108
- <div style="margin-bottom: 20px; padding: 20px; background: rgba(59, 130, 246, 0.1); border-radius: 16px; text-align: center;">
2109
- <div style="font-size: 36px; font-weight: 900; margin-bottom: 5px;">$${(defi.total_tvl / 1e9).toFixed(2)}B</div>
2110
- <div style="color: var(--text-secondary); font-size: 14px;">Total Value Locked</div>
2111
- </div>
2112
- <div style="display: grid; gap: 12px;">
2113
- ${protocols.map((p, i) => `
2114
- <div style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: rgba(255, 255, 255, 0.05); border-radius: 12px;">
2115
  <div>
2116
- <div style="font-weight: 600;">${i + 1}. ${p.name}</div>
2117
- <div style="font-size: 12px; color: var(--text-secondary);">${p.chain}</div>
2118
- </div>
2119
- <div style="text-align: right;">
2120
- <div style="font-weight: 700; font-size: 16px;">$${(p.tvl / 1e9).toFixed(2)}B</div>
2121
- <div style="font-size: 12px; color: ${p.change_24h >= 0 ? 'var(--accent-green)' : 'var(--accent-red)'};">
2122
- ${p.change_24h >= 0 ? '+' : ''}${p.change_24h.toFixed(2)}%
2123
- </div>
2124
  </div>
2125
  </div>
2126
- `).join('')}
2127
- </div>
2128
- `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2129
  }
2130
 
2131
  function initCharts() {
@@ -2195,22 +2985,30 @@ Crypto market is bullish today</textarea>
2195
  }
2196
 
2197
  // استفاده از WebSocket Client جدید
 
 
 
 
2198
  function connectWebSocket() {
2199
  // WebSocket client از websocket-client.js استفاده می‌شود
2200
  // که به صورت خودکار اتصال برقرار می‌کند
2201
 
2202
- if (window.wsClient) {
 
2203
  console.log('✅ WebSocket Client آماده است');
 
2204
 
2205
  // ثبت handler برای به‌روزرسانی آمار
2206
  window.wsClient.on('stats_update', (message) => {
2207
  console.log('📊 Stats update:', message.data);
2208
- updateOnlineStats(message.data);
 
 
2209
  });
2210
 
2211
  window.wsClient.on('provider_stats', (message) => {
2212
  console.log('📡 Provider stats:', message.data);
2213
- if (currentTab === 'monitor') {
2214
  updateProviderStatsDisplay(message.data);
2215
  }
2216
  });
@@ -2224,20 +3022,40 @@ Crypto market is bullish today</textarea>
2224
 
2225
  // درخواست آمار اولیه
2226
  setTimeout(() => {
2227
- if (window.wsClient.isConnected) {
2228
  window.wsClient.requestStats();
2229
  }
2230
  }, 1000);
2231
 
2232
- // درخواست آمار هر 10 ثانیه
2233
- setInterval(() => {
2234
- if (window.wsClient && window.wsClient.isConnected) {
2235
- window.wsClient.requestStats();
2236
- }
2237
- }, 10000);
 
 
2238
  } else {
2239
- console.log('⏳ در انتظار WebSocket Client...');
2240
- setTimeout(connectWebSocket, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2241
  }
2242
  }
2243
 
@@ -2277,34 +3095,76 @@ Crypto market is bullish today</textarea>
2277
  // Monitor Functions
2278
  async function loadMonitorData() {
2279
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2280
  const [status, providers] = await Promise.all([
2281
- fetch('/api/status').then(r => r.json()),
2282
- fetch('/api/providers').then(r => r.json())
2283
  ]);
2284
 
2285
- document.getElementById('totalAPIs').textContent = status.total_providers;
2286
- document.getElementById('onlineAPIs').textContent = status.online;
2287
- document.getElementById('offlineAPIs').textContent = status.offline;
2288
- document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms';
2289
 
2290
- const tbody = document.getElementById('providersTable');
2291
- tbody.innerHTML = providers.map(p => {
2292
- let statusClass = 'badge-success';
2293
- if (p.status === 'offline') statusClass = 'badge-danger';
2294
- else if (p.status === 'degraded') statusClass = 'badge-warning';
 
 
 
 
 
 
 
2295
 
2296
- return `
2297
- <tr>
2298
- <td><strong>${p.name}</strong></td>
2299
- <td><span class="badge badge-info">${p.category}</span></td>
2300
- <td><span class="badge ${statusClass}">${p.status.toUpperCase()}</span></td>
2301
- <td>${p.response_time_ms}ms</td>
2302
- <td style="color: var(--text-secondary); font-size: 13px;">${new Date(p.last_fetch).toLocaleTimeString()}</td>
2303
- </tr>
2304
- `;
2305
- }).join('');
 
 
 
 
 
 
 
 
 
 
 
2306
  } catch (error) {
2307
  console.error('Error loading monitor data:', error);
 
 
 
 
 
 
 
 
 
 
2308
  }
2309
  }
2310
 
@@ -2884,19 +3744,262 @@ Crypto market is bullish today</textarea>
2884
  }
2885
 
2886
  // Toast notification function
2887
- function showToast(message, type = 'info') {
 
 
2888
  const toast = document.createElement('div');
2889
  toast.className = `toast toast-${type}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2890
  toast.innerHTML = `
2891
- <span style="font-size: 20px;">${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'}</span>
2892
- <span>${message}</span>
 
 
 
 
2893
  `;
2894
- document.body.appendChild(toast);
2895
-
 
 
2896
  setTimeout(() => {
2897
- toast.style.animation = 'slideInRight 0.3s reverse';
2898
  setTimeout(() => toast.remove(), 300);
2899
- }, 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2900
  }
2901
 
2902
  // Close modals when clicking outside
 
1070
  transform: translateX(400%);
1071
  }
1072
  }
1073
+
1074
+ /* === Modern UI Enhancements === */
1075
+
1076
+ /* Ripple Effect for Buttons */
1077
+ .ripple {
1078
+ position: relative;
1079
+ overflow: hidden;
1080
+ }
1081
+
1082
+ .ripple::after {
1083
+ content: '';
1084
+ position: absolute;
1085
+ top: 50%;
1086
+ left: 50%;
1087
+ width: 0;
1088
+ height: 0;
1089
+ border-radius: 50%;
1090
+ background: rgba(255, 255, 255, 0.5);
1091
+ transform: translate(-50%, -50%);
1092
+ transition: width 0.6s, height 0.6s;
1093
+ }
1094
+
1095
+ .ripple:active::after {
1096
+ width: 300px;
1097
+ height: 300px;
1098
+ }
1099
+
1100
+ /* Enhanced Card Animations */
1101
+ .stat-card,
1102
+ .market-section,
1103
+ .chart-container {
1104
+ animation: cardFadeIn 0.6s ease-out;
1105
+ animation-fill-mode: both;
1106
+ }
1107
+
1108
+ .stat-card:nth-child(1) { animation-delay: 0.1s; }
1109
+ .stat-card:nth-child(2) { animation-delay: 0.2s; }
1110
+ .stat-card:nth-child(3) { animation-delay: 0.3s; }
1111
+ .stat-card:nth-child(4) { animation-delay: 0.4s; }
1112
+ .stat-card:nth-child(5) { animation-delay: 0.5s; }
1113
+
1114
+ @keyframes cardFadeIn {
1115
+ from {
1116
+ opacity: 0;
1117
+ transform: translateY(30px) scale(0.95);
1118
+ }
1119
+ to {
1120
+ opacity: 1;
1121
+ transform: translateY(0) scale(1);
1122
+ }
1123
+ }
1124
+
1125
+ /* Number Counter Animation */
1126
+ .number-counter {
1127
+ display: inline-block;
1128
+ transition: all 0.3s ease;
1129
+ }
1130
+
1131
+ .number-counter.updated {
1132
+ animation: numberPop 0.5s ease;
1133
+ }
1134
+
1135
+ @keyframes numberPop {
1136
+ 0%, 100% { transform: scale(1); }
1137
+ 50% { transform: scale(1.15); color: var(--accent-blue); }
1138
+ }
1139
+
1140
+ /* Skeleton Loading */
1141
+ .skeleton-loader {
1142
+ background: linear-gradient(
1143
+ 90deg,
1144
+ rgba(255, 255, 255, 0.05) 25%,
1145
+ rgba(255, 255, 255, 0.15) 50%,
1146
+ rgba(255, 255, 255, 0.05) 75%
1147
+ );
1148
+ background-size: 200% 100%;
1149
+ animation: skeleton-loading 1.5s ease-in-out infinite;
1150
+ border-radius: 8px;
1151
+ }
1152
+
1153
+ @keyframes skeleton-loading {
1154
+ 0% { background-position: 200% 0; }
1155
+ 100% { background-position: -200% 0; }
1156
+ }
1157
+
1158
+ .skeleton-text {
1159
+ height: 16px;
1160
+ margin-bottom: 8px;
1161
+ }
1162
+
1163
+ .skeleton-title {
1164
+ height: 24px;
1165
+ width: 60%;
1166
+ margin-bottom: 16px;
1167
+ }
1168
+
1169
+ .skeleton-avatar {
1170
+ width: 40px;
1171
+ height: 40px;
1172
+ border-radius: 50%;
1173
+ }
1174
+
1175
+ /* Enhanced Table Row Animations */
1176
+ table tbody tr {
1177
+ animation: rowSlideIn 0.4s ease-out;
1178
+ animation-fill-mode: both;
1179
+ }
1180
+
1181
+ table tbody tr:nth-child(1) { animation-delay: 0.05s; }
1182
+ table tbody tr:nth-child(2) { animation-delay: 0.1s; }
1183
+ table tbody tr:nth-child(3) { animation-delay: 0.15s; }
1184
+ table tbody tr:nth-child(4) { animation-delay: 0.2s; }
1185
+ table tbody tr:nth-child(5) { animation-delay: 0.25s; }
1186
+ table tbody tr:nth-child(n+6) { animation-delay: 0.3s; }
1187
+
1188
+ @keyframes rowSlideIn {
1189
+ from {
1190
+ opacity: 0;
1191
+ transform: translateX(-20px);
1192
+ }
1193
+ to {
1194
+ opacity: 1;
1195
+ transform: translateX(0);
1196
+ }
1197
+ }
1198
+
1199
+ /* Enhanced Hover Effects */
1200
+ tr {
1201
+ transition: all 0.2s ease;
1202
+ cursor: pointer;
1203
+ }
1204
+
1205
+ tr:hover {
1206
+ background: rgba(59, 130, 246, 0.1) !important;
1207
+ transform: translateX(5px);
1208
+ box-shadow: -5px 0 0 var(--accent-blue);
1209
+ }
1210
+
1211
+ /* Search Bar */
1212
+ .search-container {
1213
+ position: relative;
1214
+ margin-bottom: 20px;
1215
+ }
1216
+
1217
+ .search-input {
1218
+ width: 100%;
1219
+ padding: 14px 20px 14px 50px;
1220
+ background: rgba(17, 24, 39, 0.8);
1221
+ border: 2px solid var(--border);
1222
+ border-radius: 12px;
1223
+ color: var(--text-primary);
1224
+ font-size: 14px;
1225
+ transition: all 0.3s ease;
1226
+ }
1227
+
1228
+ .search-input:focus {
1229
+ outline: none;
1230
+ border-color: var(--accent-blue);
1231
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
1232
+ background: rgba(17, 24, 39, 0.95);
1233
+ }
1234
+
1235
+ .search-icon {
1236
+ position: absolute;
1237
+ left: 18px;
1238
+ top: 50%;
1239
+ transform: translateY(-50%);
1240
+ color: var(--text-secondary);
1241
+ font-size: 18px;
1242
+ pointer-events: none;
1243
+ }
1244
+
1245
+ /* Filter Chips */
1246
+ .filter-chips {
1247
+ display: flex;
1248
+ gap: 10px;
1249
+ flex-wrap: wrap;
1250
+ margin-bottom: 20px;
1251
+ }
1252
+
1253
+ .filter-chip {
1254
+ padding: 8px 16px;
1255
+ background: rgba(17, 24, 39, 0.6);
1256
+ border: 1px solid var(--border);
1257
+ border-radius: 20px;
1258
+ color: var(--text-secondary);
1259
+ font-size: 13px;
1260
+ font-weight: 600;
1261
+ cursor: pointer;
1262
+ transition: all 0.3s ease;
1263
+ }
1264
+
1265
+ .filter-chip:hover {
1266
+ border-color: var(--accent-blue);
1267
+ color: var(--accent-blue);
1268
+ transform: translateY(-2px);
1269
+ }
1270
+
1271
+ .filter-chip.active {
1272
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
1273
+ border-color: transparent;
1274
+ color: white;
1275
+ box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
1276
+ }
1277
+
1278
+ /* Enhanced Toast Notifications */
1279
+ .toast-container {
1280
+ position: fixed;
1281
+ top: 20px;
1282
+ right: 20px;
1283
+ z-index: 10000;
1284
+ display: flex;
1285
+ flex-direction: column;
1286
+ gap: 12px;
1287
+ max-width: 400px;
1288
+ }
1289
+
1290
+ .toast {
1291
+ position: relative;
1292
+ padding: 16px 20px;
1293
+ background: var(--bg-card);
1294
+ border: 1px solid var(--border);
1295
+ border-radius: 12px;
1296
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
1297
+ display: flex;
1298
+ align-items: center;
1299
+ gap: 12px;
1300
+ min-width: 300px;
1301
+ animation: toastSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
1302
+ backdrop-filter: blur(20px);
1303
+ }
1304
+
1305
+ @keyframes toastSlideIn {
1306
+ from {
1307
+ opacity: 0;
1308
+ transform: translateX(400px) scale(0.8);
1309
+ }
1310
+ to {
1311
+ opacity: 1;
1312
+ transform: translateX(0) scale(1);
1313
+ }
1314
+ }
1315
+
1316
+ .toast-icon {
1317
+ font-size: 24px;
1318
+ flex-shrink: 0;
1319
+ }
1320
+
1321
+ .toast-content {
1322
+ flex: 1;
1323
+ }
1324
+
1325
+ .toast-title {
1326
+ font-weight: 700;
1327
+ font-size: 14px;
1328
+ margin-bottom: 4px;
1329
+ }
1330
+
1331
+ .toast-message {
1332
+ font-size: 13px;
1333
+ color: var(--text-secondary);
1334
+ }
1335
+
1336
+ .toast-close {
1337
+ background: none;
1338
+ border: none;
1339
+ color: var(--text-secondary);
1340
+ font-size: 20px;
1341
+ cursor: pointer;
1342
+ padding: 0;
1343
+ width: 24px;
1344
+ height: 24px;
1345
+ display: flex;
1346
+ align-items: center;
1347
+ justify-content: center;
1348
+ border-radius: 6px;
1349
+ transition: all 0.2s ease;
1350
+ }
1351
+
1352
+ .toast-close:hover {
1353
+ background: rgba(255, 255, 255, 0.1);
1354
+ color: var(--text-primary);
1355
+ }
1356
+
1357
+ /* Progress Indicator */
1358
+ .progress-indicator {
1359
+ position: fixed;
1360
+ top: 0;
1361
+ left: 0;
1362
+ width: 100%;
1363
+ height: 3px;
1364
+ background: rgba(255, 255, 255, 0.1);
1365
+ z-index: 10001;
1366
+ overflow: hidden;
1367
+ }
1368
+
1369
+ .progress-bar {
1370
+ height: 100%;
1371
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
1372
+ width: 0%;
1373
+ transition: width 0.3s ease;
1374
+ animation: progress-shimmer 2s infinite;
1375
+ }
1376
+
1377
+ @keyframes progress-shimmer {
1378
+ 0% { background-position: -200% 0; }
1379
+ 100% { background-position: 200% 0; }
1380
+ }
1381
+
1382
+ /* Floating Action Button */
1383
+ .fab {
1384
+ position: fixed;
1385
+ bottom: 30px;
1386
+ right: 30px;
1387
+ width: 60px;
1388
+ height: 60px;
1389
+ border-radius: 50%;
1390
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
1391
+ border: none;
1392
+ color: white;
1393
+ font-size: 24px;
1394
+ cursor: pointer;
1395
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4);
1396
+ transition: all 0.3s ease;
1397
+ z-index: 1000;
1398
+ display: flex;
1399
+ align-items: center;
1400
+ justify-content: center;
1401
+ }
1402
+
1403
+ .fab:hover {
1404
+ transform: scale(1.1) rotate(90deg);
1405
+ box-shadow: 0 15px 40px rgba(59, 130, 246, 0.6);
1406
+ }
1407
+
1408
+ .fab:active {
1409
+ transform: scale(0.95);
1410
+ }
1411
+
1412
+ /* Success/Error Feedback */
1413
+ .feedback-overlay {
1414
+ position: fixed;
1415
+ top: 0;
1416
+ left: 0;
1417
+ width: 100%;
1418
+ height: 100%;
1419
+ background: rgba(0, 0, 0, 0.7);
1420
+ backdrop-filter: blur(5px);
1421
+ z-index: 10002;
1422
+ display: flex;
1423
+ align-items: center;
1424
+ justify-content: center;
1425
+ opacity: 0;
1426
+ pointer-events: none;
1427
+ transition: opacity 0.3s ease;
1428
+ }
1429
+
1430
+ .feedback-overlay.show {
1431
+ opacity: 1;
1432
+ pointer-events: all;
1433
+ }
1434
+
1435
+ .feedback-card {
1436
+ background: var(--bg-card);
1437
+ border-radius: 20px;
1438
+ padding: 40px;
1439
+ text-align: center;
1440
+ max-width: 400px;
1441
+ border: 2px solid var(--border);
1442
+ transform: scale(0.8);
1443
+ transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
1444
+ }
1445
+
1446
+ .feedback-overlay.show .feedback-card {
1447
+ transform: scale(1);
1448
+ }
1449
+
1450
+ .feedback-icon {
1451
+ font-size: 64px;
1452
+ margin-bottom: 20px;
1453
+ animation: feedbackBounce 0.6s ease;
1454
+ }
1455
+
1456
+ @keyframes feedbackBounce {
1457
+ 0%, 100% { transform: scale(1); }
1458
+ 50% { transform: scale(1.2); }
1459
+ }
1460
+
1461
+ .feedback-title {
1462
+ font-size: 24px;
1463
+ font-weight: 800;
1464
+ margin-bottom: 10px;
1465
+ }
1466
+
1467
+ .feedback-message {
1468
+ color: var(--text-secondary);
1469
+ margin-bottom: 30px;
1470
+ }
1471
+
1472
+ /* Pulse Animation for Live Data */
1473
+ .pulse-data {
1474
+ animation: pulseGlow 2s ease-in-out infinite;
1475
+ }
1476
+
1477
+ @keyframes pulseGlow {
1478
+ 0%, 100% {
1479
+ box-shadow: 0 0 5px rgba(59, 130, 246, 0.5);
1480
+ }
1481
+ 50% {
1482
+ box-shadow: 0 0 20px rgba(59, 130, 246, 0.8), 0 0 30px rgba(59, 130, 246, 0.4);
1483
+ }
1484
+ }
1485
+
1486
+ /* Smooth Scroll */
1487
+ html {
1488
+ scroll-behavior: smooth;
1489
+ }
1490
+
1491
+ /* Enhanced Focus States */
1492
+ *:focus-visible {
1493
+ outline: 2px solid var(--accent-blue);
1494
+ outline-offset: 2px;
1495
+ border-radius: 4px;
1496
+ }
1497
+
1498
+ /* Loading Overlay */
1499
+ .loading-overlay {
1500
+ position: fixed;
1501
+ top: 0;
1502
+ left: 0;
1503
+ width: 100%;
1504
+ height: 100%;
1505
+ background: rgba(10, 14, 26, 0.9);
1506
+ backdrop-filter: blur(10px);
1507
+ z-index: 10003;
1508
+ display: flex;
1509
+ flex-direction: column;
1510
+ align-items: center;
1511
+ justify-content: center;
1512
+ gap: 20px;
1513
+ opacity: 0;
1514
+ pointer-events: none;
1515
+ transition: opacity 0.3s ease;
1516
+ }
1517
+
1518
+ .loading-overlay.show {
1519
+ opacity: 1;
1520
+ pointer-events: all;
1521
+ }
1522
+
1523
+ .loading-spinner-large {
1524
+ width: 80px;
1525
+ height: 80px;
1526
+ border: 6px solid var(--border);
1527
+ border-top-color: var(--accent-blue);
1528
+ border-radius: 50%;
1529
+ animation: spin 1s linear infinite;
1530
+ }
1531
+
1532
+ .loading-text {
1533
+ font-size: 18px;
1534
+ font-weight: 600;
1535
+ color: var(--text-primary);
1536
+ }
1537
+
1538
+ /* Tooltip */
1539
+ .tooltip {
1540
+ position: relative;
1541
+ cursor: help;
1542
+ }
1543
+
1544
+ .tooltip::before {
1545
+ content: attr(data-tooltip);
1546
+ position: absolute;
1547
+ bottom: 100%;
1548
+ left: 50%;
1549
+ transform: translateX(-50%) translateY(-10px);
1550
+ padding: 8px 12px;
1551
+ background: var(--bg-card);
1552
+ border: 1px solid var(--border);
1553
+ border-radius: 8px;
1554
+ font-size: 12px;
1555
+ white-space: nowrap;
1556
+ opacity: 0;
1557
+ pointer-events: none;
1558
+ transition: all 0.3s ease;
1559
+ z-index: 1000;
1560
+ }
1561
+
1562
+ .tooltip:hover::before {
1563
+ opacity: 1;
1564
+ transform: translateX(-50%) translateY(-5px);
1565
+ }
1566
+
1567
+ /* Gradient Text Animation */
1568
+ .gradient-text {
1569
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
1570
+ background-size: 200% 200%;
1571
+ -webkit-background-clip: text;
1572
+ -webkit-text-fill-color: transparent;
1573
+ background-clip: text;
1574
+ animation: gradientShift 3s ease infinite;
1575
+ }
1576
+
1577
+ @keyframes gradientShift {
1578
+ 0%, 100% { background-position: 0% 50%; }
1579
+ 50% { background-position: 100% 50%; }
1580
+ }
1581
+
1582
+ /* Badge Pulse */
1583
+ .badge-pulse {
1584
+ animation: badgePulse 2s ease-in-out infinite;
1585
+ }
1586
+
1587
+ @keyframes badgePulse {
1588
+ 0%, 100% { transform: scale(1); }
1589
+ 50% { transform: scale(1.1); }
1590
+ }
1591
+
1592
+ /* Smooth Transitions for All Interactive Elements */
1593
+ button, a, input, select, textarea {
1594
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1595
+ }
1596
+
1597
+ /* Enhanced Table Styling */
1598
+ table {
1599
+ border-collapse: separate;
1600
+ border-spacing: 0;
1601
+ }
1602
+
1603
+ thead th:first-child {
1604
+ border-top-left-radius: 12px;
1605
+ }
1606
+
1607
+ thead th:last-child {
1608
+ border-top-right-radius: 12px;
1609
+ }
1610
+
1611
+ tbody tr:last-child td:first-child {
1612
+ border-bottom-left-radius: 12px;
1613
+ }
1614
+
1615
+ tbody tr:last-child td:last-child {
1616
+ border-bottom-right-radius: 12px;
1617
+ }
1618
  </style>
1619
  </head>
1620
 
1621
  <body>
1622
+ <!-- Progress Indicator -->
1623
+ <div class="progress-indicator" id="progressIndicator">
1624
+ <div class="progress-bar" id="progressBar"></div>
1625
+ </div>
1626
+
1627
+ <!-- Toast Container -->
1628
+ <div class="toast-container" id="toastContainer"></div>
1629
+
1630
+ <!-- Loading Overlay -->
1631
+ <div class="loading-overlay" id="loadingOverlay">
1632
+ <div class="loading-spinner-large"></div>
1633
+ <div class="loading-text" id="loadingText">در حال بارگذاری...</div>
1634
+ </div>
1635
+
1636
+ <!-- Feedback Overlay -->
1637
+ <div class="feedback-overlay" id="feedbackOverlay">
1638
+ <div class="feedback-card">
1639
+ <div class="feedback-icon" id="feedbackIcon">✅</div>
1640
+ <div class="feedback-title" id="feedbackTitle">موفق!</div>
1641
+ <div class="feedback-message" id="feedbackMessage">عملیات با موفقیت انجام شد</div>
1642
+ <button class="refresh-btn ripple" onclick="hideFeedback()">بستن</button>
1643
+ </div>
1644
+ </div>
1645
+
1646
+ <!-- Floating Action Button -->
1647
+ <button class="fab ripple" onclick="scrollToTop()" title="بازگشت به بالا">
1648
+
1649
+ </button>
1650
+
1651
  <!-- WebSocket Status Indicator -->
1652
  <div id="ws-connection-status" class="ws-status-indicator disconnected">
1653
  <div id="ws-status-dot" class="status-dot status-dot-offline"></div>
1654
  <span id="ws-status-text" class="ws-status-text">در حال اتصال...</span>
1655
+ <div id="online-users-badge" class="badge badge-info badge-pulse" style="margin-left: 10px;">0</div>
1656
  </div>
1657
 
1658
  <div class="container">
 
1789
  <!-- Market Table -->
1790
  <div class="market-section">
1791
  <div class="section-header">
1792
+ <div class="section-title gradient-text">💎 Live Market Data</div>
1793
+ <button class="refresh-btn ripple" onclick="loadMarketData()" data-tooltip="به‌روزرسانی داده‌های بازار">↻ Refresh</button>
1794
+ </div>
1795
+
1796
+ <!-- Search Bar -->
1797
+ <div class="search-container">
1798
+ <span class="search-icon">🔍</span>
1799
+ <input type="text" class="search-input" id="marketSearch" placeholder="جستجوی ارز دیجیتال (مثال: Bitcoin, BTC, Ethereum)..." oninput="filterMarketTable()">
1800
+ </div>
1801
+
1802
+ <!-- Filter Chips -->
1803
+ <div class="filter-chips">
1804
+ <button class="filter-chip active" onclick="filterByCategory('all')">همه</button>
1805
+ <button class="filter-chip" onclick="filterByCategory('top10')">Top 10</button>
1806
+ <button class="filter-chip" onclick="filterByCategory('gainers')">📈 در حال رشد</button>
1807
+ <button class="filter-chip" onclick="filterByCategory('losers')">📉 در حال سقوط</button>
1808
+ <button class="filter-chip" onclick="filterByCategory('volume')">💹 حجم بالا</button>
1809
  </div>
1810
  <div style="overflow-x: auto;">
1811
  <table id="marketTable">
 
1934
  <div class="market-section">
1935
  <div class="section-header">
1936
  <div class="section-title">📊 API Providers Status</div>
1937
+ <button class="refresh-btn ripple" onclick="loadMonitorData()">↻ Refresh</button>
1938
  </div>
1939
  <div style="overflow-x: auto;">
1940
  <table>
 
1965
  ETH looks weak
1966
  Market is bullish today</textarea>
1967
  </div>
1968
+ <button class="refresh-btn ripple" onclick="runSentiment()">🧠 Analyze Sentiment</button>
1969
  <div id="sentimentResult"
1970
  style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;">
1971
  —</div>
 
2605
  // Market Data Functions
2606
  async function loadMarketData() {
2607
  try {
2608
+ // Show loading state
2609
+ const marketTableBody = document.getElementById('marketTableBody');
2610
+ if (marketTableBody) {
2611
+ marketTableBody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px;"><div class="loading"><div class="spinner"></div></div><div style="margin-top: 10px; color: var(--text-secondary);">در حال بارگذاری داده‌های بازار...</div></td></tr>';
2612
+ }
2613
+
2614
+ showProgress(60);
2615
+
2616
+ const [marketRes, statsRes, sentimentRes, trendingRes, defiRes] = await Promise.all([
2617
+ fetch('/api/market'),
2618
+ fetch('/api/stats'),
2619
+ fetch('/api/sentiment'),
2620
+ fetch('/api/trending'),
2621
+ fetch('/api/defi')
2622
+ ]);
2623
+
2624
+ showProgress(80);
2625
+
2626
+ // Check if responses are OK
2627
+ if (!marketRes.ok) throw new Error(`خطا در دریافت داده‌های بازار: ${marketRes.status}`);
2628
+ if (!statsRes.ok) throw new Error(`خطا در دریافت آمار: ${statsRes.status}`);
2629
+ if (!sentimentRes.ok) throw new Error(`خطا در دریافت احساسات: ${sentimentRes.status}`);
2630
+ if (!trendingRes.ok) throw new Error(`خطا در دریافت ترندها: ${trendingRes.status}`);
2631
+ if (!defiRes.ok) throw new Error(`خطا در دریافت DeFi: ${defiRes.status}`);
2632
+
2633
  const [market, stats, sentiment, trending, defi] = await Promise.all([
2634
+ marketRes.json(),
2635
+ statsRes.json(),
2636
+ sentimentRes.json(),
2637
+ trendingRes.json(),
2638
+ defiRes.json()
2639
  ]);
2640
 
2641
+ // Validate data with more detailed checks
2642
+ if (!market || !Array.isArray(market.cryptocurrencies)) {
2643
+ throw new Error('داده‌های بازار نامعتبر است: cryptocurrencies array not found');
2644
+ }
2645
+ if (!stats || typeof stats !== 'object' || stats === null) {
2646
+ console.error('Invalid stats:', stats);
2647
+ throw new Error('آمار نامعتبر است: stats object not found');
2648
+ }
2649
+ // Check if stats.market exists and is an object
2650
+ if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) {
2651
+ console.error('Invalid stats.market:', stats.market);
2652
+ console.error('Full stats object:', JSON.stringify(stats, null, 2));
2653
+ throw new Error('آمار نامعتبر است: stats.market object not found');
2654
+ }
2655
+ if (!sentiment || typeof sentiment !== 'object' || sentiment === null) {
2656
+ throw new Error('داده‌های احساسات نامعتبر است: sentiment object not found');
2657
+ }
2658
+ // Note: sentiment can have different structures:
2659
+ // - sentiment.fear_greed_index (from /api/sentiment)
2660
+ // - sentiment.fear_greed_value (from /api/stats)
2661
+ // So we don't validate the exact structure here
2662
+ if (!trending || !Array.isArray(trending.trending)) {
2663
+ throw new Error('داده‌های ترند نامعتبر است: trending array not found');
2664
+ }
2665
+ if (!defi || typeof defi !== 'object' || defi === null) {
2666
+ throw new Error('داده‌های DeFi نامعتبر است: defi object not found');
2667
+ }
2668
+
2669
+ // Call updateStats with validated data - double check before calling
2670
+ if (stats && stats.market && typeof stats.market === 'object' && !Array.isArray(stats.market)) {
2671
+ updateStats(stats, sentiment);
2672
+ } else {
2673
+ console.error('Failed final validation before updateStats:', { stats, sentiment });
2674
+ throw new Error('داده‌های stats.market نامعتبر است');
2675
+ }
2676
  updateMarketTable(market.cryptocurrencies);
2677
  updateTrending(trending.trending);
2678
  updateDeFi(defi);
2679
  updateCharts(market, sentiment);
2680
  } catch (error) {
2681
  console.error('Error loading market data:', error);
2682
+ const marketTableBody = document.getElementById('marketTableBody');
2683
+ if (marketTableBody) {
2684
+ marketTableBody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">
2685
+ <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
2686
+ <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
2687
+ <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'خطای نامشخص'}</div>
2688
+ <button onclick="loadMarketData()" style="margin-top: 15px; padding: 10px 20px; background: var(--accent-blue); border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: 600;">تلاش مجدد</button>
2689
+ </td></tr>`;
2690
+ }
2691
+ showToast('❌ خطا در بارگذاری داده‌های بازار: ' + (error.message || 'خطای نامشخص'), 'error');
2692
  }
2693
  }
2694
 
2695
  function updateStats(stats, sentiment) {
2696
+ try {
2697
+ // More robust validation with detailed checks
2698
+ if (!stats || typeof stats !== 'object' || stats === null) {
2699
+ console.warn('updateStats: stats is undefined, null, or not an object', stats);
2700
+ return;
2701
+ }
2702
+ if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) {
2703
+ console.warn('updateStats: stats.market is invalid', { stats, market: stats.market });
2704
+ return;
2705
+ }
2706
+ if (!sentiment || typeof sentiment !== 'object' || sentiment === null) {
2707
+ console.warn('updateStats: sentiment is undefined, null, or not an object', sentiment);
2708
+ return;
2709
+ }
2710
+
2711
+ // Use safe property access for market data with additional checks
2712
+ const marketObj = stats.market;
2713
+ if (!marketObj || typeof marketObj !== 'object' || marketObj === null) {
2714
+ console.warn('updateStats: marketObj is invalid', marketObj);
2715
+ return;
2716
+ }
2717
+
2718
+ const mcap = (typeof marketObj.total_market_cap !== 'undefined' && marketObj.total_market_cap !== null) ? marketObj.total_market_cap : 0;
2719
+ const totalMarketCapEl = document.getElementById('totalMarketCap');
2720
+ if (totalMarketCapEl) {
2721
+ totalMarketCapEl.textContent = '$' + (mcap / 1e12).toFixed(2) + 'T';
2722
+ }
2723
 
2724
+ const volume = (typeof marketObj.total_volume !== 'undefined' && marketObj.total_volume !== null) ? marketObj.total_volume : 0;
2725
+ const totalVolumeEl = document.getElementById('totalVolume');
2726
+ if (totalVolumeEl) {
2727
+ totalVolumeEl.textContent = '$' + (volume / 1e9).toFixed(2) + 'B';
2728
+ }
2729
 
2730
+ const btcDom = (typeof marketObj.btc_dominance !== 'undefined' && marketObj.btc_dominance !== null) ? marketObj.btc_dominance : 0;
2731
+ const btcDominanceEl = document.getElementById('btcDominance');
2732
+ if (btcDominanceEl) {
2733
+ btcDominanceEl.textContent = btcDom.toFixed(1) + '%';
2734
+ }
2735
 
2736
+ // Handle sentiment data - support both structures:
2737
+ // 1. sentiment.fear_greed_index.value (from /api/sentiment)
2738
+ // 2. sentiment.fear_greed_value (from /api/stats)
2739
+ let fg = 50;
2740
+ let classification = 'Neutral';
2741
+
2742
+ if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') {
2743
+ // Structure from /api/sentiment endpoint
2744
+ fg = (typeof sentiment.fear_greed_index.value !== 'undefined') ? sentiment.fear_greed_index.value : 50;
2745
+ classification = sentiment.fear_greed_index.classification || 'Neutral';
2746
+ } else if (typeof sentiment.fear_greed_value !== 'undefined') {
2747
+ // Structure from /api/stats endpoint
2748
+ fg = sentiment.fear_greed_value;
2749
+ classification = sentiment.classification || 'Neutral';
2750
+ } else if (typeof sentiment.value !== 'undefined') {
2751
+ // Fallback structure
2752
+ fg = sentiment.value;
2753
+ classification = sentiment.classification || 'Neutral';
2754
+ }
2755
 
2756
+ const fearGreedEl = document.getElementById('fearGreed');
2757
+ if (fearGreedEl) {
2758
+ fearGreedEl.textContent = fg;
2759
+ }
2760
+ const sentimentLabelEl = document.getElementById('sentimentLabel');
2761
+ if (sentimentLabelEl) {
2762
+ sentimentLabelEl.innerHTML = `<span>${classification}</span>`;
2763
+
2764
+ if (fg < 25) {
2765
+ sentimentLabelEl.style.color = 'var(--accent-red)';
2766
+ } else if (fg < 45) {
2767
+ sentimentLabelEl.style.color = 'var(--accent-yellow)';
2768
+ } else if (fg < 55) {
2769
+ sentimentLabelEl.style.color = 'var(--text-secondary)';
2770
+ } else if (fg < 75) {
2771
+ sentimentLabelEl.style.color = 'var(--accent-blue)';
2772
+ } else {
2773
+ sentimentLabelEl.style.color = 'var(--accent-green)';
2774
+ }
2775
+ }
2776
+ } catch (error) {
2777
+ console.error('Error updating stats:', error);
2778
+ console.error('Stats object:', stats);
2779
+ console.error('Sentiment object:', sentiment);
2780
  }
2781
  }
2782
 
2783
  function updateMarketTable(cryptos) {
2784
+ try {
2785
+ if (!cryptos || !Array.isArray(cryptos) || cryptos.length === 0) {
2786
+ const tbody = document.getElementById('marketTableBody');
2787
+ if (tbody) {
2788
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ داده‌ای یافت نشد</td></tr>';
2789
+ }
2790
+ return;
2791
+ }
2792
+
2793
+ const tbody = document.getElementById('marketTableBody');
2794
+ if (!tbody) return;
2795
+
2796
+ // Store data for filtering
2797
+ marketDataCache = cryptos;
2798
+
2799
+ tbody.innerHTML = cryptos.map((crypto, index) => {
2800
+ const price = crypto.price || 0;
2801
+ const change24h = crypto.change_24h || 0;
2802
+ const marketCap = crypto.market_cap || 0;
2803
+ const volume24h = crypto.volume_24h || 0;
2804
+ const symbol = crypto.symbol || 'N/A';
2805
+ const name = crypto.name || 'نامشخص';
2806
+ const changeClass = change24h >= 0 ? 'positive' : 'negative';
2807
+ const changeIcon = change24h >= 0 ? '📈' : '📉';
2808
+
2809
+ return `
2810
+ <tr data-name="${name.toLowerCase()}" data-symbol="${symbol.toLowerCase()}" data-change="${change24h}" data-rank="${crypto.rank || index + 1}">
2811
+ <td style="font-weight: 700; color: var(--text-secondary);">${crypto.rank || index + 1}</td>
2812
+ <td>
2813
+ <div class="crypto-name">
2814
+ ${crypto.image ? `<img src="${crypto.image}" class="crypto-img" alt="${symbol}" onerror="this.style.display='none'">` :
2815
+ `<div class="crypto-img" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; font-weight: 700; color: white;">${symbol[0] || '?'}</div>`}
2816
+ <div>
2817
+ <div style="font-weight: 600;">${name}</div>
2818
+ <div class="crypto-symbol">${symbol}</div>
2819
+ </div>
2820
+ </div>
2821
+ </td>
2822
+ <td class="price number-counter">$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}</td>
2823
+ <td><span class="change ${changeClass} pulse-data">${changeIcon} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}%</span></td>
2824
+ <td style="font-weight: 600;">$${(marketCap / 1e9).toFixed(2)}B</td>
2825
+ <td style="color: var(--text-secondary);">$${(volume24h / 1e9).toFixed(2)}B</td>
2826
+ </tr>
2827
+ `;
2828
+ }).join('');
2829
+ } catch (error) {
2830
+ console.error('Error updating market table:', error);
2831
+ const tbody = document.getElementById('marketTableBody');
2832
+ if (tbody) {
2833
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش داده‌ها</td></tr>';
2834
+ }
2835
+ }
2836
  }
2837
 
2838
  function updateTrending(trending) {
2839
+ try {
2840
+ const grid = document.getElementById('trendingGrid');
2841
+ if (!grid) return;
 
 
 
 
 
 
 
 
 
2842
 
2843
+ if (!trending || !Array.isArray(trending) || trending.length === 0) {
2844
+ grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ ترندی یافت نشد</div>';
2845
+ return;
2846
+ }
2847
 
2848
+ grid.innerHTML = trending.map((coin, index) => {
2849
+ const name = coin.name || 'نامشخص';
2850
+ const symbol = coin.symbol || 'N/A';
2851
+ return `
2852
+ <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; padding: 15px; display: flex; align-items: center; gap: 12px;">
2853
+ <div style="font-size: 20px; font-weight: 900; color: var(--accent-yellow);">#${index + 1}</div>
2854
+ ${coin.thumb ? `<img src="${coin.thumb}" style="width: 32px; height: 32px; border-radius: 8px;" onerror="this.style.display='none'">` : ''}
 
2855
  <div>
2856
+ <div style="font-weight: 600;">${name}</div>
2857
+ <div style="font-size: 12px; color: var(--text-secondary);">${symbol}</div>
 
 
 
 
 
 
2858
  </div>
2859
  </div>
2860
+ `;
2861
+ }).join('');
2862
+ } catch (error) {
2863
+ console.error('Error updating trending:', error);
2864
+ const grid = document.getElementById('trendingGrid');
2865
+ if (grid) {
2866
+ grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش ترندها</div>';
2867
+ }
2868
+ }
2869
+ }
2870
+
2871
+ function updateDeFi(defi) {
2872
+ try {
2873
+ const list = document.getElementById('defiList');
2874
+ if (!list) return;
2875
+
2876
+ const protocols = defi && defi.protocols ? defi.protocols : [];
2877
+ const totalTvl = defi && defi.total_tvl ? defi.total_tvl : 0;
2878
+
2879
+ list.innerHTML = `
2880
+ <div class="stat-card" style="margin-bottom: 20px; text-align: center; background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));">
2881
+ <div class="stat-value gradient-text" style="font-size: 42px; margin-bottom: 8px;">$${(totalTvl / 1e9).toFixed(2)}B</div>
2882
+ <div class="stat-label" style="font-size: 16px;">Total Value Locked</div>
2883
+ </div>
2884
+ <div style="display: grid; gap: 12px;">
2885
+ ${protocols.length > 0 ? protocols.map((p, i) => {
2886
+ const name = p.name || 'نامشخص';
2887
+ const chain = p.chain || 'N/A';
2888
+ const tvl = p.tvl || 0;
2889
+ const change24h = p.change_24h || 0;
2890
+ const changeClass = change24h >= 0 ? 'positive' : 'negative';
2891
+ return `
2892
+ <div class="stat-card" style="animation-delay: ${i * 0.05}s; cursor: pointer;" onclick="showToast('${name}: $${(tvl / 1e9).toFixed(2)}B TVL', 'info', 'DeFi Protocol')">
2893
+ <div style="display: flex; justify-content: space-between; align-items: center;">
2894
+ <div>
2895
+ <div style="font-weight: 700; font-size: 16px; margin-bottom: 4px;">${i + 1}. ${name}</div>
2896
+ <div style="font-size: 12px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px;">
2897
+ <span>🔗</span> <span>${chain}</span>
2898
+ </div>
2899
+ </div>
2900
+ <div style="text-align: right;">
2901
+ <div class="stat-value" style="font-size: 18px; margin-bottom: 4px;">$${(tvl / 1e9).toFixed(2)}B</div>
2902
+ <div class="stat-change ${changeClass}" style="font-size: 13px;">
2903
+ ${change24h >= 0 ? '📈' : '📉'} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}%
2904
+ </div>
2905
+ </div>
2906
+ </div>
2907
+ </div>
2908
+ `;
2909
+ }).join('') : '<div class="empty-state"><div class="empty-state-icon">📦</div><div>هیچ پروتکلی یافت نشد</div></div>'}
2910
+ </div>
2911
+ `;
2912
+ } catch (error) {
2913
+ console.error('Error updating DeFi:', error);
2914
+ const list = document.getElementById('defiList');
2915
+ if (list) {
2916
+ list.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش داده‌های DeFi</div>';
2917
+ }
2918
+ }
2919
  }
2920
 
2921
  function initCharts() {
 
2985
  }
2986
 
2987
  // استفاده از WebSocket Client جدید
2988
+ let wsConnectAttempts = 0;
2989
+ const MAX_WS_CONNECT_ATTEMPTS = 10; // حداکثر 10 تلاش (10 ثانیه)
2990
+ let wsStatsInterval = null;
2991
+
2992
  function connectWebSocket() {
2993
  // WebSocket client از websocket-client.js استفاده می‌شود
2994
  // که به صورت خودکار اتصال برقرار می‌کند
2995
 
2996
+ // بررسی وجود wsClient و متدهای مورد نیاز
2997
+ if (window.wsClient && typeof window.wsClient.on === 'function' && typeof window.wsClient.requestStats === 'function') {
2998
  console.log('✅ WebSocket Client آماده است');
2999
+ wsConnectAttempts = 0; // Reset counter on success
3000
 
3001
  // ثبت handler برای به‌روزرسانی آمار
3002
  window.wsClient.on('stats_update', (message) => {
3003
  console.log('📊 Stats update:', message.data);
3004
+ if (typeof updateOnlineStats === 'function') {
3005
+ updateOnlineStats(message.data);
3006
+ }
3007
  });
3008
 
3009
  window.wsClient.on('provider_stats', (message) => {
3010
  console.log('📡 Provider stats:', message.data);
3011
+ if (currentTab === 'monitor' && typeof updateProviderStatsDisplay === 'function') {
3012
  updateProviderStatsDisplay(message.data);
3013
  }
3014
  });
 
3022
 
3023
  // درخواست آمار اولیه
3024
  setTimeout(() => {
3025
+ if (window.wsClient && window.wsClient.isConnected) {
3026
  window.wsClient.requestStats();
3027
  }
3028
  }, 1000);
3029
 
3030
+ // درخواست آمار هر 10 ثانیه (فقط یک بار تنظیم شود)
3031
+ if (!wsStatsInterval) {
3032
+ wsStatsInterval = setInterval(() => {
3033
+ if (window.wsClient && window.wsClient.isConnected) {
3034
+ window.wsClient.requestStats();
3035
+ }
3036
+ }, 10000);
3037
+ }
3038
  } else {
3039
+ wsConnectAttempts++;
3040
+ if (wsConnectAttempts < MAX_WS_CONNECT_ATTEMPTS) {
3041
+ // فقط هر 5 ثانیه یک بار لاگ کنیم تا console پر نشود
3042
+ if (wsConnectAttempts % 5 === 0 || wsConnectAttempts === 1) {
3043
+ console.log(`⏳ در انتظار WebSocket Client... (${wsConnectAttempts}/${MAX_WS_CONNECT_ATTEMPTS})`);
3044
+ }
3045
+ setTimeout(connectWebSocket, 1000);
3046
+ } else {
3047
+ console.warn('⚠️ WebSocket Client پس از ' + MAX_WS_CONNECT_ATTEMPTS + ' تلاش آماده نشد. ممکن است فایل websocket-client.js لود نشده باشد یا WebSocket پشتیبانی نشود.');
3048
+ console.warn('⚠️ بررسی کنید که فایل /static/js/websocket-client.js به درستی لود شده باشد.');
3049
+ // تلاش نهایی بعد از 5 ثانیه
3050
+ setTimeout(() => {
3051
+ if (!window.wsClient) {
3052
+ console.warn('⚠️ WebSocket Client غیرفعال است. برخی ویژگی‌های real-time ممکن است کار نکنند.');
3053
+ console.warn('⚠️ برای فعال کردن WebSocket، صفحه را refresh کنید (Ctrl+F5 برای clear cache).');
3054
+ }
3055
+ }, 5000);
3056
+ // متوقف کردن تلاش‌های بیشتر
3057
+ return;
3058
+ }
3059
  }
3060
  }
3061
 
 
3095
  // Monitor Functions
3096
  async function loadMonitorData() {
3097
  try {
3098
+ // Show loading state
3099
+ const tbody = document.getElementById('providersTable');
3100
+ if (tbody) {
3101
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;"><div class="loading"><div class="spinner"></div></div><div style="margin-top: 10px; color: var(--text-secondary);">در حال بارگذاری وضعیت APIها...</div></td></tr>';
3102
+ }
3103
+
3104
+ const [statusRes, providersRes] = await Promise.all([
3105
+ fetch('/api/status'),
3106
+ fetch('/api/providers')
3107
+ ]);
3108
+
3109
+ // Check if responses are OK
3110
+ if (!statusRes.ok) throw new Error(`خطا در دریافت وضعیت: ${statusRes.status}`);
3111
+ if (!providersRes.ok) throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status}`);
3112
+
3113
  const [status, providers] = await Promise.all([
3114
+ statusRes.json(),
3115
+ providersRes.json()
3116
  ]);
3117
 
3118
+ // Validate data
3119
+ if (!status || typeof status.total_providers === 'undefined') throw new Error('داده‌های وضعیت نامعتبر است');
3120
+ if (!providers || !Array.isArray(providers)) throw new Error('لیست APIها نامعتبر است');
 
3121
 
3122
+ if (document.getElementById('totalAPIs')) {
3123
+ document.getElementById('totalAPIs').textContent = status.total_providers || 0;
3124
+ }
3125
+ if (document.getElementById('onlineAPIs')) {
3126
+ document.getElementById('onlineAPIs').textContent = status.online || 0;
3127
+ }
3128
+ if (document.getElementById('offlineAPIs')) {
3129
+ document.getElementById('offlineAPIs').textContent = status.offline || 0;
3130
+ }
3131
+ if (document.getElementById('avgResponse')) {
3132
+ document.getElementById('avgResponse').textContent = (status.avg_response_time_ms || 0) + 'ms';
3133
+ }
3134
 
3135
+ if (tbody) {
3136
+ if (providers.length === 0) {
3137
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ APIای یافت نشد</td></tr>';
3138
+ } else {
3139
+ tbody.innerHTML = providers.map(p => {
3140
+ let statusClass = 'badge-success';
3141
+ if (p.status === 'offline') statusClass = 'badge-danger';
3142
+ else if (p.status === 'degraded') statusClass = 'badge-warning';
3143
+
3144
+ return `
3145
+ <tr>
3146
+ <td><strong>${p.name || 'نامشخص'}</strong></td>
3147
+ <td><span class="badge badge-info">${p.category || 'نامشخص'}</span></td>
3148
+ <td><span class="badge ${statusClass}">${(p.status || 'unknown').toUpperCase()}</span></td>
3149
+ <td>${p.response_time_ms || p.avg_response_time_ms || 0}ms</td>
3150
+ <td style="color: var(--text-secondary); font-size: 13px;">${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString() : 'نامشخص'}</td>
3151
+ </tr>
3152
+ `;
3153
+ }).join('');
3154
+ }
3155
+ }
3156
  } catch (error) {
3157
  console.error('Error loading monitor data:', error);
3158
+ const tbody = document.getElementById('providersTable');
3159
+ if (tbody) {
3160
+ tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 40px; color: var(--accent-red);">
3161
+ <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
3162
+ <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
3163
+ <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'خطای نامشخص'}</div>
3164
+ <button onclick="loadMonitorData()" style="margin-top: 15px; padding: 10px 20px; background: var(--accent-blue); border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: 600;">تلاش مجدد</button>
3165
+ </td></tr>`;
3166
+ }
3167
+ showToast('❌ خطا در بارگذاری داده‌های مانیتور: ' + (error.message || 'خطای نامشخص'), 'error');
3168
  }
3169
  }
3170
 
 
3744
  }
3745
 
3746
  // Toast notification function
3747
+ // Enhanced Toast Notification System
3748
+ function showToast(message, type = 'info', title = null) {
3749
+ const toastContainer = document.getElementById('toastContainer') || document.body;
3750
  const toast = document.createElement('div');
3751
  toast.className = `toast toast-${type}`;
3752
+
3753
+ const icons = {
3754
+ success: '✅',
3755
+ error: '❌',
3756
+ warning: '⚠️',
3757
+ info: 'ℹ️'
3758
+ };
3759
+
3760
+ const titles = {
3761
+ success: 'موفق!',
3762
+ error: 'خطا!',
3763
+ warning: 'هشدار!',
3764
+ info: 'اطلاعیه'
3765
+ };
3766
+
3767
  toast.innerHTML = `
3768
+ <div class="toast-icon">${icons[type] || icons.info}</div>
3769
+ <div class="toast-content">
3770
+ <div class="toast-title">${title || titles[type] || titles.info}</div>
3771
+ <div class="toast-message">${message}</div>
3772
+ </div>
3773
+ <button class="toast-close" onclick="this.parentElement.remove()">×</button>
3774
  `;
3775
+
3776
+ toastContainer.appendChild(toast);
3777
+
3778
+ // Auto remove after 5 seconds
3779
  setTimeout(() => {
3780
+ toast.style.animation = 'toastSlideIn 0.3s reverse';
3781
  setTimeout(() => toast.remove(), 300);
3782
+ }, 5000);
3783
+
3784
+ // Add click to dismiss
3785
+ toast.addEventListener('click', (e) => {
3786
+ if (e.target.classList.contains('toast-close') || e.target === toast) {
3787
+ toast.style.animation = 'toastSlideIn 0.3s reverse';
3788
+ setTimeout(() => toast.remove(), 300);
3789
+ }
3790
+ });
3791
+ }
3792
+
3793
+ // Progress Indicator Functions
3794
+ function showProgress(percent = 0) {
3795
+ const progressBar = document.getElementById('progressBar');
3796
+ if (progressBar) {
3797
+ progressBar.style.width = percent + '%';
3798
+ }
3799
+ }
3800
+
3801
+ function hideProgress() {
3802
+ const progressBar = document.getElementById('progressBar');
3803
+ if (progressBar) {
3804
+ progressBar.style.width = '0%';
3805
+ }
3806
+ }
3807
+
3808
+ // Loading Overlay Functions
3809
+ function showLoading(message = 'در حال بارگذاری...') {
3810
+ const overlay = document.getElementById('loadingOverlay');
3811
+ const text = document.getElementById('loadingText');
3812
+ if (overlay) {
3813
+ overlay.classList.add('show');
3814
+ }
3815
+ if (text) {
3816
+ text.textContent = message;
3817
+ }
3818
+ }
3819
+
3820
+ function hideLoading() {
3821
+ const overlay = document.getElementById('loadingOverlay');
3822
+ if (overlay) {
3823
+ overlay.classList.remove('show');
3824
+ }
3825
+ }
3826
+
3827
+ // Feedback Overlay Functions
3828
+ function showFeedback(type, title, message) {
3829
+ const overlay = document.getElementById('feedbackOverlay');
3830
+ const icon = document.getElementById('feedbackIcon');
3831
+ const titleEl = document.getElementById('feedbackTitle');
3832
+ const messageEl = document.getElementById('feedbackMessage');
3833
+
3834
+ if (overlay && icon && titleEl && messageEl) {
3835
+ const icons = {
3836
+ success: '✅',
3837
+ error: '❌',
3838
+ warning: '⚠️'
3839
+ };
3840
+
3841
+ icon.textContent = icons[type] || icons.success;
3842
+ titleEl.textContent = title;
3843
+ messageEl.textContent = message;
3844
+
3845
+ overlay.classList.add('show');
3846
+
3847
+ // Auto hide after 3 seconds
3848
+ setTimeout(() => {
3849
+ hideFeedback();
3850
+ }, 3000);
3851
+ }
3852
+ }
3853
+
3854
+ function hideFeedback() {
3855
+ const overlay = document.getElementById('feedbackOverlay');
3856
+ if (overlay) {
3857
+ overlay.classList.remove('show');
3858
+ }
3859
+ }
3860
+
3861
+ // Scroll to Top Function
3862
+ function scrollToTop() {
3863
+ window.scrollTo({
3864
+ top: 0,
3865
+ behavior: 'smooth'
3866
+ });
3867
+ }
3868
+
3869
+ // Show FAB when scrolling down
3870
+ let lastScroll = 0;
3871
+ window.addEventListener('scroll', () => {
3872
+ const fab = document.querySelector('.fab');
3873
+ if (fab) {
3874
+ const currentScroll = window.pageYOffset;
3875
+ if (currentScroll > 300) {
3876
+ fab.style.opacity = '1';
3877
+ fab.style.pointerEvents = 'all';
3878
+ } else {
3879
+ fab.style.opacity = '0';
3880
+ fab.style.pointerEvents = 'none';
3881
+ }
3882
+ lastScroll = currentScroll;
3883
+ }
3884
+ });
3885
+
3886
+ // Filter Market Table Function
3887
+ let currentFilter = 'all';
3888
+ let marketDataCache = [];
3889
+
3890
+ function filterMarketTable() {
3891
+ const searchInput = document.getElementById('marketSearch');
3892
+ const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
3893
+ const tbody = document.getElementById('marketTableBody');
3894
+
3895
+ if (!tbody) return;
3896
+
3897
+ const rows = tbody.querySelectorAll('tr');
3898
+ let visibleCount = 0;
3899
+
3900
+ // Remove existing no-results row
3901
+ const existingNoResults = tbody.querySelector('tr[data-no-results]');
3902
+ if (existingNoResults) {
3903
+ existingNoResults.remove();
3904
+ }
3905
+
3906
+ rows.forEach((row, index) => {
3907
+ if (row.querySelector('td[colspan]')) {
3908
+ return; // Skip loading/error rows
3909
+ }
3910
+
3911
+ const cells = row.querySelectorAll('td');
3912
+ if (cells.length < 4) return;
3913
+
3914
+ const name = cells[1]?.textContent?.toLowerCase() || '';
3915
+ const symbol = cells[1]?.querySelector('.crypto-symbol')?.textContent?.toLowerCase() || '';
3916
+ const changeText = cells[3]?.textContent || '';
3917
+ const changeValue = parseFloat(changeText.replace(/[^0-9.-]/g, '')) || 0;
3918
+
3919
+ let matchesSearch = !searchTerm || name.includes(searchTerm) || symbol.includes(searchTerm);
3920
+ let matchesFilter = true;
3921
+
3922
+ if (currentFilter === 'top10') {
3923
+ matchesFilter = index < 10;
3924
+ } else if (currentFilter === 'gainers') {
3925
+ matchesFilter = changeValue > 0;
3926
+ } else if (currentFilter === 'losers') {
3927
+ matchesFilter = changeValue < 0;
3928
+ } else if (currentFilter === 'volume') {
3929
+ // This would need volume data - for now show all
3930
+ matchesFilter = true;
3931
+ }
3932
+
3933
+ if (matchesSearch && matchesFilter) {
3934
+ row.style.display = '';
3935
+ visibleCount++;
3936
+ row.style.animation = `rowSlideIn 0.3s ease-out`;
3937
+ row.style.animationDelay = `${index * 0.05}s`;
3938
+ } else {
3939
+ row.style.display = 'none';
3940
+ }
3941
+ });
3942
+
3943
+ // Show message if no results
3944
+ if (visibleCount === 0 && rows.length > 0 && !searchTerm && currentFilter === 'all') {
3945
+ // Don't show message if no search/filter is applied
3946
+ return;
3947
+ }
3948
+
3949
+ if (visibleCount === 0) {
3950
+ const noResultsRow = document.createElement('tr');
3951
+ noResultsRow.setAttribute('data-no-results', 'true');
3952
+ noResultsRow.innerHTML = `<td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);">
3953
+ <div style="font-size: 48px; margin-bottom: 10px;">🔍</div>
3954
+ <div style="font-weight: 600; margin-bottom: 5px;">نتیجه‌ای یافت نشد</div>
3955
+ <div style="font-size: 14px;">لطفاً عبارت جستجوی دیگری را امتحان کنید</div>
3956
+ </td>`;
3957
+ tbody.appendChild(noResultsRow);
3958
+ }
3959
+ }
3960
+
3961
+ function filterByCategory(category) {
3962
+ currentFilter = category;
3963
+
3964
+ // Update active chip
3965
+ document.querySelectorAll('.filter-chip').forEach(chip => {
3966
+ chip.classList.remove('active');
3967
+ });
3968
+ if (event && event.target) {
3969
+ event.target.classList.add('active');
3970
+ }
3971
+
3972
+ filterMarketTable();
3973
+ }
3974
+
3975
+ // Number Counter Animation
3976
+ function animateNumber(element, from, to, duration = 1000) {
3977
+ if (!element) return;
3978
+
3979
+ const start = performance.now();
3980
+ const difference = to - from;
3981
+
3982
+ function update(currentTime) {
3983
+ const elapsed = currentTime - start;
3984
+ const progress = Math.min(elapsed / duration, 1);
3985
+
3986
+ // Easing function
3987
+ const easeOutQuart = 1 - Math.pow(1 - progress, 4);
3988
+ const current = from + (difference * easeOutQuart);
3989
+
3990
+ element.textContent = typeof to === 'number' && to >= 1000
3991
+ ? current.toLocaleString('fa-IR', { maximumFractionDigits: 2 })
3992
+ : current.toFixed(2);
3993
+
3994
+ if (progress < 1) {
3995
+ requestAnimationFrame(update);
3996
+ } else {
3997
+ element.classList.add('updated');
3998
+ setTimeout(() => element.classList.remove('updated'), 500);
3999
+ }
4000
+ }
4001
+
4002
+ requestAnimationFrame(update);
4003
  }
4004
 
4005
  // Close modals when clicking outside